{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeFamilies #-} -- | A generic interface to online radio services module Radio ( SongMeta(..) , Radio(..) , NeedLogin(..) , getLordDir , getPidFile , getLogFile , getStateFile , writeLog ) where import Codec.Binary.UTF8.String (encodeString) import Control.Applicative ((<$>)) import Control.Concurrent (forkIO, threadDelay) import Control.Concurrent.MVar import qualified Control.Exception as E import Control.Monad (liftM, when, void) import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as C import Data.Conduit (runResourceT, ($$+-)) import Data.Conduit.Binary (sinkFile) import Data.Yaml import Network.HTTP.Conduit hiding (path) import Network.MPD hiding (play, Value) import qualified Network.MPD as MPD import System.Directory (doesFileExist, getHomeDirectory) import System.IO import System.IO.Unsafe (unsafePerformIO) import System.Log.FastLogger eof :: MVar () eof = unsafePerformIO newEmptyMVar data SongMeta = SongMeta { artist :: String , album :: String , title :: String } class FromJSON a => Radio a where data Param a :: * parsePlaylist :: Value -> [a] getPlaylist :: Param a -> IO [a] songUrl :: Param a -> a -> IO String songMeta :: a -> SongMeta tagged :: a -> Bool -- Mpd can play remote mp3 files directly. -- For m4a files, lord download it as ~/.lord/lord.m4a playable :: a -> Bool playable _ = True reportRequired :: a -> Bool reportRequired _ = False report :: Param a -> a -> IO () report _ _ = return () reportLoop :: Param a -> a -> IO () reportLoop param x = do time <- liftM stTime <$> withMPD status case time of Right (elapsed, _) -> if elapsed < 30 then threadDelay (5*1000000) >> reportLoop param x else report param x 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) 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 play logger reqData xs ) threadDelay (3*1000000) mpdLoad m4a play logger reqData xs where m4a = "lord/lord.m4a" mpdLoad :: Path -> IO () mpdLoad path | playable x = do withMPD $ do clear 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 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 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 class (Radio a, ToJSON (Param a), ToJSON (Config a)) => NeedLogin a where login :: String -> IO (Param a) login keywords = do hSetBuffering stdout NoBuffering hSetEcho stdin True putStrLn "Please Log in" putStr "Email: " email <- getLine putStr "Password: " hSetEcho stdin False pwd <- getLine hSetEcho stdin True putStrLn "" mtoken <- createSession keywords email pwd case mtoken of Just tok -> do saveToken tok return tok Nothing -> do putStrLn "ERROR: Invalid email or password!" login keywords createSession :: String -> String -> String -> IO (Maybe (Param a)) data Config a :: * mkConfig :: Param a -> Config a saveToken :: Param a -> IO () saveToken tok = do home <- getLordDir let yml = home ++ "/lord.yml" exist <- doesFileExist yml bs <- if exist then B.readFile yml else return "" let config = mkConfig tok B.writeFile yml $ B.append bs (encode config) putStrLn $ "Your token has been saved to " ++ yml readToken :: String -> IO (Maybe (Param a)) getLordDir :: IO FilePath getLordDir = (++ "/.lord") <$> getHomeDirectory getPidFile :: IO FilePath getPidFile = (++ "/lord.pid") <$> getLordDir getLogFile :: IO FilePath getLogFile = (++ "/lord.log") <$> getLordDir getStateFile :: IO FilePath getStateFile = (++ "/lordstate") <$> getLordDir formatLogMessage :: IO ZonedDate -> String -> IO [LogStr] formatLogMessage getdate msg = do now <- getdate return [ LB now , LB " : " , LS $ encodeString msg , LB "\n" ] writeLog :: Logger -> String -> IO () writeLog l msg = formatLogMessage (loggerDate l) msg >>= loggerPutStr l