module Network.MPD.Core (
MonadMPD(..),
MPD, MPDError(..), ACKType(..), Response, Host, Port, Password,
withMPDEx,
getResponse, kill,
) where
import Network.MPD.Utils
import Network.MPD.Core.Class
import Network.MPD.Core.Error
import Data.Char (isDigit)
import Control.Applicative (Applicative(..), (<$>))
import Control.Monad (ap, unless)
import Control.Monad.Error (ErrorT(..), MonadError(..))
import Control.Monad.Reader (ReaderT(..), ask)
import Control.Monad.State (StateT, MonadIO(..), modify, get, evalStateT)
import qualified Data.Foldable as F
import Data.List (isPrefixOf)
import Network (PortID(..), withSocketsDo, connectTo)
import System.IO (Handle, hPutStrLn, hReady, hClose, hFlush)
import System.IO.Error (isEOFError)
import System.IO.Unsafe (unsafeInterleaveIO)
import qualified System.IO.UTF8 as U
type Host = String
type Port = Integer
newtype MPD a =
MPD { runMPD :: ErrorT MPDError
(StateT MPDState
(ReaderT (Host, Port) IO)) a
} deriving (Functor, Monad, MonadIO, MonadError MPDError)
instance Applicative MPD where
(<*>) = ap
pure = return
instance MonadMPD MPD where
open = mpdOpen
close = mpdClose
send = mpdSend
receive = mpdReceive
getHandle = MPD $ get >>= return . stHandle
getPassword = MPD $ get >>= return . stPassword
setPassword pw = MPD $ modify (\st -> st { stPassword = pw })
getVersion = MPD $ get >>= return . stVersion
data MPDState =
MPDState { stHandle :: Maybe Handle
, stPassword :: String
, stVersion :: (Int, Int, Int)
}
type Response = Either MPDError
withMPDEx :: Host -> Port -> Password -> MPD a -> IO (Response a)
withMPDEx host port pw x = withSocketsDo $
runReaderT (evalStateT (runErrorT . runMPD $ open >> x) initState)
(host, port)
where initState = MPDState Nothing pw (0, 0, 0)
mpdOpen :: MPD ()
mpdOpen = MPD $ do
(host, port) <- ask
runMPD close
handle <- liftIO (safeConnectTo host port)
modify (\st -> st { stHandle = handle })
F.forM_ handle (const $ runMPD checkConn >>= flip unless (runMPD close))
where
safeConnectTo host@('/':_) _ =
(Just <$> connectTo "" (UnixSocket host))
`catch` const (return Nothing)
safeConnectTo host port =
(Just <$> connectTo host (PortNumber $ fromInteger port))
`catch` const (return Nothing)
checkConn = do
msg <- mpdReceive >>= checkMsg
if isPrefixOf "OK MPD" msg
then do
MPD $ maybe (throwError $ Custom "Couldn't determine MPD version")
(\v -> modify (\st -> st { stVersion = v }))
(parseVersion msg)
return True
else return False
checkMsg ls =
if null ls
then throwError $ Custom "No welcome message"
else return $ head ls
parseVersion = parseTriple '.' parseNum . dropWhile (not . isDigit)
mpdClose :: MPD ()
mpdClose =
MPD $ do
get >>= F.mapM_ (liftIO . sendClose) . stHandle
modify (\st -> st { stHandle = Nothing })
where
sendClose handle =
(hPutStrLn handle "close" >> hReady handle >> hClose handle)
`catch` whenEOF (return ())
whenEOF result err
| isEOFError err = result
| otherwise = ioError err
mpdSend :: String -> MPD ()
mpdSend str = MPD $ get >>= maybe (throwError NoMPD) go . stHandle
where
go handle =
unless (null str) $
liftIO $ U.hPutStrLn handle str >> hFlush handle
mpdReceive :: MPD [String]
mpdReceive = getHandle >>= maybe (throwError NoMPD) recv
where
recv handle = MPD $
liftIO ((Right <$> getLines handle) `catch` (return . Left))
>>= either (\err -> if isEOFError err then
modify (\st -> st { stHandle = Nothing })
>> throwError TimedOut
else liftIO (ioError err))
return
getLines handle = do
l <- U.hGetLine handle
if "OK" `isPrefixOf` l || "ACK" `isPrefixOf` l
then return [l]
else do ls <- unsafeInterleaveIO $ getLines handle
return (l:ls)
ignore :: (Monad m) => m a -> m ()
ignore x = x >> return ()
kill :: (MonadMPD m) => m ()
kill = ignore (send "kill") `catchError` cleanup
where
cleanup e = if e == TimedOut then close else throwError e
getResponse :: (MonadMPD m) => String -> m [String]
getResponse cmd = (send cmd >> receive >>= parseResponse) `catchError` sendpw
where
sendpw e@(ACK Auth _) = do
pw <- getPassword
if null pw
then throwError e
else do send ("password " ++ pw) >> receive >>= parseResponse
send cmd >> receive >>= parseResponse
sendpw e =
throwError e
parseResponse :: (MonadError MPDError m) => [String] -> m [String]
parseResponse xs
| null xs = throwError $ NoMPD
| isPrefixOf "ACK" (head xs) = throwError $ parseAck (head xs)
| otherwise = return $ Prelude.takeWhile ("OK" /=) xs
parseAck :: String -> MPDError
parseAck s = ACK ack msg
where
ack = case code of
2 -> InvalidArgument
3 -> InvalidPassword
4 -> Auth
5 -> UnknownCommand
50 -> FileNotFound
51 -> PlaylistMax
52 -> System
53 -> PlaylistLoad
54 -> Busy
55 -> NotPlaying
56 -> FileExists
_ -> UnknownACK
(code, _, msg) = splitAck s
splitAck :: String -> (Int, String, String)
splitAck s = (read code, cmd, msg)
where
(code, notCode) = between '[' '@' s
(cmd, notCmd) = between '{' '}' notCode
msg = drop 1 $ dropWhile (' ' ==) notCmd
between a b xs = let (_, y) = break (== a) xs
in break (== b) (drop 1 y)