lord 1.20131130 → 2.20131201
raw patch · 10 files changed
+293/−190 lines, 10 filesdep +process
Dependencies added: process
Files
- Radio.hs +121/−86
- Radio/Cmd.hs +4/−5
- Radio/Douban.hs +3/−4
- Radio/EightTracks.hs +96/−45
- Radio/EightTracks/Explore.hs +10/−6
- Radio/Jing.hs +6/−6
- Radio/Reddit.hs +1/−3
- lord.cabal +7/−5
- main.hs +22/−5
- test/main.hs +23/−25
Radio.hs view
@@ -26,13 +26,15 @@ import Data.Conduit.Binary (sinkFile) import Data.Yaml import Network.HTTP.Conduit hiding (path)-import Network.MPD hiding (play, Value)+import Network.MPD hiding (play, pause, Value) import qualified Network.MPD as MPD import System.Directory (doesFileExist, getHomeDirectory) import System.IO import System.IO.Unsafe (unsafePerformIO) import System.Log.FastLogger+import System.Process + eof :: MVar () eof = unsafePerformIO newEmptyMVar @@ -40,8 +42,11 @@ { artist :: String , album :: String , title :: String- }+ } +instance Show SongMeta where+ show meta = artist meta ++ " - " ++ title meta + class FromJSON a => Radio a where data Param a :: * @@ -77,98 +82,128 @@ Left err -> print err play :: Logger -> Param a -> [a] -> IO ()- play logger reqData [] = getPlaylist reqData >>= play logger reqData- play logger reqData (x:xs)- | playable x = do- surl <- songUrl reqData x- print surl- when (surl /= "") $ do- let song = artist (songMeta x) ++ " - " ++ title (songMeta x)- writeLog logger song - getStateFile >>= flip writeFile song- -- Report song played if needed- when (reportRequired x) $ void (forkIO $ reportLoop reqData x)- mpdLoad $ Path $ C.pack surl- takeMVar eof -- Finished- play logger reqData xs- | otherwise = do- surl <- songUrl reqData x- print surl- req <- parseUrl surl- home <- getLordDir- manager <- newManager def- forkIO $ E.catch - (do- let song = artist (songMeta x) ++ " - " ++ title (songMeta x)- writeLog logger song - getStateFile >>= flip writeFile song- -- Report song played if needed- when (reportRequired x) $ void (forkIO $ reportLoop reqData x)+ play logger reqData xxs = do+ st <- withMPD status+ case st of+ Right _ -> playWithMPD logger reqData xxs+ Left _ -> playWithMplayer logger reqData xxs - runResourceT $ do - res <- http req manager- responseBody res $$+- sinkFile (home ++ "/lord.m4a")- -- This will block until eof.+playWithMPD :: Radio a => Logger -> Param a -> [a] -> IO ()+playWithMPD logger reqData [] = + getPlaylist reqData >>= playWithMPD logger reqData+playWithMPD logger reqData (x:xs)+ | playable x = loadAndPlay logger reqData (x:xs)+ | otherwise = downloadAndPlay logger reqData (x:xs) - putMVar eof ())- (\e -> do- print (e :: E.SomeException)- writeLog logger $ show e- play logger reqData xs- )- threadDelay (3*1000000)- mpdLoad m4a- play logger reqData xs- where- m4a = "lord/lord.m4a"+loadAndPlay :: Radio a => Logger -> Param a -> [a] -> IO ()+loadAndPlay logger reqData [] = + getPlaylist reqData >>= loadAndPlay logger reqData+loadAndPlay logger reqData (x:xs) = do+ surl <- songUrl reqData x+ print surl+ when (surl /= "") $ do+ logAndReport logger reqData x+ mpdLoad $ Path $ C.pack surl+ takeMVar eof -- Finished+ loadAndPlay logger reqData xs+ where+ mpdLoad :: Path -> IO ()+ mpdLoad path = do+ withMPD $ do+ clear+ add path+ withMPD $ MPD.play Nothing+ mpdPlay - mpdLoad :: Path -> IO ()- mpdLoad path- | playable x = do- withMPD $ do+ mpdPlay :: IO ()+ mpdPlay = do+ st <- mpdState+ if st == Right Stopped + then putMVar eof ()+ else mpdPlay++downloadAndPlay :: Radio a => Logger -> Param a -> [a] -> IO ()+downloadAndPlay logger reqData [] = + getPlaylist reqData >>= downloadAndPlay logger reqData+downloadAndPlay logger reqData (x:xs) = do+ surl <- songUrl reqData x+ print surl+ req <- parseUrl surl+ home <- getLordDir+ manager <- newManager def+ forkIO $ E.catch + (do+ logAndReport logger reqData x++ runResourceT $ do + res <- http req manager+ responseBody res $$+- sinkFile (home ++ "/lord.m4a")+ -- This will block until eof.++ putMVar eof ())+ (\e -> do+ print (e :: E.SomeException)+ writeLog logger $ show e+ downloadAndPlay logger reqData xs+ )+ threadDelay (3*1000000)+ mpdLoad m4a+ downloadAndPlay logger reqData xs+ where+ m4a = "lord/lord.m4a"++ mpdLoad :: Path -> IO ()+ mpdLoad path = do+ s <- withMPD $ do clear+ update [Path "lord"] add path- withMPD $ MPD.play Nothing- mpdPlay- | otherwise = do- s <- withMPD $ do- clear- update [Path "lord"]- add path- case s of- Right _ -> do- withMPD $ MPD.play Nothing- mpdPlay- _ -> mpdLoad path+ case s of+ Right _ -> do+ withMPD $ MPD.play Nothing+ mpdPlay+ _ -> mpdLoad path - mpdState :: IO (MPD.Response State)- mpdState = do- withMPD $ idle [PlayerS] - -- This will block until paused/finished.+ mpdPlay :: IO ()+ mpdPlay = do+ st <- mpdState+ bd <- isEmptyMVar eof+ if st == Right Stopped + then if bd + then do -- Slow Network+ withMPD $ MPD.play Nothing+ mpdPlay+ else do+ withMPD clear+ takeMVar eof -- Finished+ else mpdPlay -- Pause - st <- liftM stState <$> withMPD status- print st- return st+playWithMplayer :: Radio a => Logger -> Param a -> [a] -> IO ()+playWithMplayer logger reqData [] = + getPlaylist reqData >>= playWithMplayer logger reqData+playWithMplayer logger reqData (x:xs) = do+ surl <- songUrl reqData x+ when (surl /= "") $ do+ logAndReport logger reqData x+ let sh = "mplayer -cache 2048 -cache-min 5 -novideo " ++ surl+ void $ waitForProcess =<< runCommand sh+ playWithMplayer logger reqData xs - mpdPlay :: IO ()- mpdPlay- | playable x = do- st <- mpdState- if st == Right Stopped - then putMVar eof ()- else mpdPlay- | otherwise = do- st <- mpdState- bd <- isEmptyMVar eof- if st == Right Stopped - then if bd - then do -- Slow Network- withMPD $ MPD.play Nothing- mpdPlay- else do- withMPD clear- takeMVar eof -- Finished- else mpdPlay -- Pause+logAndReport :: Radio a => Logger -> Param a -> a -> IO ()+logAndReport logger reqData x = do+ writeLog logger (show $ songMeta x) + getStateFile >>= flip writeFile (show $ songMeta x)+ -- Report song played if needed+ when (reportRequired x) $ void (forkIO $ reportLoop reqData x)++mpdState :: IO (MPD.Response State)+mpdState = do+ withMPD $ idle [PlayerS] + -- This will block until paused/finished.++ st <- liftM stState <$> withMPD status+ print st+ return st class (Radio a, ToJSON (Param a), ToJSON (Config a)) => NeedLogin a where login :: String -> IO (Param a)
Radio/Cmd.hs view
@@ -19,7 +19,6 @@ import qualified Radio -type Param a = Radio.Param Cmd data Cmd = Cmd { sc_id :: Int@@ -42,7 +41,7 @@ instance Radio.Radio Cmd where data Param Cmd = Genre String - parsePlaylist val = do+ parsePlaylist val = case fromJSON val of Success s -> s Error err -> error $ "Parse playlist failed: " ++ show err@@ -64,8 +63,8 @@ let req = initReq { method = "GET" , queryString = renderQuery False query }- E.catch (do withManager $ \manager -> - httpLbs req { redirectCount = 0 } manager >> return "")+ E.catch (withManager $ \manager -> + httpLbs req { redirectCount = 0 } manager >> return "") (\e -> case e of (StatusCodeException s hdr _) -> if s == status302 then redirect hdr @@ -116,4 +115,4 @@ putStrLn $ foldr1 f (take 4 gs) pprGenres $ drop 4 gs where- f a b = a ++ (concat $ take (20 - length(a)) $ repeat " ") ++ b+ f a b = a ++ concat (replicate (20 - length a) " ") ++ b
Radio/Douban.hs view
@@ -23,7 +23,6 @@ import qualified Radio -type Param a = Radio.Param Douban data Douban = Douban { picture :: String@@ -72,8 +71,8 @@ musicianID :: String -> IO (Maybe String) musicianID mname = do- let rurl = "http://music.douban.com/search/" ++ - (C.unpack $ urlEncode True (C.pack $ encodeString mname))+ let rurl = "http://music.douban.com/subject_search/?search_text=" ++ + C.unpack (urlEncode True (C.pack $ encodeString mname)) rsp <- simpleHttp rurl let cursor = fromDocument $ parseLBS rsp href = cursor $// element "a" @@ -174,7 +173,7 @@ where rurl = "http://douban.fm/j/explore/search?query=" ++ -- encodeString: encode chinese characters- (C.unpack $ urlEncode True (C.pack $ encodeString key))+ C.unpack (urlEncode True (C.pack $ encodeString key)) search' :: String -> IO [Channel]
Radio/EightTracks.hs view
@@ -16,8 +16,10 @@ import Data.Maybe (fromJust) import Data.Yaml hiding (decode) import Data.CaseInsensitive (mk)+import Data.Char (isDigit) import Data.Conduit (($$+-))-import Data.Conduit.Attoparsec (sinkParser)+import Data.Conduit.Attoparsec (sinkParser, ParseError)+import qualified Data.List as L import GHC.Generics (Generic) import Network.HTTP.Types import Network.HTTP.Conduit@@ -41,8 +43,10 @@ verHdr = (mk "X-Api-Version", "3") keyHdr = (mk "X-Api-Key", C.pack apiKey) -type Param a = Radio.Param EightTracks+type ETParam = Radio.Param EightTracks +-- | Response from http://8tracks.com/sets/new.json+-- `play_token` is the value of interest data PlaySession = PlaySession { play_token :: String , status :: String@@ -50,15 +54,16 @@ , notices :: Maybe String , api_version :: Int } deriving (Show, Generic)+instance FromJSON PlaySession -data EightTracks = EightTracks - { id :: Int- , track_file_stream_url :: String- , name :: String- , performer :: String- , release_name :: String- , url :: String+-- | Response from /play.json /next.json+-- `track` is the value of interest+data PlayResponse = PlayResponse+ { play_set :: MixSet+ , play_status :: String } deriving (Show, Generic)+instance FromJSON PlayResponse where+ parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = drop 5 } data MixSet = MixSet { at_beginning :: Bool@@ -67,18 +72,27 @@ , skip_allowed :: Bool , track :: EightTracks } deriving (Show, Generic)+instance FromJSON MixSet -data MixResponse = MixResponse- { mix_set :: MixSet- , mix_status :: String+data EightTracks = EightTracks + { id :: Int+ , track_file_stream_url :: String+ , name :: String+ , performer :: String+ , release_name :: String+ , url :: String } deriving (Show, Generic)--instance FromJSON PlaySession instance FromJSON EightTracks-instance FromJSON MixSet -instance FromJSON MixResponse where- parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = drop 4 }+-- | Response from http://8tracks.com/mixes/14.json or+-- http://8tracks.com/dp/electrominimalicious.json+-- `info_mix` is the value of interest+data MixInfo = MixInfo+ { info_mix :: Exp.Mix+ , info_status :: String+ } deriving (Show, Generic)+instance FromJSON MixInfo where+ parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = drop 5 } instance Radio.Radio EightTracks where data Param EightTracks = Token@@ -90,7 +104,7 @@ parsePlaylist val = case fromJSON val of- Success s -> [track $ mix_set s]+ Success s -> [track $ play_set s] Error err -> error $ "Parse playlist failed: " ++ show err -- Request play.json will record current mix to listening history.@@ -101,14 +115,14 @@ rurl <- if justStarted then do putMVar running ()- return $ "http://8tracks.com/sets/" ++ (show $ playToken tok) ++ "/play.json"+ return $ "http://8tracks.com/sets/" ++ show (playToken tok) ++ "/play.json" else- return $ "http://8tracks.com/sets/" ++ (show $ playToken tok) ++ "/next.json"+ return $ "http://8tracks.com/sets/" ++ show (playToken tok) ++ "/next.json" getPlaylist' rurl) (\e -> do -- When reached the last track in this mix. Play it again print (e :: E.SomeException) - let rurl = "http://8tracks.com/sets/" ++ (show $ playToken tok) ++ "/play.json"+ let rurl = "http://8tracks.com/sets/" ++ show (playToken tok) ++ "/play.json" getPlaylist' rurl) where usrHdr = (mk "X-User-Token", C.pack $ userToken tok)@@ -143,15 +157,16 @@ res <- withManager $ \manager -> httpLbs req manager print $ responseBody res where- rurl = "http://8tracks.com/sets/" ++ (show $ playToken tok) ++ "/report.json"+ rurl = "http://8tracks.com/sets/" ++ show (playToken tok) ++ "/report.json" query = [ ("track_id", C.pack $ show $ id x) , ("mix_id", C.pack $ show $ mixId tok) ] -instance FromJSON (Radio.Param EightTracks)-instance ToJSON (Radio.Param EightTracks)+instance FromJSON ETParam+instance ToJSON ETParam instance NeedLogin EightTracks where createSession strMixId email pwd = do+ mId <- getMixId strMixId initReq <- parseUrl rurl let req = initReq { method = "POST" , queryString = renderSimpleQuery False query@@ -166,13 +181,13 @@ where rurl = "http://8tracks.com/sessions.json" query = [ ("login", C.pack email), ("password", C.pack pwd) ]- mId = read strMixId :: Int - data Config EightTracks = Config { eight :: Radio.Param EightTracks } deriving Generic+ data Config EightTracks = Config { eight :: ETParam } deriving Generic - mkConfig tok = Config tok+ mkConfig = Config - readToken mid = do+ readToken strMixId = do+ mId <- getMixId strMixId home <- Radio.getLordDir let yml = home ++ "/lord.yml" exist <- doesFileExist yml@@ -183,7 +198,7 @@ Nothing -> error $ "Invalid YAML file: " ++ show conf Just c -> case fromJSON c of- Success tok -> return $ Just $ (eight tok) { mixId = read mid }+ Success tok -> return $ Just $ (eight tok) { mixId = mId } Error err -> do print $ "Parse token failed: " ++ show err return Nothing@@ -200,23 +215,21 @@ where rurl = "http://8tracks.com/sets/new.json?api_version=3&api_key=" ++ apiKey +smartUrl :: String -> String+smartUrl smartId = + "http://8tracks.com/mix_sets/" ++ smartId ++ ".json?include=mixes"++smartGet :: String -> IO [Exp.Mix]+smartGet smartId = get (smartUrl smartId) (Exp.mixes . Exp.mix_set)+ search :: String -> IO [Exp.Mix] search [] = return []-search key = search' rurl- where - rurl = "http://8tracks.com/mix_sets/keyword:" ++ key ++ ".json?include=mixes"--search' :: String -> IO [Exp.Mix]-search' rurl = do- initReq <- parseUrl rurl- let req = initReq { requestHeaders = [verHdr, keyHdr] }- val <- withManager $ \manager -> do- res <- http req manager- responseBody res $$+- sinkParser json+search key = smartGet $ "keyword:" ++ key - case fromJSON val of- Success v -> return $ Exp.mixes $ Exp.mix_set v- Error err -> putStrLn err >> return []+featured, trending, newest :: IO [Exp.Mix]+featured = smartGet "collection:homepage"+trending = smartGet "all"+newest = smartGet "all:recent" pprMixes :: [Exp.Mix] -> IO () pprMixes mixes =@@ -224,9 +237,47 @@ setSGR [SetConsoleIntensity BoldIntensity] putStr $ "* " ++ Exp.name m setSGR [SetColor Foreground Vivid Green]- putStrLn $ " id=" ++ show (Exp.id m)+ putStr $ " id=" ++ show (Exp.id m)+ putStr $ " ▶" ++ show (Exp.plays_count m)+ putStr $ " ♥" ++ show (Exp.likes_count m)+ putStrLn $ " (" ++ show (Exp.tracks_count m) ++ " tracks)" setSGR [Reset]- putStrLn $ " Description: " ++ Exp.description m+ putStrLn $ " Description: " ++ + unlines (map (" " ++) (lines $ Exp.description m)) putStrLn $ " Tags: " ++ Exp.tag_list_cache m putStrLn "" )++get :: FromJSON a => String -> (a -> b) -> IO b+get rurl selector = do+ initReq <- parseUrl rurl+ let req = initReq { requestHeaders = [verHdr, keyHdr] }+ val <- E.catches + (withManager $ \manager -> do+ res <- http req manager+ responseBody res $$+- sinkParser json)+ [ E.Handler (\e -> print (e :: ParseError) >>+ error "The mix you are trying to access may be private.")+ , E.Handler (\e -> case e of+ (StatusCodeException s _ _) ->+ error $ show s+ otherException ->+ error $ show otherException) + ]++ case fromJSON val of+ Success v -> return $ selector v+ Error err -> error err++getMixId :: String -> IO Int+getMixId m+ | isNumerical m = return $ read m+ | otherwise = do+ let mixUrl = if domain `L.isPrefixOf` m+ then m+ else (domain ++)+ (if "/" `L.isPrefixOf` m then m else '/' : m)+ get (mixUrl ++ ".json") (Exp.id . info_mix)+ where+ isNumerical = and . fmap isDigit+ domain = "http://8tracks.com"
Radio/EightTracks/Explore.hs view
@@ -5,12 +5,16 @@ import GHC.Generics (Generic) data Mix = Mix- { id :: Int- , path :: String- , web_path :: String- , name :: String- , description :: String- , tag_list_cache :: String+ { id :: Int+ , path :: String+ , web_path :: String+ , name :: String+ , description :: String+ , plays_count :: Int+ , likes_count :: Int+ , tag_list_cache:: String+ , duration :: Int+ , tracks_count :: Int } deriving (Show, Generic) instance FromJSON Mix
Radio/Jing.hs view
@@ -28,7 +28,7 @@ import Radio -type Param a = Radio.Param Jing+type JingParam = Radio.Param Jing data Jing = Jing { abid :: Int -- album id@@ -127,8 +127,8 @@ playable _ = False -instance FromJSON (Radio.Param Jing)-instance ToJSON (Radio.Param Jing)+instance FromJSON JingParam+instance ToJSON JingParam instance NeedLogin Jing where createSession keywords email pwd = do@@ -140,7 +140,7 @@ let hmap = HM.fromList $ responseHeaders res atoken = HM.lookup "Jing-A-Token-Header" hmap rtoken = HM.lookup "Jing-R-Token-Header" hmap- parseToken :: Value -> Maybe (Radio.Param Jing)+ parseToken :: Value -> Maybe JingParam parseToken (Object hm) = do let user = HM.lookup "result" hm >>= \(Object hm') -> HM.lookup "usr" hm'@@ -155,9 +155,9 @@ parseToken _ = error "Unrecognized token format." liftM parseToken (runResourceT $ responseBody res $$+- sinkParser json) - data Config Jing = Config { jing :: Radio.Param Jing } deriving Generic+ data Config Jing = Config { jing :: JingParam } deriving Generic - mkConfig tok = Config tok+ mkConfig = Config readToken keywords = do home <- Radio.getLordDir
Radio/Reddit.hs view
@@ -15,8 +15,6 @@ import qualified Radio -type Param a = Radio.Param Reddit- data Reddit = Reddit { title :: String , artist :: String@@ -61,4 +59,4 @@ putStrLn $ foldr1 f (take 4 gs) pprGenres $ drop 4 gs where- f a b = a ++ (concat $ take (20 - length(a)) $ repeat " ") ++ b+ f a b = a ++ concat (replicate (20 - length a) " ") ++ b
lord.cabal view
@@ -1,8 +1,8 @@ name: lord-version: 1.20131130+version: 2.20131201 synopsis: A command line interface to online radios. description: - A unified interface to several online radio service providers, use @mpd@ (<http://musicpd.org>) as backend.+ A unified command line interface to several online radios, use @mpd@ (<http://musicpd.org>) as backend by default. Will fallback to mplayer (http://www.mplayerhq.hu) when mpd is unavailable. . Supported radios: .@@ -22,16 +22,16 @@ > lord status > lord kill >- > lord 8tracks listen <mix_id> [--no-daemon]+ > lord 8tracks listen [<mix_id> | <mix_url>] [--no-daemon] > lord 8tracks search <keywords>+ > lord 8tracks [featured | trending | newest] > > lord cmd listen <genre> [--no-daemon] > lord cmd genres > > lord douban listen [<channel_id> | <musician>] [--no-daemon] > lord douban search <keywords>- > lord douban hot- > lord douban trending+ > lord douban [hot | trending] > > lord jing listen <keywords> [--no-daemon] >@@ -82,6 +82,7 @@ http-types >= 0.8, libmpd >= 0.8, optparse-applicative >= 0.5,+ process >= 1.1, text >= 0.11, transformers >= 0.3, unix >= 2.5, @@ -115,6 +116,7 @@ HUnit >= 1.2, libmpd >= 0.8, optparse-applicative >= 0.5,+ process >= 1.1, text >= 0.11, transformers >= 0.3, unix >= 2.5,
main.hs view
@@ -49,6 +49,9 @@ deriving (Eq, Show) data ETSubCommand = ETListen String+ | ETFeatured+ | ETTrending+ | ETNewest | ETSearch String deriving (Eq, Show) @@ -82,6 +85,9 @@ main :: IO () main = do+ -- Make sure ~/.lord exists+ getLordDir >>= createDirectoryIfMissing False+ o <- execParser' $ info (helper <*> optParser) (fullDesc <> header "Lord: radio commander") let nodaemon = optDaemon o@@ -99,6 +105,9 @@ EightTracks subCommand -> case subCommand of ETListen mId -> etListen nodaemon mId+ ETFeatured -> ET.featured >>= ET.pprMixes+ ETTrending -> ET.trending >>= ET.pprMixes+ ETNewest -> ET.newest >>= ET.pprMixes ETSearch key -> etSearch key JingFM (JingListen key) -> jingListen nodaemon key RedditFM subCommand ->@@ -159,6 +168,9 @@ ( command "listen" (info (helper <*> (ETListen <$> argument str (metavar "mix id"))) (progDesc "Provide mix id to listen to 8tracks.com"))+ <> command "featured" (info (pure ETFeatured) (progDesc "Featured mixes"))+ <> command "trending" (info (pure ETFeatured) (progDesc "Trending mixes"))+ <> command "newest" (info (pure ETFeatured) (progDesc "Newest mixes")) <> command "search" (info (helper <*> (ETSearch <$> argument str (metavar "KEYWORDS"))) (progDesc "search mixes"))@@ -223,17 +235,22 @@ listen :: Radio a => Bool -> Radio.Param a -> IO () listen nodaemon param = do- -- Make sure ~/.lord exists- getLordDir >>= createDirectoryIfMissing False+ -- mplayer backend won't work in daemon mode!+ st <- withMPD status+ nodaemon' <- case st of + Right _ -> return nodaemon+ Left e -> print e >> + putStrLn "Lord will run in foreground" >>+ return True pid <- getPidFile- logger <- if nodaemon then mkLogger True stdout + logger <- if nodaemon' then mkLogger True stdout else getLogFile >>= flip openFile AppendMode >>= mkLogger True let listen' = play logger param [] running <- isRunning pid when running $ killAndWait pid - if nodaemon then runInForeground pid listen'- else runDetached (Just pid) def listen'+ if nodaemon' then runInForeground pid listen'+ else runDetached (Just pid) def listen' -- Partially taken from System.Posix.Daemon module runInForeground :: FilePath -> IO () -> IO ()
test/main.hs view
@@ -12,34 +12,32 @@ main :: IO ()-main = do- hspec spec+main = hspec spec spec :: Spec-spec = do- describe "getPlaylist" $ do- it "cmd: given genre" $ do- ss <- Radio.getPlaylist (Cmd.Genre "Dream Pop")- assert $ length ss > 0+spec = describe "getPlaylist" $ do+ it "cmd: given genre" $ do+ ss <- Radio.getPlaylist (Cmd.Genre "Dream Pop")+ assert $ not $ null ss - it "douban: given channel id" $ do- ss <- Radio.getPlaylist (Cid 6)- assert $ length ss > 0- - it "douban: given musician name" $ do- ss <- Radio.getPlaylist (Musician "Sigur RóS")- assert $ length ss > 0+ it "douban: given channel id" $ do+ ss <- Radio.getPlaylist (Cid 6)+ assert $ not $ null ss - it "8tracks: given mix id" $ do- tok <- readToken "14" :: IO (Maybe (Radio.Param ET.EightTracks))- ss <- Radio.getPlaylist $ fromJust tok- assert $ length ss > 0+ it "douban: given musician name" $ do+ ss <- Radio.getPlaylist (Musician "Sigur RóS")+ assert $ not $ null ss - it "jing: given keywords" $ do- tok <- readToken "postrock" :: IO (Maybe (Radio.Param Jing))- ss <- Radio.getPlaylist $ fromJust tok- assert $ length ss > 0+ it "8tracks: given mix id" $ do+ tok <- readToken "14" :: IO (Maybe (Radio.Param ET.EightTracks))+ ss <- Radio.getPlaylist $ fromJust tok+ assert $ not $ null ss - it "reddit: given genre" $ do- ss <- Radio.getPlaylist (Reddit.Genre "indie")- assert $ length ss > 0+ it "jing: given keywords" $ do+ tok <- readToken "postrock" :: IO (Maybe (Radio.Param Jing))+ ss <- Radio.getPlaylist $ fromJust tok+ assert $ not $ null ss++ it "reddit: given genre" $ do+ ss <- Radio.getPlaylist (Reddit.Genre "indie")+ assert $ not $ null ss