playlists (empty) → 0.1.0.0
raw patch · 12 files changed
+624/−0 lines, 12 filesdep +attoparsecdep +basedep +bytestringsetup-changed
Dependencies added: attoparsec, base, bytestring, hspec, optparse-applicative, playlists, text, word8
Files
- LICENSE +30/−0
- Setup.hs +2/−0
- playlist.hs +72/−0
- playlists.cabal +99/−0
- src/Text/Playlist.hs +54/−0
- src/Text/Playlist/Internal/Attoparsec.hs +50/−0
- src/Text/Playlist/M3U/Reader.hs +72/−0
- src/Text/Playlist/M3U/Writer.hs +40/−0
- src/Text/Playlist/PLS/Reader.hs +86/−0
- src/Text/Playlist/PLS/Writer.hs +56/−0
- src/Text/Playlist/Types.hs +37/−0
- test/Main.hs +26/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2013, Peter Jones <pjones@devalot.com>++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Peter Jones nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ playlist.hs view
@@ -0,0 +1,72 @@+{-++This file is part of the Haskell package playlists. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/playlists/LICENSE. No part+of playlists package, including this file, may be copied, modified,+propagated, or distributed except according to the terms contained in+the LICENSE file.++-}++--------------------------------------------------------------------------------+module Main where++--------------------------------------------------------------------------------+import qualified Data.ByteString as BS+import qualified Data.ByteString.Lazy as BL+import Data.Monoid+import qualified Data.Text as T+import Options.Applicative+import Text.Playlist++--------------------------------------------------------------------------------+data Command = CmdURLs Format | CmdConvert Format Format++--------------------------------------------------------------------------------+urlCommandOptions :: Parser Command+urlCommandOptions = CmdURLs+ <$> option (short 'f' <> long "format" <> metavar "FORMAT" <> help "Input format")++--------------------------------------------------------------------------------+convertCommandOptions :: Parser Command+convertCommandOptions = CmdConvert+ <$> option (short 'f' <> long "from" <> metavar "FORMAT" <> help "Input format")+ <*> option (short 't' <> long "to" <> metavar "FORMAT" <> help "Output format")++--------------------------------------------------------------------------------+commands :: Parser Command+commands = subparser+ (command "urls" (info urlCommandOptions (progDesc urlsDesc)) <>+ command "convert" (info convertCommandOptions (progDesc convertDesc)))+ where+ urlsDesc = "Dump all URLs (or file names) from a playlist"+ convertDesc = "Convert a playlist from one format to another"++--------------------------------------------------------------------------------+readPlaylist :: Format -> IO Playlist+readPlaylist fmt = do+ content <- BS.getContents+ case parsePlaylist fmt content of+ Left err -> fail $ "failed to parse playlist on stdin: " ++ err+ Right x -> return x++--------------------------------------------------------------------------------+writePlaylist :: Format -> Playlist -> IO ()+writePlaylist fmt plst = BL.putStr $ generatePlaylist fmt plst++--------------------------------------------------------------------------------+dumpURLs :: Format -> IO ()+dumpURLs fmt = mapM_ (putStrLn . T.unpack . trackURL) =<< readPlaylist fmt++--------------------------------------------------------------------------------+convertPlaylist :: Format -> Format -> IO ()+convertPlaylist fmtA fmtB = readPlaylist fmtA >>= writePlaylist fmtB++--------------------------------------------------------------------------------+main :: IO ()+main = do+ cmd <- execParser $ info (commands <**> helper) idm+ case cmd of+ CmdURLs x -> dumpURLs x+ CmdConvert x y -> convertPlaylist x y
+ playlists.cabal view
@@ -0,0 +1,99 @@+name: playlists+version: 0.1.0.0+synopsis: Library and executable for working with playlist files.+homepage: https://github.com/pjones/playlists+license: BSD3+license-file: LICENSE+author: Peter Jones <pjones@devalot.com>+maintainer: Peter Jones <pjones@devalot.com>+copyright: Copyright (c) 2013 Peter Jones <pjones@devalot.com>+category: Text+build-type: Simple+cabal-version: >=1.8+description: + Playlists is a library for working with media playlist files. The+ original motivation for the library was extracting URLs for+ streaming radio stations that use PLS and M3U playlist files.+ .+ The package also includes an executable that can dump the URLs from+ a playlist file and convert between playlist file formats.+ .+ /Example:/+ .+ > import qualified Data.ByteString as BS+ > import Text.Playlist+ > + > readPlaylist :: Format -> IO Playlist+ > readPlaylist fmt = do+ > content <- BS.getContents+ > case parsePlaylist fmt content of+ > Left err -> fail $ "failed to parse playlist on stdin: " ++ err+ > Right x -> return x+ .+ /Playlist Executable Examples:/+ .+ > $ playlist urls --format PLS < somefile.pls+ >+ > $ playlist convert --from PLS --to M3U < somefile.pls++--------------------------------------------------------------------------------+-- Work around a bug in Cabal < 1.16.0.3.+flag profiling+ description: Build with executable profiling enabled+ default: False++--------------------------------------------------------------------------------+source-repository head+ type: git+ location: https://github.com/pjones/playlists.git++--------------------------------------------------------------------------------+library+ exposed-modules:+ Text.Playlist+ other-modules:+ Text.Playlist.Types+ Text.Playlist.M3U.Reader+ Text.Playlist.M3U.Writer+ Text.Playlist.PLS.Reader+ Text.Playlist.PLS.Writer+ Text.Playlist.Internal.Attoparsec++ hs-source-dirs: src+ ghc-options: -Wall+ ghc-prof-options: -fprof-auto-top+ extensions: OverloadedStrings+ build-depends: base >= 4.6 && < 5+ , attoparsec >= 0.10 && < 0.11+ , bytestring >= 0.10 && < 0.11+ , text >= 0.11 && < 0.12+ , word8 >= 0.0 && < 0.1++--------------------------------------------------------------------------------+executable playlist+ main-is: playlist.hs+ ghc-options: -Wall -rtsopts + ghc-prof-options: -fprof-auto-top -fprof-cafs+ extensions: OverloadedStrings++ -- Work around a bug in Cabal < 1.16.0.3+ -- if flag(profiling)+ -- ghc-options: -prof -fprof-auto-top -fprof-cafs++ build-depends: base+ , bytestring+ , optparse-applicative >= 0.5 && < 0.6+ , playlists+ , text++--------------------------------------------------------------------------------+test-suite spec+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ ghc-options: -Wall+ extensions: OverloadedStrings+ main-is: Main.hs+ build-depends: base+ , bytestring+ , hspec >= 1.4.0 && < 1.5+ , playlists
+ src/Text/Playlist.hs view
@@ -0,0 +1,54 @@+{-++This file is part of the Haskell package playlists. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/playlists/LICENSE. No part+of playlists package, including this file, may be copied, modified,+propagated, or distributed except according to the terms contained in+the LICENSE file.++-}++--------------------------------------------------------------------------------+module Text.Playlist+ ( -- * Playlist Types+ Track (..)+ , Playlist+ -- * Playlist Formats+ , Format (..)+ -- * Parsing and Generating+ , parsePlaylist+ , generatePlaylist+ ) where++--------------------------------------------------------------------------------+import qualified Data.Attoparsec.ByteString as Atto+import Data.ByteString (ByteString)+import qualified Data.ByteString.Lazy as BL+import qualified Data.ByteString.Lazy.Builder as BL+import qualified Text.Playlist.M3U.Reader as M3U+import qualified Text.Playlist.M3U.Writer as M3U+import qualified Text.Playlist.PLS.Reader as PLS+import qualified Text.Playlist.PLS.Writer as PLS+import Text.Playlist.Types++--------------------------------------------------------------------------------+-- | Parse a playlist from a @ByteString@. Parsing may fail in which+-- case an error message is returned in @Left@.+--+-- > content <- BS.getContents+-- > case parsePlaylist M3U content of+-- > Left err -> fail $ "failed to parse playlist: " ++ err+-- > Right x -> return x+parsePlaylist :: Format -> ByteString -> Either String Playlist+parsePlaylist M3U = Atto.parseOnly M3U.parsePlaylist+parsePlaylist PLS = Atto.parseOnly PLS.parsePlaylist++--------------------------------------------------------------------------------+-- | Generate a lazy @ByteString@ containing playlist data from the+-- given playlist and in the given format.+--+-- > BL.putStr $ generatePlaylist M3U somePlaylist+generatePlaylist :: Format -> Playlist -> BL.ByteString+generatePlaylist M3U = BL.toLazyByteString . M3U.writePlaylist+generatePlaylist PLS = BL.toLazyByteString . PLS.writePlaylist
+ src/Text/Playlist/Internal/Attoparsec.hs view
@@ -0,0 +1,50 @@+{-++This file is part of the Haskell package playlists. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/playlists/LICENSE. No part+of playlists package, including this file, may be copied, modified,+propagated, or distributed except according to the terms contained in+the LICENSE file.++-}++--------------------------------------------------------------------------------+-- | Helper functions for @Attoparsec@ and @ByteString@.+module Text.Playlist.Internal.Attoparsec+ ( isEOL+ , isEq+ , skipEq+ , skipSpace+ , skipLine+ ) where++--------------------------------------------------------------------------------+import Data.Attoparsec.ByteString+import Data.Word (Word8)+import Data.Word8 (isSpace)++--------------------------------------------------------------------------------+-- | True if the given @Word8@ is an end of line character.+isEOL :: Word8 -> Bool+isEOL x = x == 10 || x == 13++--------------------------------------------------------------------------------+-- | True if the given @Word8@ is an equal sign.+isEq :: Word8 -> Bool+isEq = (== 61)++--------------------------------------------------------------------------------+-- | Skip an equal sign and any space around it.+skipEq :: Parser ()+skipEq = skipSpace >> skip isEq >> skipSpace++--------------------------------------------------------------------------------+-- | Skip all whitespace.+skipSpace :: Parser ()+skipSpace = skipWhile isSpace++--------------------------------------------------------------------------------+-- | Skip all characters up to and including the next EOL.+skipLine :: Parser ()+skipLine = skipWhile (not . isEOL) >> skipSpace
+ src/Text/Playlist/M3U/Reader.hs view
@@ -0,0 +1,72 @@+{-++This file is part of the Haskell package playlists. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/playlists/LICENSE. No part+of playlists package, including this file, may be copied, modified,+propagated, or distributed except according to the terms contained in+the LICENSE file.++-}++--------------------------------------------------------------------------------+module Text.Playlist.M3U.Reader (parsePlaylist) where++--------------------------------------------------------------------------------+import Control.Applicative+import Control.Monad (msum, void)+import Data.Attoparsec.ByteString+import Data.Text (Text)+import Data.Text.Encoding (decodeUtf8)+import Data.Word8 (isDigit)+import Text.Playlist.Internal.Attoparsec+import Text.Playlist.Types++--------------------------------------------------------------------------------+-- | Parser for a complete M3U playlist.+parsePlaylist :: Parser Playlist+parsePlaylist = do+ ts <- many1 parseTrack+ void (many' commentOrTitle) -- Trailing comments.+ return ts++--------------------------------------------------------------------------------+-- | Parser for a single track in a M3U file.+parseTrack :: Parser Track+parseTrack = do+ -- Get the title closest to the URL or Nothing.+ title <- msum . reverse <$> many' commentOrTitle+ url <- parseURL++ return Track { trackURL = url+ , trackTitle = title+ }++--------------------------------------------------------------------------------+-- | Parser for URL or file name in a M3U file. The URL is the entire+-- line so this parser extracts the entire line and decodes it.+parseURL :: Parser Text+parseURL = decodeUtf8 <$> takeWhile1 (not . isEOL) <* skipSpace++--------------------------------------------------------------------------------+-- | Comment parser with a twist. In the extended M3U format metadata+-- for a track can be placed in a comment that appears just before the+-- URL. This parser succeeds if the current line is a comment, and+-- always skips over the entire comment. If the comment represents a+-- track title then that information will be returned in a @Just@. If+-- it's just a regular comment then @Nothing@ is returned.+commentOrTitle :: Parser (Maybe Text)+commentOrTitle = do+ skipSpace+ skip (== 35) -- Comment character "#"+ istitle <- (string "EXTINF:" >> return True) <|> return False+ if istitle then title <|> comment else comment+ where+ comment = skipLine >> return Nothing+ title = do skip (== 45) <|> return () -- Skip optional negative sign.+ skipWhile isDigit -- Skip length.+ skip (== 44) -- Skip comma.+ skipSpace+ text <- decodeUtf8 <$> takeWhile1 (not . isEOL)+ skipSpace+ return $! Just text
+ src/Text/Playlist/M3U/Writer.hs view
@@ -0,0 +1,40 @@+{-++This file is part of the Haskell package playlists. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/playlists/LICENSE. No part+of playlists package, including this file, may be copied, modified,+propagated, or distributed except according to the terms contained in+the LICENSE file.++-}++--------------------------------------------------------------------------------+module Text.Playlist.M3U.Writer (writePlaylist) where++--------------------------------------------------------------------------------+import Data.ByteString.Lazy.Builder (Builder)+import qualified Data.ByteString.Lazy.Builder as B+import Data.Monoid+import Data.Text (Text)+import Data.Text.Encoding (encodeUtf8)+import Text.Playlist.Types++--------------------------------------------------------------------------------+writePlaylist :: Playlist -> Builder+writePlaylist x = B.byteString "#EXTM3U\n" <> mconcat (map writeTrack x)++--------------------------------------------------------------------------------+writeTrack :: Track -> Builder+writeTrack x =+ writeTitle (trackTitle x) <>+ B.byteString (encodeUtf8 $ trackURL x) <>+ B.charUtf8 '\n'++--------------------------------------------------------------------------------+writeTitle :: Maybe Text -> Builder+writeTitle Nothing = mempty+writeTitle (Just x) =+ B.byteString "#EXTINF:-1," <>+ B.byteString (encodeUtf8 x) <>+ B.charUtf8 '\n'
+ src/Text/Playlist/PLS/Reader.hs view
@@ -0,0 +1,86 @@+{-++This file is part of the Haskell package playlists. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/playlists/LICENSE. No part+of playlists package, including this file, may be copied, modified,+propagated, or distributed except according to the terms contained in+the LICENSE file.++-}++--------------------------------------------------------------------------------+module Text.Playlist.PLS.Reader (parsePlaylist) where++--------------------------------------------------------------------------------+import Control.Applicative+import Control.Monad (void)+import Data.Attoparsec.ByteString+import Data.ByteString (ByteString)+import Data.Text (Text)+import Data.Text.Encoding (decodeUtf8)+import Data.Word8 (isDigit)+import Text.Playlist.Internal.Attoparsec+import Text.Playlist.Types++--------------------------------------------------------------------------------+-- | A parser that will process an entire playlist.+parsePlaylist :: Parser Playlist+parsePlaylist = do+ parseHeader+ ts <- many1 parseTrack+ skipMany skipUnusedLine+ return ts++--------------------------------------------------------------------------------+-- | A pls header will at least contain the "[playlist]" bit but some+-- files also include the lines you'd expect in the footer too.+parseHeader :: Parser ()+parseHeader = do+ skipSpace >> string "[playlist]" >> skipSpace+ skipMany skipUnusedLine++--------------------------------------------------------------------------------+-- | Parse a single track. Tracks begin with "FileN" where N is a+-- digit. They are followed by an optional title and optional length.+parseTrack :: Parser Track+parseTrack = do+ (n, url) <- parseFileN+ title <- (Just <$> parseTitle n) <|> return Nothing++ -- Skip optional length field, we don't use it.+ (skipSpace >> string "Length" >> skipLine) <|> return ()++ return Track { trackURL = url+ , trackTitle = title+ }++--------------------------------------------------------------------------------+-- | Skip any line that isn't part of a track.+skipUnusedLine :: Parser ()+skipUnusedLine =+ (string "numberofentries" <|>+ string "NumberOfEntries" <|>+ string "version" <|>+ string "Version") >> skipLine++--------------------------------------------------------------------------------+-- | Parser for the "FileN" line that contains the track number and+-- URL for the track. The result is a pair where the first member is+-- the track number and the second member is the URL.+parseFileN :: Parser (ByteString, Text)+parseFileN = do+ skipSpace+ n <- string "File" >> takeWhile1 isDigit+ skipEq+ url <- takeWhile1 (not . isEOL)+ return (n, decodeUtf8 url)++--------------------------------------------------------------------------------+-- | Parser for the title line with the given track number.+parseTitle :: ByteString -> Parser Text+parseTitle n = do+ skipSpace+ void (string "Title" >> string n)+ skipEq+ decodeUtf8 <$> takeWhile1 (not . isEOL)
+ src/Text/Playlist/PLS/Writer.hs view
@@ -0,0 +1,56 @@+{-++This file is part of the Haskell package playlists. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/playlists/LICENSE. No part+of playlists package, including this file, may be copied, modified,+propagated, or distributed except according to the terms contained in+the LICENSE file.++-}++--------------------------------------------------------------------------------+module Text.Playlist.PLS.Writer+ ( writePlaylist+ , writeTrack+ ) where++--------------------------------------------------------------------------------+import Data.ByteString.Lazy.Builder (Builder)+import qualified Data.ByteString.Lazy.Builder as B+import Data.Monoid+import Data.Text.Encoding (encodeUtf8)+import Text.Playlist.Types++--------------------------------------------------------------------------------+writePlaylist :: Playlist -> Builder+writePlaylist x =+ B.byteString "[playlist]\n" <>+ mconcat (map writeTrack $ zip x [1..]) <>+ B.byteString "NumberOfEntries=" <>+ B.stringUtf8 (show . length $ x) <>+ B.byteString "\nVersion=2\n"++--------------------------------------------------------------------------------+writeTrack :: (Track, Int) -> Builder+writeTrack x = writeFileN x <> writeTitle x++--------------------------------------------------------------------------------+writeFileN :: (Track, Int) -> Builder+writeFileN (x, n) =+ B.byteString "File" <>+ B.stringUtf8 (show n) <>+ B.charUtf8 '=' <>+ B.byteString (encodeUtf8 . trackURL $ x) <>+ B.charUtf8 '\n'++--------------------------------------------------------------------------------+writeTitle :: (Track, Int) -> Builder+writeTitle (x, n) =+ case trackTitle x of+ Nothing -> mempty+ Just y -> B.byteString "Title" <>+ B.stringUtf8 (show n) <>+ B.charUtf8 '=' <>+ B.byteString (encodeUtf8 y) <>+ B.charUtf8 '\n'
+ src/Text/Playlist/Types.hs view
@@ -0,0 +1,37 @@+{-++This file is part of the Haskell package playlists. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/playlists/LICENSE. No part+of playlists package, including this file, may be copied, modified,+propagated, or distributed except according to the terms contained in+the LICENSE file.++-}++--------------------------------------------------------------------------------+module Text.Playlist.Types+ ( Track (..)+ , Playlist+ , Format (..)+ ) where++--------------------------------------------------------------------------------+import Data.Text (Text)++--------------------------------------------------------------------------------+-- | A single music file or streaming URL.+data Track = Track+ { trackURL :: Text -- ^ URL for a file or streaming resource.+ , trackTitle :: Maybe Text -- ^ Optional title.+ } deriving (Show, Eq)++--------------------------------------------------------------------------------+-- | A list of 'Track's.+type Playlist = [Track]++--------------------------------------------------------------------------------+-- | Playlist formats.+data Format = PLS -- ^ <http://en.wikipedia.org/wiki/PLS_(file_format)>+ | M3U -- ^ M3U and M3U8. <http://en.wikipedia.org/wiki/M3U>+ deriving (Read, Show, Eq)
+ test/Main.hs view
@@ -0,0 +1,26 @@+{-++This file is part of the Haskell package playlists. It is subject to+the license terms in the LICENSE file found in the top-level directory+of this distribution and at git://pmade.com/playlists/LICENSE. No part+of playlists package, including this file, may be copied, modified,+propagated, or distributed except according to the terms contained in+the LICENSE file.++-}++--------------------------------------------------------------------------------+module Main where++--------------------------------------------------------------------------------+import Test.Hspec++--------------------------------------------------------------------------------+import qualified M3USpec+import qualified PLSSpec++--------------------------------------------------------------------------------+main :: IO ()+main = hspec $ do+ describe "M3U" M3USpec.spec+ describe "PLS" PLSSpec.spec