diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/playlist.hs b/playlist.hs
new file mode 100644
--- /dev/null
+++ b/playlist.hs
@@ -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
diff --git a/playlists.cabal b/playlists.cabal
new file mode 100644
--- /dev/null
+++ b/playlists.cabal
@@ -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
diff --git a/src/Text/Playlist.hs b/src/Text/Playlist.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Playlist.hs
@@ -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
diff --git a/src/Text/Playlist/Internal/Attoparsec.hs b/src/Text/Playlist/Internal/Attoparsec.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Playlist/Internal/Attoparsec.hs
@@ -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
diff --git a/src/Text/Playlist/M3U/Reader.hs b/src/Text/Playlist/M3U/Reader.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Playlist/M3U/Reader.hs
@@ -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
diff --git a/src/Text/Playlist/M3U/Writer.hs b/src/Text/Playlist/M3U/Writer.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Playlist/M3U/Writer.hs
@@ -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'
diff --git a/src/Text/Playlist/PLS/Reader.hs b/src/Text/Playlist/PLS/Reader.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Playlist/PLS/Reader.hs
@@ -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)
diff --git a/src/Text/Playlist/PLS/Writer.hs b/src/Text/Playlist/PLS/Writer.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Playlist/PLS/Writer.hs
@@ -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'
diff --git a/src/Text/Playlist/Types.hs b/src/Text/Playlist/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Playlist/Types.hs
@@ -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)
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -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
