diff --git a/LICENSE b/LICENSE
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2013, Peter Jones <pjones@devalot.com>
+Copyright (c) 2013,2014 Peter Jones <pjones@devalot.com>
 
 All rights reserved.
 
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,42 @@
+# Haskell Playlists Library and Tool
+
+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.
+
+## Supported Formats
+
+  * [PLS] []
+  * [M3U and M3U8] [M3U]
+
+## Future Plans
+
+Some playlist files can be really big.  I plan on adding support for
+incremental parsing and generating via [io-streams] [] at some point
+in the future.
+
+## Library 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
+
+
+## Executable Example
+
+    $ playlist urls --format PLS < somefile.pls
+
+    $ playlist convert --from PLS --to M3U < somefile.pls
+
+[pls]: http://en.wikipedia.org/wiki/PLS_(file_format)
+[m3u]: http://en.wikipedia.org/wiki/M3U
+[io-streams]: http://hackage.haskell.org/package/io-streams
diff --git a/playlists.cabal b/playlists.cabal
--- a/playlists.cabal
+++ b/playlists.cabal
@@ -1,12 +1,12 @@
 name:          playlists
-version:       0.1.0.0
+version:       0.2.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>
+copyright:     Copyright (c) 2013,2014 Peter Jones <pjones@devalot.com>
 category:      Text
 build-type:    Simple
 cabal-version: >=1.8
@@ -37,10 +37,18 @@
   > $ 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
+extra-source-files:
+  README.md
+  test/aa.m3u
+  test/ab.m3u
+  test/Examples.hs
+  test/Helper.hs
+  test/hp.m3u
+  test/M3USpec.hs
+  test/pig.pls
+  test/PLSSpec.hs
+  test/sa.pls
+  test/utf8.pls
 
 --------------------------------------------------------------------------------
 source-repository head
@@ -48,6 +56,23 @@
   location: https://github.com/pjones/playlists.git
 
 --------------------------------------------------------------------------------
+flag maintainer
+  description: Enable settings for the package maintainer.
+  default: False
+
+------------------------------------------------------------------------------
+-- You can disable the hlint test suite with -f-test-hlint
+flag test-hlint
+  default: True
+  manual: True
+
+------------------------------------------------------------------------------
+-- You can disable the doctest test suite with -f-test-doctest
+flag test-doctest
+  default: True
+  manual: True
+
+--------------------------------------------------------------------------------
 library
   exposed-modules:
     Text.Playlist
@@ -63,11 +88,16 @@
   ghc-options: -Wall
   ghc-prof-options: -fprof-auto-top
   extensions: OverloadedStrings
+
+  if flag(maintainer)
+    ghc-options: -Werror
+
   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
+               , attoparsec >= 0.10  && < 1.0
+               , bytestring >= 0.10  && < 1.0
+               , text       >= 0.11  && < 1.2
+               , word8      >= 0.0   && < 1.0
+               , filepath   >= 1.3   && < 2.0
 
 --------------------------------------------------------------------------------
 executable playlist
@@ -76,13 +106,12 @@
   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
+  if flag(maintainer)
+    ghc-options: -Werror
 
   build-depends: base
                , bytestring
-               , optparse-applicative >= 0.5 && < 0.6
+               , optparse-applicative >= 0.5 && < 1.0
                , playlists
                , text
 
@@ -95,5 +124,31 @@
   main-is: Main.hs
   build-depends: base
                , bytestring
-               , hspec >= 1.4.0 && < 1.5
+               , hspec >= 1.4.0
                , playlists
+
+------------------------------------------------------------------------------
+test-suite hlint
+  type: exitcode-stdio-1.0
+  main-is: hlint.hs
+  ghc-options: -w
+  hs-source-dirs: test
+
+  if !flag(test-hlint)
+    buildable: False
+  else
+    build-depends: base
+                 , hlint >= 1.7
+
+------------------------------------------------------------------------------
+test-suite doctests
+  type: exitcode-stdio-1.0
+  ghc-options: -threaded
+  main-is: doctest.hs
+  hs-source-dirs: test
+
+  if !flag(test-doctest)
+    buildable: False
+  else
+    build-depends: base
+                 , doctest >= 0.9.11
diff --git a/src/Text/Playlist.hs b/src/Text/Playlist.hs
--- a/src/Text/Playlist.hs
+++ b/src/Text/Playlist.hs
@@ -19,6 +19,8 @@
          -- * Parsing and Generating
        , parsePlaylist
        , generatePlaylist
+       , fileNameToFormat
+       , appendExtension
        ) where
 
 --------------------------------------------------------------------------------
@@ -26,6 +28,7 @@
 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
@@ -52,3 +55,29 @@
 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")
diff --git a/src/Text/Playlist/PLS/Writer.hs b/src/Text/Playlist/PLS/Writer.hs
--- a/src/Text/Playlist/PLS/Writer.hs
+++ b/src/Text/Playlist/PLS/Writer.hs
@@ -25,10 +25,10 @@
 --------------------------------------------------------------------------------
 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 "[playlist]\n"                  <>
+  mconcat (zipWith (curry writeTrack) x [1..]) <>
+  B.byteString "NumberOfEntries="              <>
+  B.stringUtf8 (show . length $ x)             <>
   B.byteString "\nVersion=2\n"
 
 --------------------------------------------------------------------------------
diff --git a/test/Examples.hs b/test/Examples.hs
new file mode 100644
--- /dev/null
+++ b/test/Examples.hs
@@ -0,0 +1,81 @@
+{-
+
+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 Examples
+       ( secretAgent
+       , pigRadio
+       , utf8Radio
+       , hitParty
+       , aaFile
+       , abFile
+       ) where
+
+--------------------------------------------------------------------------------
+import Text.Playlist
+
+--------------------------------------------------------------------------------
+secretAgent :: Playlist
+secretAgent =
+  [ Track { trackURL   = "http://mp2.somafm.com:9016"
+          , trackTitle = Just "SomaFM: Secret Agent (#1 128k mp3): The soundtrack for your stylish, mysterious, dangerous life. For Spies and PIs too!"
+          }
+
+  , Track { trackURL   = "http://mp3.somafm.com:443"
+          , trackTitle = Just "SomaFM: Secret Agent (#2 128k mp3): The soundtrack for your stylish, mysterious, dangerous life. For Spies and PIs too!"
+          }
+
+  , Track { trackURL   = "http://ice.somafm.com/secretagent"
+          , trackTitle = Just "SomaFM: Secret Agent (Firewall-friendly 128k mp3) The soundtrack for your stylish, mysterious, dangerous life. For Spies and PIs too!"
+          }
+  ]
+
+--------------------------------------------------------------------------------
+pigRadio :: Playlist
+pigRadio =
+  [ Track { trackURL   = "http://s6.mediastreaming.it:8080"
+          , trackTitle = Just "Pig Radio - Devoted in Playing the Best Indie Pop/Rock & Electronic. 24/7"
+          }
+
+  , Track { trackURL   = "http://s1.viastreaming.net:7480"
+          , trackTitle = Just "Pig Radio - Devoted in Playing the Best Indie Pop/Rock & Electronic. 24/7"
+          }
+  ]
+
+--------------------------------------------------------------------------------
+utf8Radio :: Playlist
+utf8Radio =
+  [ Track { trackURL   = "http://fake.com"
+          , trackTitle = Just "Alle otto i bambini erano già in costume da bagno"
+          }
+  ]
+
+--------------------------------------------------------------------------------
+hitParty :: Playlist
+hitParty =
+  [ Track { trackURL   = "http://firewall.hitparty.net:443"
+          , trackTitle = Just "(#1 - Generique) HITPARTY HIT - Pas de pub Que du HIT - Only Hits"
+          }
+  , Track { trackURL   = "http://icecast.pulsradio.com:80/hitpartyHD.mp3.m3u"
+          , trackTitle = Just "(#1 - Generique) HITPARTY HIT - Pas de pub Que du HIT - Only Hits"
+          }
+  ]
+
+--------------------------------------------------------------------------------
+aaFile :: Playlist
+aaFile =
+  [ Track { trackURL = "http://foo.com", trackTitle = Nothing}
+  , Track { trackURL = "http://bar.com", trackTitle = Nothing}
+  ]
+
+--------------------------------------------------------------------------------
+abFile :: Playlist
+abFile = aaFile
diff --git a/test/Helper.hs b/test/Helper.hs
new file mode 100644
--- /dev/null
+++ b/test/Helper.hs
@@ -0,0 +1,35 @@
+{-
+
+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 Helper (playlistFromFile, roundTrip) where
+
+--------------------------------------------------------------------------------
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as BL
+import Data.Char (toLower)
+import Text.Playlist
+
+--------------------------------------------------------------------------------
+playlistFromFile :: Format -> FilePath -> IO Playlist
+playlistFromFile fmt file = do
+    content <- BS.readFile file'
+    case parsePlaylist fmt content of
+      Left err   -> fail $ "failed to parse: " ++ file' ++ ": " ++ err
+      Right plst -> return plst
+  where file' = "test/" ++ file ++ "." ++ map toLower (show fmt)
+
+--------------------------------------------------------------------------------
+roundTrip :: Format -> Playlist -> IO Playlist
+roundTrip fmt plst =
+  case parsePlaylist fmt $ BL.toStrict $ generatePlaylist fmt plst of
+    Left err    -> fail $ "failed to roundTrip playlist: " ++ err
+    Right plst' -> return plst'
diff --git a/test/M3USpec.hs b/test/M3USpec.hs
new file mode 100644
--- /dev/null
+++ b/test/M3USpec.hs
@@ -0,0 +1,38 @@
+{-
+
+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 M3USpec (spec) where
+
+--------------------------------------------------------------------------------
+import Examples (hitParty, aaFile, abFile)
+import Helper (playlistFromFile, roundTrip)
+import Test.Hspec
+import Text.Playlist
+
+--------------------------------------------------------------------------------
+spec :: Spec
+spec = do
+  describe "Parsing" $ do
+    it "HIT Party" $ playlistFromFile' "hp" `shouldReturn` hitParty
+    it "File aa"   $ playlistFromFile' "aa" `shouldReturn` aaFile
+    it "File ab"   $ playlistFromFile' "ab" `shouldReturn` abFile
+  describe "Generating" $ do
+    it "HIT Party" $ roundTrip' hitParty `shouldReturn` hitParty
+    it "File aa"   $ roundTrip' aaFile   `shouldReturn` aaFile
+
+--------------------------------------------------------------------------------
+playlistFromFile' :: FilePath -> IO Playlist
+playlistFromFile' = playlistFromFile M3U
+
+--------------------------------------------------------------------------------
+roundTrip' :: Playlist -> IO Playlist
+roundTrip' = roundTrip M3U
diff --git a/test/PLSSpec.hs b/test/PLSSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/PLSSpec.hs
@@ -0,0 +1,39 @@
+{-
+
+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 PLSSpec (spec) where
+
+--------------------------------------------------------------------------------
+import Examples (secretAgent, pigRadio, utf8Radio)
+import Helper (playlistFromFile, roundTrip)
+import Test.Hspec
+import Text.Playlist
+
+--------------------------------------------------------------------------------
+spec :: Spec
+spec = do
+  describe "Parsing" $ do
+    it "Secret Agent" $ playlistFromFile' "sa"   `shouldReturn` secretAgent
+    it "Pig Radio"    $ playlistFromFile' "pig"  `shouldReturn` pigRadio
+    it "UTF8 Radio"   $ playlistFromFile' "utf8" `shouldReturn` utf8Radio
+  describe "Generating" $ do
+    it "Secret Agent" $ roundTrip' secretAgent `shouldReturn` secretAgent
+    it "Pig Radio"    $ roundTrip' pigRadio    `shouldReturn` pigRadio
+    it "UTF8 Radio"   $ roundTrip' utf8Radio   `shouldReturn` utf8Radio
+
+--------------------------------------------------------------------------------
+playlistFromFile' :: FilePath -> IO Playlist
+playlistFromFile' = playlistFromFile PLS
+
+--------------------------------------------------------------------------------
+roundTrip' :: Playlist -> IO Playlist
+roundTrip' = roundTrip PLS
diff --git a/test/aa.m3u b/test/aa.m3u
new file mode 100644
--- /dev/null
+++ b/test/aa.m3u
@@ -0,0 +1,2 @@
+http://foo.com
+http://bar.com
diff --git a/test/ab.m3u b/test/ab.m3u
new file mode 100644
--- /dev/null
+++ b/test/ab.m3u
@@ -0,0 +1,3 @@
+http://foo.com
+# This is a comment
+http://bar.com
diff --git a/test/doctest.hs b/test/doctest.hs
new file mode 100644
--- /dev/null
+++ b/test/doctest.hs
@@ -0,0 +1,36 @@
+{-
+
+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.
+
+-}
+
+--------------------------------------------------------------------------------
+-- | Wrapper around doctest to make sure examples in the source code work.
+module Main (main) where
+
+--------------------------------------------------------------------------------
+import Control.Applicative
+import System.Environment
+import Test.DocTest
+
+--------------------------------------------------------------------------------
+-- Totally stupid right now.  Will search the directory later.
+files :: IO [String]
+files = fmap ("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"]
+
+--------------------------------------------------------------------------------
+-- | Check the docs.
+main :: IO ()
+main = doctest =<< (++) <$> getArgs <*> fmap (flags ++) files
diff --git a/test/hlint.hs b/test/hlint.hs
new file mode 100644
--- /dev/null
+++ b/test/hlint.hs
@@ -0,0 +1,23 @@
+-----------------------------------------------------------------------------
+-- |
+-- 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
diff --git a/test/hp.m3u b/test/hp.m3u
new file mode 100644
--- /dev/null
+++ b/test/hp.m3u
@@ -0,0 +1,6 @@
+#EXTM3U
+#EXTINF:-1,(#1 - Generique) HITPARTY HIT - Pas de pub Que du HIT - Only Hits
+http://firewall.hitparty.net:443
+#EXTINF:-1,(#1 - Generique) HITPARTY HIT - Pas de pub Que du HIT - Only Hits
+http://icecast.pulsradio.com:80/hitpartyHD.mp3.m3u
+
diff --git a/test/pig.pls b/test/pig.pls
new file mode 100644
--- /dev/null
+++ b/test/pig.pls
@@ -0,0 +1,11 @@
+[playlist]
+File1=http://s6.mediastreaming.it:8080
+Title1=Pig Radio - Devoted in Playing the Best Indie Pop/Rock & Electronic. 24/7
+Length1=0
+File2=http://s1.viastreaming.net:7480
+Title2=Pig Radio - Devoted in Playing the Best Indie Pop/Rock & Electronic. 24/7
+Length2=-1
+
+
+NumberOfEntries=2
+Version=2
diff --git a/test/sa.pls b/test/sa.pls
new file mode 100644
--- /dev/null
+++ b/test/sa.pls
@@ -0,0 +1,12 @@
+[playlist]
+numberofentries=3
+File1=http://mp2.somafm.com:9016
+Title1=SomaFM: Secret Agent (#1 128k mp3): The soundtrack for your stylish, mysterious, dangerous life. For Spies and PIs too!
+Length1=-1
+File2=http://mp3.somafm.com:443
+Title2=SomaFM: Secret Agent (#2 128k mp3): The soundtrack for your stylish, mysterious, dangerous life. For Spies and PIs too!
+Length2=-1
+File3=http://ice.somafm.com/secretagent
+Title3=SomaFM: Secret Agent (Firewall-friendly 128k mp3) The soundtrack for your stylish, mysterious, dangerous life. For Spies and PIs too!
+Length3=-1
+Version=2
diff --git a/test/utf8.pls b/test/utf8.pls
new file mode 100644
--- /dev/null
+++ b/test/utf8.pls
@@ -0,0 +1,3 @@
+[playlist]
+File1=http://fake.com
+Title1=Alle otto i bambini erano già in costume da bagno
