packages feed

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 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