packages feed

lord-1.20130928: 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, 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