@@ -0,0 +1,2 @@ | |||
cabal-dev | |||
dist |
@@ -0,0 +1,15 @@ | |||
################################################################################ | |||
# A stupid Makefile that doesn't do much. | |||
.PHONEY: all test clean | |||
################################################################################ | |||
all: | |||
cabal-dev install | |||
################################################################################ | |||
test:: | |||
cabal-dev install --enable-tests | |||
################################################################################ | |||
clean: | |||
rm -rf dist |
@@ -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. |
@@ -0,0 +1,7 @@ | |||
# Haskell Playlist Library and Tools | |||
## Supported Formats | |||
* [PLS] [] | |||
[pls]: http://en.wikipedia.org/wiki/PLS_(file_format) |
@@ -0,0 +1,2 @@ | |||
import Distribution.Simple | |||
main = defaultMain |
@@ -0,0 +1,42 @@ | |||
name: playlists | |||
version: 0.1.0.0 | |||
synopsis: Library and executable for working with playlist files. | |||
license: BSD3 | |||
license-file: LICENSE | |||
author: Peter Jones | |||
maintainer: pjones@devalot.com | |||
copyright: Copyright (c) 2013, Peter Jones <pjones@devalot.com> | |||
category: Text | |||
build-type: Simple | |||
cabal-version: >=1.8 | |||
-- description: | |||
-------------------------------------------------------------------------------- | |||
library | |||
exposed-modules: | |||
Text.Playlist | |||
other-modules: | |||
Text.Playlist.Types | |||
Text.Playlist.PLS.Reader | |||
Text.Playlist.Internal.Attoparsec | |||
hs-source-dirs: src | |||
ghc-options: -Wall | |||
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 | |||
-------------------------------------------------------------------------------- | |||
test-suite spec | |||
type: exitcode-stdio-1.0 | |||
hs-source-dirs: test | |||
ghc-options: -Wall -Werror | |||
extensions: OverloadedStrings | |||
main-is: Main.hs | |||
build-depends: base | |||
, bytestring | |||
, hspec >= 1.4.0 && < 1.5 | |||
, playlists |
@@ -0,0 +1,17 @@ | |||
-------------------------------------------------------------------------------- | |||
module Text.Playlist | |||
( Track (..) | |||
, Playlist | |||
, Format (..) | |||
, parsePlaylist | |||
) where | |||
-------------------------------------------------------------------------------- | |||
import qualified Data.Attoparsec.ByteString as Atto | |||
import Data.ByteString (ByteString) | |||
import Text.Playlist.Types | |||
import qualified Text.Playlist.PLS.Reader as PLS | |||
-------------------------------------------------------------------------------- | |||
parsePlaylist :: Format -> ByteString -> Either String Playlist | |||
parsePlaylist PLS x = Atto.parseOnly PLS.parsePlaylist x |
@@ -0,0 +1,39 @@ | |||
-------------------------------------------------------------------------------- | |||
-- | 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 |
@@ -0,0 +1,80 @@ | |||
-------------------------------------------------------------------------------- | |||
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 | |||
parseFooter | |||
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 | |||
void (many' 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 all footer lines, we don't use them. | |||
parseFooter :: Parser () | |||
parseFooter = void (many' skipUnusedLine) | |||
-------------------------------------------------------------------------------- | |||
-- | 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) |
@@ -0,0 +1,25 @@ | |||
-------------------------------------------------------------------------------- | |||
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) | |||
deriving (Show, Eq) |
@@ -0,0 +1,33 @@ | |||
-------------------------------------------------------------------------------- | |||
module Examples (secretAgent, pigRadio) 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" | |||
} | |||
] |
@@ -0,0 +1,13 @@ | |||
-------------------------------------------------------------------------------- | |||
module Main where | |||
-------------------------------------------------------------------------------- | |||
import Test.Hspec | |||
-------------------------------------------------------------------------------- | |||
import qualified PLSSpec | |||
-------------------------------------------------------------------------------- | |||
main :: IO () | |||
main = hspec $ do | |||
describe "PLS" PLSSpec.spec |
@@ -0,0 +1,24 @@ | |||
-------------------------------------------------------------------------------- | |||
module PLSSpec (spec) where | |||
-------------------------------------------------------------------------------- | |||
import qualified Data.ByteString as BS | |||
import Examples (secretAgent, pigRadio) | |||
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 | |||
-------------------------------------------------------------------------------- | |||
playlistFromFile :: FilePath -> IO Playlist | |||
playlistFromFile file = do | |||
content <- BS.readFile file' | |||
case parsePlaylist PLS content of | |||
Left err -> fail $ "failed to parse: " ++ file' ++ ": " ++ err | |||
Right plst -> return plst | |||
where file' = "test/" ++ file ++ ".pls" |
@@ -0,0 +1,24 @@ | |||
#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 | |||
#EXTINF:-1,(#2 - Generique) HITPARTY HIT - Pas de pub Que du HIT - Only Hits | |||
http://firewall.hitparty.net:443 | |||
#EXTINF:-1,(#3 - Generique) HITPARTY HIT - Pas de pub Que du HIT - Only Hits | |||
http://87.98.129.202:443 | |||
#EXTINF:-1,(#4 - Generique) HITPARTY HIT - Pas de pub Que du HIT - Only Hits | |||
http://193.17.192.12:443 | |||
#EXTINF:-1,(#5 - Generique) HITPARTY HIT - Pas de pub Que du HIT - Only Hits | |||
http://88.190.224.133:443 | |||
#EXTINF:-1,(#6 - Generique) HITPARTY HIT - Pas de pub Que du HIT - Only Hits | |||
http://stream.hitparty.net:8000 | |||
#EXTINF:-1,(#7 - Generique) HITPARTY HIT - Pas de pub Que du HIT - Only Hits | |||
http://88.190.224.133:443 | |||
#EXTINF:-1,(#8 - Generique) HITPARTY HIT - Pas de pub Que du HIT - Only Hits | |||
http://87.98.129.202:443 | |||
#EXTINF:-1,(#9 - Generique) PULSRADIO - Dance & Trance NON STOP - www.pulsradio.com | |||
http://193.17.192.13:80 | |||
#EXTINF:-1,(#10 - Generique) PULSRADIO - Dance & Trance NON STOP - www.pulsradio.com | |||
http://87.98.148.76:80 | |||
@@ -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 |
@@ -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 |
@@ -0,0 +1,3 @@ | |||
[playlist] | |||
File1=http://fake.com | |||
Title1=Alle otto i bambini erano già in costume da bagno |