packages feed

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