lord (empty) → 1.20130928
raw patch · 5 files changed
+356/−0 lines, 5 filesdep +HUnitdep +aesondep +ansi-terminalsetup-changed
Dependencies added: HUnit, aeson, ansi-terminal, attoparsec-conduit, base, bytestring, case-insensitive, conduit, configurator, daemons, data-default, directory, fast-logger, hspec, html-conduit, http-conduit, http-types, libmpd, optparse-applicative, text, transformers, unix, unordered-containers, utf8-string, xml-conduit, yaml
Files
- LICENSE +24/−0
- Setup.hs +2/−0
- lord.cabal +87/−0
- main.hs +209/−0
- test/main.hs +34/−0
+ LICENSE view
@@ -0,0 +1,24 @@+This is free and unencumbered software released into the public domain.++Anyone is free to copy, modify, publish, use, compile, sell, or+distribute this software, either in source code form or as a compiled+binary, for any purpose, commercial or non-commercial, and by any+means.++In jurisdictions that recognize copyright laws, the author or authors+of this software dedicate any and all copyright interest in the+software to the public domain. We make this dedication for the benefit+of the public at large and to the detriment of our heirs and+successors. We intend this dedication to be an overt act of+relinquishment in perpetuity of all present and future rights to this+software under copyright law.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR+OTHER DEALINGS IN THE SOFTWARE.++For more information, please refer to <http://unlicense.org/>
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ lord.cabal view
@@ -0,0 +1,87 @@+name: lord+version: 1.20130928+synopsis: Lord of radio daemon+description: + A unified interface to online radio service providers, use mpd as backend.+ .+ Supported radios:+ <http://cmd.fm>+ <http://douban.fm>+ <http://jing.fm>+homepage: https://github.com/rnons/lord+bug-reports: https://github.com/rnons/lord/issues+license: PublicDomain+license-file: LICENSE+author: rnons+maintainer: remotenonsense@gmail.com+category: Web+build-type: Simple+cabal-version: >=1.10+tested-with: GHC == 7.6.3++source-repository head+ type: git+ location: git://github.com/rnons/lord.git+ +executable lord+ main-is: main.hs + -- other-modules: + ghc-options: -Wall -fno-warn-unused-do-bind+ build-depends: base >= 4 && < 5, + aeson >= 0.6, + ansi-terminal >= 0.6, + attoparsec-conduit >= 1.0, + bytestring >= 0.9, + case-insensitive >= 1.0, + conduit >= 1.0, + configurator >= 0.2, + daemons >= 0.1.2,+ data-default >= 0.5,+ directory >= 1.1, + fast-logger >= 0.3,+ html-conduit >= 1.1, + http-conduit >= 1.9, + http-types >= 0.8, + libmpd >= 0.8, + optparse-applicative >= 0.5,+ text >= 0.11, + transformers >= 0.3, + unix >= 2.5, + unordered-containers >= 0.2, + utf8-string >= 0.3, + xml-conduit >= 1.1,+ yaml >= 0.8+ default-language: Haskell2010++test-suite test+ type: exitcode-stdio-1.0+ main-is: test/main.hs+ ghc-options: -Wall -fno-warn-unused-do-bind+ hs-source-dirs: .+ build-depends: base >= 4 && < 5,+ aeson >= 0.6, + ansi-terminal >= 0.6, + attoparsec-conduit >= 1.0, + bytestring >= 0.9, + case-insensitive >= 1.0, + conduit >= 1.0, + configurator >= 0.2, + daemons >= 0.1.2,+ data-default >= 0.5,+ directory >= 1.1, + fast-logger >= 0.3,+ hspec >= 1.6,+ html-conduit >= 1.1, + http-conduit >= 1.9, + http-types >= 0.8, + HUnit >= 1.2,+ libmpd >= 0.8, + optparse-applicative >= 0.5,+ text >= 0.11, + transformers >= 0.3, + unix >= 2.5, + unordered-containers >= 0.2, + utf8-string >= 0.3, + xml-conduit >= 1.1,+ yaml >= 0.8+ default-language: Haskell2010
+ main.hs view
@@ -0,0 +1,209 @@+import Control.Monad (when)+import qualified Data.ByteString.Char8 as C+import Data.Char (isDigit)+import Data.Default (def)+import Network.MPD (withMPD, clear, status, stState)+import Options.Applicative+import Options.Applicative.Types (ParserPrefs, ParserInfo)+import System.Directory (createDirectoryIfMissing)+import System.Environment (getArgs, getProgName)+import System.Exit (exitWith, exitSuccess, ExitCode(..))+import System.IO ( openFile, IOMode(AppendMode)+ , hPutStr, stdout, stderr, SeekMode(..) )+import System.Log.FastLogger (mkLogger, Logger)+import System.Posix.Daemon+import System.Posix.Files (stdFileMode)+import System.Posix.IO ( fdWrite, createFile, setLock+ , LockRequest(..) )+import System.Posix.Process (getProcessID)++import Radio+import Radio.Cmd+import Radio.Douban+import Radio.Jing++data Options = Options+ { optCommand :: Command+ , optDaemon :: Bool+ } deriving (Eq, Show)++data Command = CmdFM CmdSubCommand+ | DoubanFM DoubanSubCommand+ | JingFM JingSubCommand+ | Status+ | Kill+ deriving (Eq, Show)++data CmdSubCommand = CmdListen String+ | CmdGenreList+ deriving (Eq, Show)++data DoubanSubCommand = DoubanListen String+ | DoubanHot+ | DoubanTrending+ | DoubanSearch String+ deriving (Eq, Show)++data JingSubCommand = JingListen String+ deriving (Eq, Show)++type Keywords = String++optParser :: Parser Options+optParser = Options + <$> subparser ( command "cmd" (info (helper <*> cmdOptions)+ (progDesc "cmd.fm commander"))+ <> command "douban" (info (helper <*> doubanOptions)+ (progDesc "douban.fm commander"))+ <> command "jing" (info (helper <*> jingOptions)+ (progDesc "jing.fm commander"))+ <> command "status" (info (pure Status)+ (progDesc "show current status"))+ <> command "kill" (info (pure Kill)+ (progDesc "kill the current running lord session"))+ )+ <*> switch (long "no-daemon" <> help "don't detach from console")++main :: IO ()+main = do+ o <- execParser' $ info (helper <*> optParser) + (fullDesc <> header "Lord: radio commander")+ let nodaemon = optDaemon o+ case optCommand o of+ CmdFM subCommand ->+ case subCommand of+ CmdListen genre -> listen nodaemon (cmd genre)+ CmdGenreList -> cmdGenres+ DoubanFM subCommand -> + case subCommand of+ DoubanListen key -> listen nodaemon (douban key)+ DoubanHot -> doubanHot+ DoubanTrending -> doubanTrending+ DoubanSearch key -> doubanSearch key+ JingFM (JingListen key) -> jingListen nodaemon key+ Status -> lordStatus+ Kill -> killLord++-- Taken from Options.Applicative.Extra+execParser' :: ParserInfo a -> IO a+execParser' = customExecParser' (prefs idm)++-- Taken from Options.Applicative.Extra+customExecParser' :: ParserPrefs -> ParserInfo a -> IO a+customExecParser' pprefs pinfo = do+ args <- getArgs+ + -- My modification!+ -- Run lord with no args is equivalent to run lord status.+ when (null args) $ lordStatus >> exitSuccess+ + case execParserPure pprefs pinfo args of+ Right a -> return a+ Left failure -> do+ progn <- getProgName+ let c = errExitCode failure+ msg <- errMessage failure progn+ case c of+ ExitSuccess -> putStr msg+ _ -> hPutStr stderr msg+ exitWith c++cmdOptions :: Parser Command+cmdOptions = CmdFM <$> subparser+ ( command "listen" + (info (helper <*> (CmdListen <$> argument str (metavar "GENRE")))+ (progDesc "Provide genre to listen to cmd.fm"))+ <> command "genres" + (info (pure CmdGenreList) (progDesc "List available genres"))+ )++doubanOptions :: Parser Command+doubanOptions = DoubanFM <$> subparser + ( command "listen" + (info (helper <*> (DoubanListen <$> argument str (metavar "[<channel_id> | <musician>]")))+ (progDesc "Provide cid/musician to listen to douban.fm"))+ <> command "search" + (info (helper <*> (DoubanSearch <$> argument str (metavar "KEYWORDS")))+ (progDesc "search channels"))+ <> command "hot" (info (pure DoubanHot) (progDesc "hot channels"))+ <> command "trending" + (info (pure DoubanTrending) (progDesc "trending up channels"))+ )++jingOptions :: Parser Command+jingOptions = JingFM <$> subparser + ( command "listen" + (info (helper <*> (JingListen <$> argument str (metavar "KEYWORDS")))+ (progDesc "Provide keywords to listen to jing.fm"))+ )++cmdGenres :: IO ()+cmdGenres = genres >>= pprGenres++doubanHot :: IO ()+doubanHot = hot >>= pprChannels++doubanTrending :: IO ()+doubanTrending = trending >>= pprChannels++doubanSearch :: String -> IO ()+doubanSearch key = search key >>= pprChannels++cmd :: Keywords -> Radio.Param Cmd+cmd = Genre++douban :: Keywords -> Radio.Param Douban+douban k+ | isChId k = Cid $ read k+ | otherwise = Musician k+ where+ isChId = and . fmap isDigit++jingListen :: Bool -> Keywords -> IO ()+jingListen nodaemon k = do+ tok <- readToken k+ case tok of+ Just tok' -> do+ putStrLn $ "Welcome back, " ++ C.unpack (nick tok')+ listen nodaemon tok'+ _ -> login k >>= listen nodaemon++listen :: Radio a => Bool -> Radio.Param a -> IO ()+listen nodaemon param = do+ -- Make sure ~/.lord exists+ getLordDir >>= createDirectoryIfMissing False++ pid <- getPidFile+ 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'++-- Partially taken from System.Posix.Daemon module+runInForeground :: FilePath -> IO () -> IO ()+runInForeground pidFile program = do+ fd <- createFile pidFile stdFileMode+ setLock fd (WriteLock, AbsoluteSeek, 0, 0)+ pid <- getProcessID+ fdWrite fd (show pid)+ program++killLord :: IO ()+killLord = withMPD clear >> getPidFile >>= kill++lordStatus :: IO ()+lordStatus = do+ running <- getPidFile >>= isRunning+ status <- + if running then do+ st <- fmap stState <$> withMPD status+ let state = case st of+ Right s -> show s+ Left err -> error $ show err+ song <- getStateFile >>= readFile + return $ "[" ++ state ++ "] " ++ song+ else return "Not running!"+ putStrLn status
+ test/main.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE OverloadedStrings #-}+import Data.Maybe (fromJust)+import Test.Hspec+import Test.HUnit++import Radio+import Radio.Cmd+import Radio.Jing+import Radio.Douban+++main :: IO ()+main = do+ hspec spec++spec :: Spec+spec = do+ describe "getPlaylist" $ do+ it "cmd: given genre" $ do+ ss <- Radio.getPlaylist (Genre "Dream Pop")+ assert $ length ss > 0++ 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 "jing" $ do+ tok <- readToken "postrock"+ ss <- Radio.getPlaylist $ fromJust tok+ assert $ length ss > 0