lord-2.20131203: main.hs
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)
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)
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 qualified Radio.Cmd as Cmd
import Radio.Douban
import qualified Radio.EightTracks as ET
import Radio.Jing
import qualified Radio.Reddit as Reddit
data Options = Options
{ optCommand :: Command
, optDaemon :: Bool
} deriving (Eq, Show)
data Command = CmdFM CmdSubCommand
| DoubanFM DoubanSubCommand
| EightTracks ETSubCommand
| JingFM JingSubCommand
| RedditFM RedditSubCommand
| 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 ETSubCommand = ETListen String
| ETFeatured
| ETTrending
| ETNewest
| ETSearch String
deriving (Eq, Show)
data JingSubCommand = JingListen String
deriving (Eq, Show)
data RedditSubCommand = RedditListen String
| RedditGenreList
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 "8tracks" (info (helper <*> etOptions)
(progDesc "8tracks.com commander"))
<> command "jing" (info (helper <*> jingOptions)
(progDesc "jing.fm commander"))
<> command "reddit" (info (helper <*> redditOptions)
(progDesc "radioreddit.com 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
-- Make sure ~/.lord exists
getLordDir >>= createDirectoryIfMissing False
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 genre)
CmdGenreList -> Cmd.genres >>= Cmd.pprGenres
DoubanFM subCommand ->
case subCommand of
DoubanListen key -> listen nodaemon (douban key)
DoubanHot -> doubanHot
DoubanTrending -> doubanTrending
DoubanSearch key -> doubanSearch key
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 ->
case subCommand of
RedditListen genre -> listen nodaemon (Reddit.Genre genre)
RedditGenreList -> Reddit.genres >>= Reddit.pprGenres
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"))
)
etOptions :: Parser Command
etOptions = EightTracks <$> subparser
( 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"))
)
jingOptions :: Parser Command
jingOptions = JingFM <$> subparser
( command "listen"
(info (helper <*> (JingListen <$> argument str (metavar "KEYWORDS")))
(progDesc "Provide keywords to listen to jing.fm"))
)
redditOptions :: Parser Command
redditOptions = RedditFM <$> subparser
( command "listen"
(info (helper <*> (RedditListen <$> argument str (metavar "GENRE")))
(progDesc "Provide genre to listen to radioreddit.com"))
<> command "genres"
(info (pure RedditGenreList) (progDesc "List available genres"))
)
doubanHot :: IO ()
doubanHot = hot >>= pprChannels
doubanTrending :: IO ()
doubanTrending = trending >>= pprChannels
doubanSearch :: String -> IO ()
doubanSearch key = search key >>= pprChannels
douban :: Keywords -> Radio.Param Douban
douban k
| isChId k = Cid $ read k
| otherwise = Musician k
where
isChId = and . fmap isDigit
etListen :: Bool -> Keywords -> IO ()
etListen nodaemon k = do
mId <- ET.getMixId k
tok <- readToken ET.eight $ show mId
case tok of
Just tok' -> do
putStrLn $ "Welcome back, " ++ ET.userName tok'
listen nodaemon tok'
_ -> do
param <- login k :: IO (Radio.Param ET.EightTracks)
listen nodaemon param
etSearch :: String -> IO ()
etSearch key = ET.search key >>= ET.pprMixes
jingListen :: Bool -> Keywords -> IO ()
jingListen nodaemon k = do
tok <- readToken jing k
case tok of
Just tok' -> do
putStrLn $ "Welcome back, " ++ C.unpack (nick tok')
listen nodaemon tok'
_ -> do
param <- login k :: IO (Radio.Param Jing)
listen nodaemon param
listen :: Radio a => Bool -> Radio.Param a -> IO ()
listen nodaemon param = do
-- 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
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
myStatus <-
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 myStatus