playlists 0.3.0.0 → 0.4.0.0
raw patch · 20 files changed
+400/−197 lines, 20 filesdep −hlintdep ~basedep ~filepathdep ~optparse-applicativePVP ok
version bump matches the API change (PVP)
Dependencies removed: hlint
Dependency ranges changed: base, filepath, optparse-applicative, text
API changes (from Hackage documentation)
- Text.Playlist: trackTitle :: Track -> Maybe Text
- Text.Playlist: trackURL :: Track -> Text
+ Text.Playlist: [trackTitle] :: Track -> Maybe Text
+ Text.Playlist: [trackURL] :: Track -> Text
+ Text.Playlist: parserForFormat :: Format -> Parser Playlist
+ Text.Playlist: resolve :: forall m. (Monad m) => Playlist -> (Text -> m Playlist) -> m Playlist
Files
- CHANGELOG +0/−3
- CHANGES.md +17/−0
- LICENSE +1/−1
- playlist.hs +0/−72
- playlists.cabal +33/−38
- src/Text/Playlist.hs +9/−56
- src/Text/Playlist/Internal/Format.hs +52/−0
- src/Text/Playlist/Internal/ReadWrite.hs +56/−0
- src/Text/Playlist/Internal/Resolve.hs +94/−0
- src/Text/Playlist/M3U/Reader.hs +2/−0
- src/Text/Playlist/M3U/Writer.hs +2/−0
- src/Text/Playlist/PLS/Reader.hs +2/−0
- src/Text/Playlist/PLS/Writer.hs +2/−0
- test/Examples.hs +2/−0
- test/Main.hs +4/−2
- test/ResolveSpec.hs +48/−0
- test/doctest.hs +2/−2
- test/hlint.hs +0/−23
- test/nested.m3u +2/−0
- util/playlist.hs +72/−0
− CHANGELOG
@@ -1,3 +0,0 @@-# Version 0.3.0.0-- * Update dependencies to recent versions (thanks to Wieland Hoffmann).
+ CHANGES.md view
@@ -0,0 +1,17 @@+# Version 0.4.0.0 (November 21, 2016)++ * Added the `resolve` function. If you have a playlist that+ contains references to remote playlists, this new function will+ recursively download and process playlists until it has a flat+ playlist with no further remote references.++ See the `playlists-http` package for an example of using the+ `resolve` function.++ * Added the `parserForFormat` function which directly exposes the+ `attoparsec` parser for each playlist type. This should make it+ easy to write space efficient functions that parse playlists.++# Version 0.3.0.0 (January 24, 2015)++ * Update dependencies to recent versions (thanks to Wieland Hoffmann).
LICENSE view
@@ -1,4 +1,4 @@-Copyright (c) 2013,2014 Peter Jones <pjones@devalot.com>+Copyright (c) 2013-2016 Peter Jones <pjones@devalot.com> All rights reserved.
− playlist.hs
@@ -1,72 +0,0 @@-{---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 auto (short 'f' <> long "format" <> metavar "FORMAT" <> help "Input format")-----------------------------------------------------------------------------------convertCommandOptions :: Parser Command-convertCommandOptions = CmdConvert- <$> option auto (short 'f' <> long "from" <> metavar "FORMAT" <> help "Input format")- <*> option auto (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
@@ -1,16 +1,16 @@ name: playlists-version: 0.3.0.0+version: 0.4.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,2014 Peter Jones+copyright: Copyright (c) 2013-2016 Peter Jones category: Text build-type: Simple-cabal-version: >=1.8-description: +cabal-version: >= 1.18+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.@@ -22,7 +22,7 @@ . > import qualified Data.ByteString as BS > import Text.Playlist- > + > > readPlaylist :: Format -> IO Playlist > readPlaylist fmt = do > content <- BS.getContents@@ -39,10 +39,9 @@ -------------------------------------------------------------------------------- extra-source-files: README.md- CHANGELOG+ CHANGES.md test/*.m3u test/*.pls- test/*.hs -------------------------------------------------------------------------------- source-repository head@@ -56,13 +55,6 @@ manual: True --------------------------------------------------------------------------------- You can disable the hlint test suite with -f-test-hlint-flag test-hlint- description: Run the source code through hlint when testing.- default: True- manual: True-------------------------------------------------------------------------------- -- You can disable the doctest test suite with -f-test-doctest flag test-doctest description: Run the source code through doctest when testing.@@ -74,17 +66,19 @@ exposed-modules: Text.Playlist other-modules:- Text.Playlist.Types+ Text.Playlist.Internal.Attoparsec+ Text.Playlist.Internal.Format+ Text.Playlist.Internal.ReadWrite+ Text.Playlist.Internal.Resolve Text.Playlist.M3U.Reader Text.Playlist.M3U.Writer Text.Playlist.PLS.Reader Text.Playlist.PLS.Writer- Text.Playlist.Internal.Attoparsec+ Text.Playlist.Types + default-language: Haskell2010 hs-source-dirs: src ghc-options: -Wall- ghc-prof-options: -fprof-auto-top- extensions: OverloadedStrings if flag(maintainer) ghc-options: -Werror@@ -98,52 +92,53 @@ -------------------------------------------------------------------------------- executable playlist- hs-source-dirs: ./+ default-language: Haskell2010+ hs-source-dirs: util main-is: playlist.hs- ghc-options: -Wall -rtsopts - ghc-prof-options: -fprof-auto-top -fprof-cafs- extensions: OverloadedStrings+ ghc-options: -Wall -rtsopts if flag(maintainer) ghc-options: -Werror+ ghc-prof-options: -fprof-auto-top -fprof-cafs build-depends: base , bytestring- , optparse-applicative >= 0.10.0 && < 0.12.0+ , optparse-applicative >= 0.10.0 && < 0.13.0 , playlists , text -------------------------------------------------------------------------------- test-suite spec type: exitcode-stdio-1.0+ default-language: Haskell2010 hs-source-dirs: test main-is: Main.hs ghc-options: -Wall- extensions: OverloadedStrings++ other-modules:+ Examples+ Helper+ M3USpec+ PLSSpec+ ResolveSpec++ if flag(maintainer)+ ghc-options: -Werror+ build-depends: base , bytestring+ , filepath , hspec >= 1.4.0 , playlists---------------------------------------------------------------------------------test-suite hlint- type: exitcode-stdio-1.0- hs-source-dirs: test- main-is: hlint.hs- ghc-options: -w-- if !flag(test-hlint)- buildable: False- else- build-depends: base- , hlint >= 1.7+ , text ------------------------------------------------------------------------------ test-suite doctests type: exitcode-stdio-1.0- ghc-options: -threaded+ default-language: Haskell2010 hs-source-dirs: test main-is: doctest.hs+ ghc-options: -threaded if !flag(test-doctest) buildable: False
src/Text/Playlist.hs view
@@ -14,70 +14,23 @@ ( -- * Playlist Types Track (..) , Playlist+ -- * Playlist Formats , Format (..)+ -- * Parsing and Generating , parsePlaylist+ , parserForFormat , generatePlaylist++ -- * Utility Functions , fileNameToFormat , appendExtension+ , resolve ) 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 System.FilePath (takeExtension)-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.Internal.Format+import Text.Playlist.Internal.ReadWrite 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------------------------------------------------------------------------------------- | Try to figure out a file's format from it's file extension.------ >>> fileNameToFormat "foo.m3u"--- Just M3U------ >>> fileNameToFormat "foo.txt"--- Nothing-fileNameToFormat :: FilePath -> Maybe Format-fileNameToFormat ext = case takeExtension ext of- ".m3u" -> Just M3U- ".m3u8" -> Just M3U- ".pls" -> Just PLS- _ -> Nothing------------------------------------------------------------------------------------- | Given a file name that does not have a file extension, return a--- file name with the appropriate extension included based on the--- given format.------ >>> appendExtension M3U "foo"--- "foo.m3u"-appendExtension :: Format -> FilePath -> FilePath-appendExtension M3U = (++ ".m3u")-appendExtension PLS = (++ ".pls")+import Text.Playlist.Internal.Resolve
+ src/Text/Playlist/Internal/Format.hs view
@@ -0,0 +1,52 @@+{-++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.Internal.Format+ ( fileNameToFormat+ , appendExtension+ ) where++--------------------------------------------------------------------------------+import Data.Char (toLower)+import System.FilePath (takeExtension)++--------------------------------------------------------------------------------+import Text.Playlist.Types++--------------------------------------------------------------------------------+-- | Try to figure out a file's format from it's file extension.+--+-- >>> fileNameToFormat "foo.m3u"+-- Just M3U+--+-- >>> fileNameToFormat "foo.M3U"+-- Just M3U+--+-- >>> fileNameToFormat "foo.txt"+-- Nothing+fileNameToFormat :: FilePath -> Maybe Format+fileNameToFormat ext = case map toLower $ takeExtension ext of+ ".m3u" -> Just M3U+ ".m3u8" -> Just M3U+ ".pls" -> Just PLS+ _ -> Nothing++--------------------------------------------------------------------------------+-- | Given a file name that does not have a file extension, return a+-- file name with the appropriate extension included based on the+-- given format.+--+-- >>> appendExtension M3U "foo"+-- "foo.m3u"+appendExtension :: Format -> FilePath -> FilePath+appendExtension M3U = (++ ".m3u")+appendExtension PLS = (++ ".pls")
+ src/Text/Playlist/Internal/ReadWrite.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.Internal.ReadWrite+ ( parserForFormat+ , 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++--------------------------------------------------------------------------------+-- | Return the appropriate attoparsec parser for the given playlist format.+parserForFormat :: Format -> Atto.Parser Playlist+parserForFormat M3U = M3U.parsePlaylist+parserForFormat PLS = PLS.parsePlaylist++--------------------------------------------------------------------------------+-- | 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 f = Atto.parseOnly (parserForFormat f)++--------------------------------------------------------------------------------+-- | 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/Resolve.hs view
@@ -0,0 +1,94 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE RecordWildCards #-}+++{-++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.Internal.Resolve+ ( resolve+ ) where++--------------------------------------------------------------------------------+import Control.Monad+import Data.Text (Text)+import qualified Data.Text as Text++--------------------------------------------------------------------------------+import Text.Playlist.Internal.Format+import Text.Playlist.Types++--------------------------------------------------------------------------------+-- Internal type to track when a playlist may need to be processed a+-- another time. (Such as when a remote playlist refers to other+-- remote playlists.)+data Resolution = Flat Playlist | Again Playlist++--------------------------------------------------------------------------------+-- | If the given 'Playlist' contains tracks that reference remote+-- playlists, this function will recursively download and process+-- these playlists. Returns a flattened playlist that should not+-- contain any references to other playlists.+--+-- You supply the downloading function as the second argument. Use+-- whichever HTTP library that makes you happy.+--+-- There are two error conditions that are ignored by this function:+--+-- 1. The nesting of playlists exceeds a (hard-coded) limit. In+-- this case no playlists beyond the limit are processed. Open a+-- pull request if you'd like to have a resolveN function that+-- allows you to specific the depth limit or one that returns an+-- error.+--+-- 2. A downloaded playlist contains a syntax error. In this case+-- the playlist is consider to have no tracks and is ignored.+-- Open a pull request if you want a version of this function+-- that returns some sort of an error instead of ignoring bad+-- playlists.+resolve :: forall m. (Monad m)+ => Playlist+ -- ^ A 'Playlist' that may contain references to other+ -- playlists.++ -> (Text -> m Playlist)+ -- ^ Downloading function. This function should take a URL+ -- and return a parsed playlist.+ --+ -- It's expected that the URL points to another playlist that+ -- needs to be parsed and possibly resolved.++ -> m Playlist+ -- ^ A fully resolved 'Playlist'. (All tracks should be files+ -- and not links to other playlists.)+resolve playlist download = go 10 playlist where++ ----------------------------------------------------------------------------+ -- Recursively process tracks in the 'Playlist' with a maximum depth+ -- of @n@.+ go :: (Monad m) => Int -> Playlist -> m Playlist+ go _ [] = return []+ go 0 xs = return xs+ go n xs = fmap join $ forM xs $ \track -> do+ r <- process track++ case r of+ Flat p -> return p+ Again p -> go (n - 1) p++ ----------------------------------------------------------------------------+ -- Process a single track.+ process :: Track -> m Resolution+ process t@Track {..} =+ case fileNameToFormat (Text.unpack trackURL) of+ Nothing -> return (Flat [t])+ Just _ -> Again <$> download trackURL
src/Text/Playlist/M3U/Reader.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE OverloadedStrings #-}+ {- This file is part of the Haskell package playlists. It is subject to
src/Text/Playlist/M3U/Writer.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE OverloadedStrings #-}+ {- This file is part of the Haskell package playlists. It is subject to
src/Text/Playlist/PLS/Reader.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE OverloadedStrings #-}+ {- This file is part of the Haskell package playlists. It is subject to
src/Text/Playlist/PLS/Writer.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE OverloadedStrings #-}+ {- This file is part of the Haskell package playlists. It is subject to
test/Examples.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE OverloadedStrings #-}+ {- This file is part of the Haskell package playlists. It is subject to
test/Main.hs view
@@ -18,9 +18,11 @@ -------------------------------------------------------------------------------- import qualified M3USpec import qualified PLSSpec+import qualified ResolveSpec -------------------------------------------------------------------------------- main :: IO () main = hspec $ do- describe "M3U" M3USpec.spec- describe "PLS" PLSSpec.spec+ describe "M3U" M3USpec.spec+ describe "PLS" PLSSpec.spec+ describe "Resolve" ResolveSpec.spec
+ test/ResolveSpec.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE OverloadedStrings #-}++{-++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 ResolveSpec (spec) where++--------------------------------------------------------------------------------+import Data.Text (Text)+import qualified Data.Text as Text+import Helper (playlistFromFile)+import System.FilePath (dropExtension, takeFileName)+import Test.Hspec++--------------------------------------------------------------------------------+import Text.Playlist++--------------------------------------------------------------------------------+spec :: Spec+spec =+ describe "Simple" $+ it "Nested" $ loadAndResolve M3U "nested" `shouldReturn` resolved++--------------------------------------------------------------------------------+loadAndResolve :: Format -> FilePath -> IO Playlist+loadAndResolve format file = do+ playlist <- playlistFromFile format file+ resolve playlist download+ where+ download :: Text -> IO Playlist+ download = playlistFromFile format . dropExtension .+ takeFileName . Text.unpack++--------------------------------------------------------------------------------+resolved :: Playlist+resolved = [ Track "http://foo.com" Nothing+ , Track "http://bar.com" Nothing+ , Track "http://foo.com/aa.baz.live" Nothing+ ]
test/doctest.hs view
@@ -21,14 +21,14 @@ -------------------------------------------------------------------------------- -- Totally stupid right now. Will search the directory later. files :: IO [String]-files = fmap ("playlist.hs" :) files' where+files = fmap ("util/playlist.hs" :) files' where files' = return ["src/Text/Playlist.hs"] -------------------------------------------------------------------------------- -- | GHC flags needed by doctest. Would be better to fetch these from -- cabal directly though. flags :: [String]-flags = ["-isrc", "-XOverloadedStrings"]+flags = ["-isrc"] -------------------------------------------------------------------------------- -- | Check the docs.
− test/hlint.hs
@@ -1,23 +0,0 @@--------------------------------------------------------------------------------- |--- Module : Main (hlint)--- Copyright : (C) 2013 Edward Kmett--- License : BSD-style (see the file https://github.com/ekmett/succinct/blob/master/LICENSE)--- Maintainer : Edward Kmett <ekmett@gmail.com>--- Stability : provisional--- Portability : portable------ This module runs HLint on the lens source tree.-------------------------------------------------------------------------------module Main where--import Control.Monad-import Language.Haskell.HLint-import System.Environment-import System.Exit--main :: IO ()-main = do- args <- getArgs- hints <- hlint $ ["src", "playlist.hs", "--cpp-define=HLINT"] ++ args- unless (null hints) exitFailure
+ test/nested.m3u view
@@ -0,0 +1,2 @@+http://foo.com/aa.m3u+http://foo.com/aa.baz.live
+ util/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 auto (short 'f' <> long "format" <> metavar "FORMAT" <> help "Input format")++--------------------------------------------------------------------------------+convertCommandOptions :: Parser Command+convertCommandOptions = CmdConvert+ <$> option auto (short 'f' <> long "from" <> metavar "FORMAT" <> help "Input format")+ <*> option auto (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