libmpd 0.3.1 → 0.4.0
raw patch · 26 files changed
+1599/−1183 lines, 26 filesdep +QuickCheckdep ~basedep ~filepathdep ~mtlnew-component:exe:testnew-uploaderPVP ok
version bump matches the API change (PVP)
Dependencies added: QuickCheck
Dependency ranges changed: base, filepath, mtl, network, utf8-string
API changes (from Hackage documentation)
- Network.MPD: Match :: Meta -> String -> Match
- Network.MPD: data Match
- Network.MPD: findAlbum :: Album -> MPD [Song]
- Network.MPD: findArtist :: Artist -> MPD [Song]
- Network.MPD: findTitle :: Title -> MPD [Song]
- Network.MPD: reconnect :: MPD ()
- Network.MPD: searchAlbum :: Album -> MPD [Song]
- Network.MPD: searchArtist :: Artist -> MPD [Song]
- Network.MPD: searchTitle :: Title -> MPD [Song]
- Network.MPD: type Query = [Match]
+ Network.MPD: (<&>) :: Query -> Query -> Query
+ Network.MPD: (=?) :: Meta -> String -> Query
+ Network.MPD: AlbumMode :: ReplayGainMode
+ Network.MPD: Database :: Subsystem
+ Network.MPD: Mixer :: Subsystem
+ Network.MPD: Off :: ReplayGainMode
+ Network.MPD: Options :: Subsystem
+ Network.MPD: Output :: Subsystem
+ Network.MPD: Player :: Subsystem
+ Network.MPD: Playlist :: Subsystem
+ Network.MPD: SongObj :: ObjectType
+ Network.MPD: StoredPlaylist :: Subsystem
+ Network.MPD: TrackMode :: ReplayGainMode
+ Network.MPD: Update :: Subsystem
+ Network.MPD: anything :: Query
+ Network.MPD: class (Monad m, MonadError MPDError m) => MonadMPD m
+ Network.MPD: consume :: (MonadMPD m) => Bool -> m ()
+ Network.MPD: data ObjectType
+ Network.MPD: data Query
+ Network.MPD: data ReplayGainMode
+ Network.MPD: data Subsystem
+ Network.MPD: decoders :: (MonadMPD m) => m [(String, [(String, String)])]
+ Network.MPD: defaultCount :: Count
+ Network.MPD: defaultDevice :: Device
+ Network.MPD: defaultSong :: Song
+ Network.MPD: defaultStats :: Stats
+ Network.MPD: defaultStatus :: Status
+ Network.MPD: findAdd :: (MonadMPD m) => Query -> m ()
+ Network.MPD: idle :: (MonadMPD m) => m [Subsystem]
+ Network.MPD: listPlaylists :: (MonadMPD m) => m [PlaylistName]
+ Network.MPD: noidle :: (MonadMPD m) => m ()
+ Network.MPD: playlistAdd :: (MonadMPD m) => PlaylistName -> Path -> m [Path]
+ Network.MPD: playlistAdd_ :: (MonadMPD m) => PlaylistName -> Path -> m ()
+ Network.MPD: playlistClear :: (MonadMPD m) => PlaylistName -> m ()
+ Network.MPD: playlistDelete :: (MonadMPD m) => PlaylistName -> Integer -> m ()
+ Network.MPD: playlistMove :: (MonadMPD m) => PlaylistName -> Integer -> Integer -> m ()
+ Network.MPD: replayGainMode :: (MonadMPD m) => ReplayGainMode -> m ()
+ Network.MPD: replayGainStatus :: (MonadMPD m) => m [String]
+ Network.MPD: rescan :: (MonadMPD m) => [Path] -> m ()
+ Network.MPD: sgAux :: Song -> [(String, String)]
+ Network.MPD: single :: (MonadMPD m) => Bool -> m ()
+ Network.MPD: stConsume :: Status -> Bool
+ Network.MPD: stNextSongID :: Status -> Maybe PLIndex
+ Network.MPD: stNextSongPos :: Status -> Maybe PLIndex
+ Network.MPD: stSingle :: Status -> Bool
+ Network.MPD: stickerDelete :: (MonadMPD m) => ObjectType -> String -> String -> m ()
+ Network.MPD: stickerFind :: (MonadMPD m) => ObjectType -> String -> String -> m [(String, String)]
+ Network.MPD: stickerGet :: (MonadMPD m) => ObjectType -> String -> String -> m [String]
+ Network.MPD: stickerList :: (MonadMPD m) => ObjectType -> String -> m [(String, String)]
+ Network.MPD: stickerSet :: (MonadMPD m) => ObjectType -> String -> String -> String -> m ()
+ Network.MPD: type Host = String
+ Network.MPD: type Password = String
+ Network.MPD: type Port = Integer
+ Network.MPD.Core: ACK :: ACKType -> String -> MPDError
+ Network.MPD.Core: Auth :: ACKType
+ Network.MPD.Core: Busy :: ACKType
+ Network.MPD.Core: Custom :: String -> MPDError
+ Network.MPD.Core: FileExists :: ACKType
+ Network.MPD.Core: FileNotFound :: ACKType
+ Network.MPD.Core: InvalidArgument :: ACKType
+ Network.MPD.Core: InvalidPassword :: ACKType
+ Network.MPD.Core: NoMPD :: MPDError
+ Network.MPD.Core: NotPlaying :: ACKType
+ Network.MPD.Core: PlaylistLoad :: ACKType
+ Network.MPD.Core: PlaylistMax :: ACKType
+ Network.MPD.Core: System :: ACKType
+ Network.MPD.Core: TimedOut :: MPDError
+ Network.MPD.Core: Unexpected :: String -> MPDError
+ Network.MPD.Core: UnknownACK :: ACKType
+ Network.MPD.Core: UnknownCommand :: ACKType
+ Network.MPD.Core: class (Monad m, MonadError MPDError m) => MonadMPD m
+ Network.MPD.Core: close :: (MonadMPD m) => m ()
+ Network.MPD.Core: data ACKType
+ Network.MPD.Core: data MPD a
+ Network.MPD.Core: data MPDError
+ Network.MPD.Core: getPassword :: (MonadMPD m) => m Password
+ Network.MPD.Core: getResponse :: (MonadMPD m) => String -> m [String]
+ Network.MPD.Core: instance Applicative MPD
+ Network.MPD.Core: instance Functor MPD
+ Network.MPD.Core: instance Monad MPD
+ Network.MPD.Core: instance MonadError MPDError MPD
+ Network.MPD.Core: instance MonadIO MPD
+ Network.MPD.Core: instance MonadMPD MPD
+ Network.MPD.Core: kill :: (MonadMPD m) => m ()
+ Network.MPD.Core: open :: (MonadMPD m) => m ()
+ Network.MPD.Core: send :: (MonadMPD m) => String -> m String
+ Network.MPD.Core: type Host = String
+ Network.MPD.Core: type Password = String
+ Network.MPD.Core: type Port = Integer
+ Network.MPD.Core: type Response = Either MPDError
+ Network.MPD.Core: withMPDEx :: Host -> Port -> Password -> MPD a -> IO (Response a)
- Network.MPD: Song :: String -> String -> String -> String -> String -> String -> String -> String -> Seconds -> Int -> (Int, Int) -> (Int, Int) -> Maybe PLIndex -> Song
+ Network.MPD: Song :: String -> String -> String -> String -> String -> String -> String -> String -> Seconds -> Int -> (Int, Int) -> Maybe (Int, Int) -> Maybe PLIndex -> [(String, String)] -> Song
- Network.MPD: Status :: State -> Int -> Bool -> Bool -> Integer -> Integer -> Maybe PLIndex -> Maybe PLIndex -> (Seconds, Seconds) -> Int -> Seconds -> (Int, Int, Int) -> Integer -> String -> Status
+ Network.MPD: Status :: State -> Int -> Bool -> Bool -> Integer -> Integer -> Maybe PLIndex -> Maybe PLIndex -> Maybe PLIndex -> Maybe PLIndex -> (Seconds, Seconds) -> Int -> Seconds -> (Int, Int, Int) -> Integer -> Bool -> Bool -> String -> Status
- Network.MPD: add :: PlaylistName -> Path -> MPD [Path]
+ Network.MPD: add :: (MonadMPD m) => Path -> m [Path]
- Network.MPD: addId :: Path -> MPD Integer
+ Network.MPD: addId :: (MonadMPD m) => Path -> Maybe Integer -> m Integer
- Network.MPD: addMany :: PlaylistName -> [Path] -> MPD ()
+ Network.MPD: addMany :: (MonadMPD m) => PlaylistName -> [Path] -> m ()
- Network.MPD: add_ :: PlaylistName -> Path -> MPD ()
+ Network.MPD: add_ :: (MonadMPD m) => Path -> m ()
- Network.MPD: clear :: PlaylistName -> MPD ()
+ Network.MPD: clear :: (MonadMPD m) => m ()
- Network.MPD: clearError :: MPD ()
+ Network.MPD: clearError :: (MonadMPD m) => m ()
- Network.MPD: close :: MPD ()
+ Network.MPD: close :: (MonadMPD m) => m ()
- Network.MPD: commands :: MPD [String]
+ Network.MPD: commands :: (MonadMPD m) => m [String]
- Network.MPD: complete :: String -> MPD [Either Path Song]
+ Network.MPD: complete :: (MonadMPD m) => String -> m [Either Path Song]
- Network.MPD: count :: Query -> MPD Count
+ Network.MPD: count :: (MonadMPD m) => Query -> m Count
- Network.MPD: crop :: Maybe PLIndex -> Maybe PLIndex -> MPD ()
+ Network.MPD: crop :: (MonadMPD m) => Maybe PLIndex -> Maybe PLIndex -> m ()
- Network.MPD: crossfade :: Seconds -> MPD ()
+ Network.MPD: crossfade :: (MonadMPD m) => Seconds -> m ()
- Network.MPD: currentSong :: MPD (Maybe Song)
+ Network.MPD: currentSong :: (Functor m, MonadMPD m) => m (Maybe Song)
- Network.MPD: delete :: PlaylistName -> PLIndex -> MPD ()
+ Network.MPD: delete :: (MonadMPD m) => PLIndex -> m ()
- Network.MPD: deleteMany :: PlaylistName -> [PLIndex] -> MPD ()
+ Network.MPD: deleteMany :: (MonadMPD m) => PlaylistName -> [PLIndex] -> m ()
- Network.MPD: disableOutput :: Int -> MPD ()
+ Network.MPD: disableOutput :: (MonadMPD m) => Int -> m ()
- Network.MPD: enableOutput :: Int -> MPD ()
+ Network.MPD: enableOutput :: (MonadMPD m) => Int -> m ()
- Network.MPD: find :: Query -> MPD [Song]
+ Network.MPD: find :: (MonadMPD m) => Query -> m [Song]
- Network.MPD: getPlaylist :: MPD [Song]
+ Network.MPD: getPlaylist :: (MonadMPD m) => m [Song]
- Network.MPD: kill :: MPD ()
+ Network.MPD: kill :: (MonadMPD m) => m ()
- Network.MPD: list :: Meta -> Query -> MPD [String]
+ Network.MPD: list :: (MonadMPD m) => Meta -> Query -> m [String]
- Network.MPD: listAlbum :: Artist -> Album -> MPD [Song]
+ Network.MPD: listAlbum :: (MonadMPD m) => Artist -> Album -> m [Song]
- Network.MPD: listAlbums :: Maybe Artist -> MPD [Album]
+ Network.MPD: listAlbums :: (MonadMPD m) => Maybe Artist -> m [Album]
- Network.MPD: listAll :: Path -> MPD [Path]
+ Network.MPD: listAll :: (MonadMPD m) => Path -> m [Path]
- Network.MPD: listAllInfo :: Path -> MPD [Either Path Song]
+ Network.MPD: listAllInfo :: (MonadMPD m) => Path -> m [Either Path Song]
- Network.MPD: listArtists :: MPD [Artist]
+ Network.MPD: listArtists :: (MonadMPD m) => m [Artist]
- Network.MPD: listPlaylist :: PlaylistName -> MPD [Path]
+ Network.MPD: listPlaylist :: (MonadMPD m) => PlaylistName -> m [Path]
- Network.MPD: listPlaylistInfo :: PlaylistName -> MPD [Song]
+ Network.MPD: listPlaylistInfo :: (MonadMPD m) => PlaylistName -> m [Song]
- Network.MPD: load :: PlaylistName -> MPD ()
+ Network.MPD: load :: (MonadMPD m) => PlaylistName -> m ()
- Network.MPD: lsDirs :: Path -> MPD [Path]
+ Network.MPD: lsDirs :: (MonadMPD m) => Path -> m [Path]
- Network.MPD: lsFiles :: Path -> MPD [Path]
+ Network.MPD: lsFiles :: (MonadMPD m) => Path -> m [Path]
- Network.MPD: lsInfo :: Path -> MPD [Either Path Song]
+ Network.MPD: lsInfo :: (MonadMPD m) => Path -> m [Either Path Song]
- Network.MPD: lsPlaylists :: MPD [PlaylistName]
+ Network.MPD: lsPlaylists :: (MonadMPD m) => m [PlaylistName]
- Network.MPD: move :: PlaylistName -> PLIndex -> Integer -> MPD ()
+ Network.MPD: move :: (MonadMPD m) => PLIndex -> Integer -> m ()
- Network.MPD: next :: MPD ()
+ Network.MPD: next :: (MonadMPD m) => m ()
- Network.MPD: notCommands :: MPD [String]
+ Network.MPD: notCommands :: (MonadMPD m) => m [String]
- Network.MPD: outputs :: MPD [Device]
+ Network.MPD: outputs :: (MonadMPD m) => m [Device]
- Network.MPD: password :: String -> MPD ()
+ Network.MPD: password :: (MonadMPD m) => String -> m ()
- Network.MPD: pause :: Bool -> MPD ()
+ Network.MPD: pause :: (MonadMPD m) => Bool -> m ()
- Network.MPD: ping :: MPD ()
+ Network.MPD: ping :: (MonadMPD m) => m ()
- Network.MPD: plChanges :: Integer -> MPD [Song]
+ Network.MPD: plChanges :: (MonadMPD m) => Integer -> m [Song]
- Network.MPD: plChangesPosId :: Integer -> MPD [(PLIndex, PLIndex)]
+ Network.MPD: plChangesPosId :: (MonadMPD m) => Integer -> m [(PLIndex, PLIndex)]
- Network.MPD: play :: Maybe PLIndex -> MPD ()
+ Network.MPD: play :: (MonadMPD m) => Maybe PLIndex -> m ()
- Network.MPD: playlist :: MPD [(PLIndex, Path)]
+ Network.MPD: playlist :: (MonadMPD m) => m [(PLIndex, Path)]
- Network.MPD: playlistFind :: Query -> MPD [Song]
+ Network.MPD: playlistFind :: (MonadMPD m) => Query -> m [Song]
- Network.MPD: playlistInfo :: Maybe PLIndex -> MPD [Song]
+ Network.MPD: playlistInfo :: (MonadMPD m) => Maybe (Either PLIndex (Int, Int)) -> m [Song]
- Network.MPD: playlistSearch :: Query -> MPD [Song]
+ Network.MPD: playlistSearch :: (MonadMPD m) => Query -> m [Song]
- Network.MPD: previous :: MPD ()
+ Network.MPD: previous :: (MonadMPD m) => m ()
- Network.MPD: prune :: MPD ()
+ Network.MPD: prune :: (MonadMPD m) => m ()
- Network.MPD: random :: Bool -> MPD ()
+ Network.MPD: random :: (MonadMPD m) => Bool -> m ()
- Network.MPD: rename :: PlaylistName -> PlaylistName -> MPD ()
+ Network.MPD: rename :: (MonadMPD m) => PlaylistName -> PlaylistName -> m ()
- Network.MPD: repeat :: Bool -> MPD ()
+ Network.MPD: repeat :: (MonadMPD m) => Bool -> m ()
- Network.MPD: rm :: PlaylistName -> MPD ()
+ Network.MPD: rm :: (MonadMPD m) => PlaylistName -> m ()
- Network.MPD: save :: PlaylistName -> MPD ()
+ Network.MPD: save :: (MonadMPD m) => PlaylistName -> m ()
- Network.MPD: search :: Query -> MPD [Song]
+ Network.MPD: search :: (MonadMPD m) => Query -> m [Song]
- Network.MPD: seek :: Maybe PLIndex -> Seconds -> MPD ()
+ Network.MPD: seek :: (MonadMPD m) => Maybe PLIndex -> Seconds -> m ()
- Network.MPD: setVolume :: Int -> MPD ()
+ Network.MPD: setVolume :: (MonadMPD m) => Int -> m ()
- Network.MPD: sgDisc :: Song -> (Int, Int)
+ Network.MPD: sgDisc :: Song -> Maybe (Int, Int)
- Network.MPD: shuffle :: MPD ()
+ Network.MPD: shuffle :: (MonadMPD m) => Maybe (Int, Int) -> m ()
- Network.MPD: stats :: MPD Stats
+ Network.MPD: stats :: (MonadMPD m) => m Stats
- Network.MPD: status :: MPD Status
+ Network.MPD: status :: (MonadMPD m) => m Status
- Network.MPD: stop :: MPD ()
+ Network.MPD: stop :: (MonadMPD m) => m ()
- Network.MPD: swap :: PLIndex -> PLIndex -> MPD ()
+ Network.MPD: swap :: (MonadMPD m) => PLIndex -> PLIndex -> m ()
- Network.MPD: tagTypes :: MPD [String]
+ Network.MPD: tagTypes :: (MonadMPD m) => m [String]
- Network.MPD: toggle :: MPD ()
+ Network.MPD: toggle :: (MonadMPD m) => m ()
- Network.MPD: type Response a = Either MPDError a
+ Network.MPD: type Response = Either MPDError
- Network.MPD: update :: [Path] -> MPD ()
+ Network.MPD: update :: (MonadMPD m) => [Path] -> m ()
- Network.MPD: updateId :: [Path] -> MPD Integer
+ Network.MPD: updateId :: (MonadMPD m) => [Path] -> m Integer
- Network.MPD: urlHandlers :: MPD [String]
+ Network.MPD: urlHandlers :: (MonadMPD m) => m [String]
- Network.MPD: volume :: Int -> MPD ()
+ Network.MPD: volume :: (MonadMPD m) => Int -> m ()
- Network.MPD: withMPDEx :: String -> Integer -> IO (Maybe String) -> MPD a -> IO (Response a)
+ Network.MPD: withMPDEx :: Host -> Port -> Password -> MPD a -> IO (Response a)
Files
- ChangeLog +10/−0
- Network/MPD.hs +12/−24
- Network/MPD/Commands.hs +426/−395
- Network/MPD/Commands/Arg.hs +63/−0
- Network/MPD/Commands/Parse.hs +171/−0
- Network/MPD/Commands/Query.hs +56/−0
- Network/MPD/Commands/Types.hs +194/−0
- Network/MPD/Core.hs +145/−132
- Network/MPD/Core/Class.hs +30/−0
- Network/MPD/Core/Error.hs +50/−0
- Network/MPD/Parse.hs +0/−156
- Network/MPD/SocketConn.hs +0/−83
- Network/MPD/Types.hs +0/−94
- Network/MPD/Utils.hs +16/−14
- README +0/−50
- README.md +99/−0
- TODO +0/−2
- libmpd.cabal +57/−16
- tests/Commands.hs +135/−56
- tests/Displayable.hs +13/−20
- tests/Main.hs +6/−1
- tests/Properties.hs +26/−109
- tests/coverage +0/−16
- tests/coverage.lhs +40/−0
- tests/run-tests +0/−15
- tests/run-tests.lhs +50/−0
ChangeLog view
@@ -1,3 +1,13 @@+* v0.4.0, 2010-03-26+ - New maintainer: Joachim Fasting <joachim.fasting@gmail.com>+ - Support QuickCheck 2+ - Better MPD api support+ Should be mostly compatible with mpd 0.16+ - Separated operations on current playlist from those on specific+ playlists+ - Fixed password sending+ - Several minor fixes and cleanups+ * v0.3.1, 2008-09-14 - Now reconnects if MPD closes the connection.
Network/MPD.hs view
@@ -1,9 +1,8 @@ -- | Module : Network.MPD--- Copyright : (c) Ben Sinclair 2005-2008+-- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010 -- License : LGPL (see LICENSE)--- Maintainer : bsinclai@turing.une.edu.au+-- Maintainer : Joachim Fasting <joachim.fasting@gmail.com> -- Stability : alpha--- Portability : Haskell 98 -- -- An MPD client library. MPD is a daemon for playing music that is -- controlled over a network socket. Its site is at <http://www.musicpd.org/>.@@ -14,7 +13,8 @@ module Network.MPD ( -- * Basic data types- MPD, MPDError(..), ACKType(..), Response,+ MonadMPD, MPD, MPDError(..), ACKType(..), Response,+ Host, Port, Password, -- * Connections withMPD, withMPDEx, module Network.MPD.Commands,@@ -22,13 +22,10 @@ import Network.MPD.Commands import Network.MPD.Core-import Network.MPD.SocketConn+import Network.MPD.Utils import Control.Monad (liftM)-import Data.Maybe (listToMaybe)-import Data.IORef (newIORef, atomicModifyIORef) import System.Environment (getEnv)-import System.IO import System.IO.Error (isDoesNotExistError, ioError) -- | A wrapper for 'withMPDEx' that uses localhost:6600 as the default@@ -39,25 +36,16 @@ -- Examples: -- -- > withMPD $ play Nothing--- > withMPD $ add_ "" "tool" >> play Nothing >> currentSong+-- > withMPD $ add_ "tool" >> play Nothing >> currentSong withMPD :: MPD a -> IO (Response a) withMPD m = do- port <- liftM read (getEnvDefault "MPD_PORT" "6600")- (pw,host) <- liftM (break (== '@')) (getEnvDefault "MPD_HOST" "localhost")- let (host',pw') = if null host then (pw,host) else (drop 1 host,pw)- pwGen <- mkPasswordGen [pw']- withMPDEx host' port pwGen m+ port <- read `liftM` getEnvDefault "MPD_PORT" "6600"+ (host, pw) <- parseHost `liftM` getEnvDefault "MPD_HOST" "localhost"+ withMPDEx host port pw m where getEnvDefault x dflt = catch (getEnv x) (\e -> if isDoesNotExistError e then return dflt else ioError e)---- | Create an action that produces passwords for a connection. You--- can pass these to 'withMPDEx' and it will use them to get passwords--- to send to the server until one works or it runs out of them.------ > do gen <- mkPasswordGen ["password1", "password2"]--- > withMPDEx "localhost" 6600 gen (update [])-mkPasswordGen :: [String] -> IO (IO (Maybe String))-mkPasswordGen = liftM f . newIORef- where f = flip atomicModifyIORef $ \xs -> (drop 1 xs, listToMaybe xs)+ parseHost s = case breakChar '@' s of+ (host, "") -> (host, "")+ (pw, host) -> (host, pw)
Network/MPD/Commands.hs view
@@ -1,51 +1,66 @@-{-# LANGUAGE PatternGuards, TypeSynonymInstances #-}+{-# LANGUAGE PatternGuards #-} -- | Module : Network.MPD.Commands--- Copyright : (c) Ben Sinclair 2005-2008+-- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010 -- License : LGPL (see LICENSE)--- Maintainer : bsinclai@turing.une.edu.au+-- Maintainer : Joachim Fasting <joachim.fasting@gmail.com> -- Stability : alpha--- Portability : unportable (uses PatternGuards and TypeSynonymInstances) -- -- Interface to the user commands supported by MPD. module Network.MPD.Commands ( -- * Command related data types- Artist, Album, Title, PlaylistName, Path,- Meta(..), Match(..), Query,- module Network.MPD.Types,+ module Network.MPD.Commands.Types, - -- * Admin commands- disableOutput, enableOutput, kill, outputs, update,+ -- * Query interface+ module Network.MPD.Commands.Query, - -- * Database commands- find, list, listAll, listAllInfo, lsInfo, search, count,+ -- * Querying MPD's status+ clearError, currentSong, idle, noidle, status, stats, - -- * Playlist commands- -- $playlist- add, add_, addId, clear, currentSong, delete, load, move,- playlistInfo, listPlaylist, listPlaylistInfo, playlist, plChanges,- plChangesPosId, playlistFind, playlistSearch, rm, rename, save, shuffle,- swap,+ -- * Playback options+ consume, crossfade, random, repeat, setVolume, single, replayGainMode,+ replayGainStatus, - -- * Playback commands- crossfade, next, pause, play, previous, random, repeat, seek, setVolume,- volume, stop,+ -- * Controlling playback+ next, pause, play, previous, seek, stop, - -- * Miscellaneous commands- clearError, close, commands, notCommands, password, ping, reconnect, stats,- status, tagTypes, urlHandlers,+ -- * The current playlist+ add, add_, addId, clear, delete, move, playlist, playlistFind,+ playlistInfo, playlistSearch, plChanges, plChangesPosId, shuffle, swap, + -- * Stored playlist+ listPlaylist, listPlaylistInfo, listPlaylists, load, playlistAdd,+ playlistAdd_, playlistClear, playlistDelete, playlistMove, rename, rm,+ save,++ -- * The music database+ count, find, findAdd, list, listAll, listAllInfo, lsInfo, search, update,+ rescan,++ -- * Stickers+ stickerGet, stickerSet, stickerDelete, stickerList, stickerFind,++ -- * Connection+ close, kill, password, ping,++ -- * Audio output devices+ disableOutput, enableOutput, outputs,++ -- * Reflection+ commands, notCommands, tagTypes, urlHandlers, decoders,+ -- * Extensions\/shortcuts addMany, deleteMany, complete, crop, prune, lsDirs, lsFiles, lsPlaylists,- findArtist, findAlbum, findTitle, listArtists, listAlbums, listAlbum,- searchArtist, searchAlbum, searchTitle, getPlaylist, toggle, updateId+ listArtists, listAlbums, listAlbum, getPlaylist, toggle, updateId, volume ) where +import Network.MPD.Commands.Arg+import Network.MPD.Commands.Parse+import Network.MPD.Commands.Query+import Network.MPD.Commands.Types import Network.MPD.Core import Network.MPD.Utils-import Network.MPD.Parse-import Network.MPD.Types import Control.Monad (liftM, unless) import Control.Monad.Error (throwError)@@ -55,421 +70,448 @@ import System.FilePath (dropFileName) ----- Data types+-- Querying MPD's status -- --- Arguments for getResponse are accumulated as strings in values of--- this type after being converted from whatever type (an instance of--- MPDArg) they were to begin with.-newtype Args = Args [String]- deriving Show---- A uniform interface for argument preparation--- The basic idea is that one should be able--- to magically prepare an argument for use with--- an MPD command, without necessarily knowing/\caring--- how it needs to be represented internally.-class Show a => MPDArg a where- prep :: a -> Args- -- Note that because of this, we almost- -- never have to actually provide- -- an implementation of 'prep'- prep = Args . return . show---- | Groups together arguments to getResponse.-infixl 3 <++>-(<++>) :: (MPDArg a, MPDArg b) => a -> b -> Args-x <++> y = Args $ xs ++ ys- where Args xs = prep x- Args ys = prep y---- | Converts a command name and a string of arguments into the string--- to hand to getResponse.-infix 2 <$>-(<$>) :: (MPDArg a) => String -> a -> String-x <$> y = x ++ " " ++ unwords (filter (not . null) y')- where Args y' = prep y--instance MPDArg Args where prep = id--instance MPDArg String where- -- We do this to avoid mangling- -- non-ascii characters with 'show'- prep x = Args ['"' : x ++ "\""]--instance (MPDArg a) => MPDArg (Maybe a) where- prep Nothing = Args []- prep (Just x) = prep x--instance MPDArg Int-instance MPDArg Integer-instance MPDArg Bool where prep = Args . return . showBool+-- | Clear the current error message in status.+clearError :: MonadMPD m => m ()+clearError = getResponse_ "clearerror" -type Artist = String-type Album = String-type Title = String+-- | Get the currently playing song.+currentSong :: (Functor m, MonadMPD m) => m (Maybe Song)+currentSong = do+ cs <- status+ if stState cs == Stopped+ then return Nothing+ else getResponse1 "currentsong" >>=+ fmap Just . runParser parseSong . toAssocList --- | Used for commands which require a playlist name.--- If empty, the current playlist is used.-type PlaylistName = String+-- | Wait until there is a noteworthy change in one or more of MPD's+-- susbystems. Note that running this command will block until either 'idle'+-- returns or is cancelled by 'noidle'.+idle :: MonadMPD m => m [Subsystem]+idle =+ mapM (\("changed", system) -> case system of "database" -> return Database+ "update" -> return Update+ "stored_playlist" -> return StoredPlaylist+ "playlist" -> return Playlist+ "player" -> return Player+ "mixer" -> return Mixer+ "output" -> return Output+ "options" -> return Options+ k -> fail ("Unknown subsystem: " ++ k))+ =<< toAssocList `liftM` getResponse "idle" --- | Used for commands which require a path within the database.--- If empty, the root path is used.-type Path = String+-- | Cancel 'idle'.+noidle :: MonadMPD m => m ()+noidle = getResponse_ "noidle" --- | Available metadata types\/scope modifiers, used for searching the--- database for entries with certain metadata values.-data Meta = Artist | Album | Title | Track | Name | Genre | Date- | Composer | Performer | Disc | Any | Filename- deriving Show+-- | Get server statistics.+stats :: MonadMPD m => m Stats+stats = getResponse "stats" >>= runParser parseStats -instance MPDArg Meta+-- | Get the server's status.+status :: MonadMPD m => m Status+status = getResponse "status" >>= runParser parseStatus --- | When searching for specific items in a collection--- of songs, we need a reliable way to build predicates. Match is--- one way of achieving this.--- Each Match is a clause, and by putting matches together in lists, we can--- compose queries. ----- For example, to match any song where the value of artist is \"Foo\", we use:------ > Match Artist "Foo"------ In composite matches (queries), all clauses must be satisfied, which means--- that each additional clause narrows the search. For example, to match--- any song where the value of artist is \"Foo\" AND the value of album is--- \"Bar\", we use:------ > [Match Artist "Foo", Match Album "Bar"]+-- Playback options ----- By adding additional clauses we can narrow the search even more, but this--- is usually not necessary.-data Match = Match Meta String -instance Show Match where- show (Match meta query) = show meta ++ " \"" ++ query ++ "\""- showList xs _ = unwords $ map show xs+-- | Set consume mode+consume :: MonadMPD m => Bool -> m ()+consume = getResponse_ . ("consume" <$>) --- | A query comprises a list of Match predicates-type Query = [Match]+-- | Set crossfading between songs.+crossfade :: MonadMPD m => Seconds -> m ()+crossfade secs = getResponse_ ("crossfade" <$> secs) -instance MPDArg Query where- prep = foldl (<++>) (Args []) . f- where f = map (\(Match m q) -> Args [show m] <++> q)+-- | Set random playing.+random :: MonadMPD m => Bool -> m ()+random = getResponse_ . ("random" <$>) ------ Admin commands---+-- | Set repeating.+repeat :: MonadMPD m => Bool -> m ()+repeat = getResponse_ . ("repeat" <$>) --- | Turn off an output device.-disableOutput :: Int -> MPD ()-disableOutput = getResponse_ . ("disableoutput" <$>)+-- | Set the volume (0-100 percent).+setVolume :: MonadMPD m => Int -> m ()+setVolume = getResponse_ . ("setvol" <$>) --- | Turn on an output device.-enableOutput :: Int -> MPD ()-enableOutput = getResponse_ . ("enableoutput" <$>)+-- | Set single mode+single :: MonadMPD m => Bool -> m ()+single = getResponse_ . ("single" <$>) --- | Retrieve information for all output devices.-outputs :: MPD [Device]-outputs = getResponse "outputs" >>= runParser parseOutputs+-- | Set the replay gain mode.+replayGainMode :: MonadMPD m => ReplayGainMode -> m ()+replayGainMode = getResponse_ . ("replay_gain_mode" <$>) --- | Update the server's database.--- If no paths are given, all paths will be scanned.--- Unreadable or non-existent paths are silently ignored.-update :: [Path] -> MPD ()-update [] = getResponse_ "update"-update [x] = getResponse_ ("update" <$> x)-update xs = getResponses (map ("update" <$>) xs) >> return ()+-- | Get the replay gain options.+replayGainStatus :: MonadMPD m => m [String]+replayGainStatus = getResponse "replay_gain_status" ----- Database commands+-- Controlling playback -- --- | List all metadata of metadata (sic).-list :: Meta -- ^ Metadata to list- -> Query -> MPD [String]-list mtype query = liftM takeValues $ getResponse ("list" <$> mtype <++> query)---- | Non-recursively list the contents of a database directory.-lsInfo :: Path -> MPD [Either Path Song]-lsInfo = lsInfo' "lsinfo"---- | List the songs (without metadata) in a database directory recursively.-listAll :: Path -> MPD [Path]-listAll path = liftM (map snd . filter ((== "file") . fst) . toAssoc)- (getResponse $ "listall" <$> path)+-- | Play the next song.+next :: MonadMPD m => m ()+next = getResponse_ "next" --- | Recursive 'lsInfo'.-listAllInfo :: Path -> MPD [Either Path Song]-listAllInfo = lsInfo' "listallinfo"+-- | Pause playing.+pause :: MonadMPD m => Bool -> m ()+pause = getResponse_ . ("pause" <$>) --- Helper for lsInfo and listAllInfo.-lsInfo' :: String -> Path -> MPD [Either Path Song]-lsInfo' cmd path = do- liftM (extractEntries (Just . Right, const Nothing, Just . Left)) $- takeEntries =<< getResponse (cmd <$> path)+-- | Begin\/continue playing.+play :: MonadMPD m => Maybe PLIndex -> m ()+play Nothing = getResponse_ "play"+play (Just (Pos x)) = getResponse_ ("play" <$> x)+play (Just (ID x)) = getResponse_ ("playid" <$> x) --- | Search the database for entries exactly matching a query.-find :: Query -> MPD [Song]-find query = getResponse ("find" <$> query) >>= takeSongs+-- | Play the previous song.+previous :: MonadMPD m => m ()+previous = getResponse_ "previous" --- | Search the database using case insensitive matching.-search :: Query -> MPD [Song]-search query = getResponse ("search" <$> query) >>= takeSongs+-- | Seek to some point in a song.+-- Seeks in current song if no position is given.+seek :: MonadMPD m => Maybe PLIndex -> Seconds -> m ()+seek (Just (Pos x)) time = getResponse_ ("seek" <$> x <++> time)+seek (Just (ID x)) time = getResponse_ ("seekid" <$> x <++> time)+seek Nothing time = do+ st <- status+ unless (stState st == Stopped) (seek (stSongID st) time) --- | Count the number of entries matching a query.-count :: Query -> MPD Count-count query = getResponse ("count" <$> query) >>= runParser parseCount+-- | Stop playing.+stop :: MonadMPD m => m ()+stop = getResponse_ "stop" ----- Playlist commands+-- The current playlist ----- $playlist--- Unless otherwise noted all playlist commands operate on the current--- playlist. -- This might do better to throw an exception than silently return 0. -- | Like 'add', but returns a playlist id.-addId :: Path -> MPD Integer-addId p = getResponse1 ("addid" <$> p) >>=- parse parseNum id . snd . head . toAssoc+addId :: MonadMPD m => Path -> Maybe Integer -- ^ Optional playlist position+ -> m Integer+addId p pos = liftM (parse parseNum id 0 . snd . head . toAssocList)+ $ getResponse1 ("addid" <$> p <++> pos) -- | Like 'add_' but returns a list of the files added.-add :: PlaylistName -> Path -> MPD [Path]-add plname x = add_ plname x >> listAll x---- | Add a song (or a whole directory) to a playlist.--- Adds to current if no playlist is specified.--- Will create a new playlist if the one specified does not already exist.-add_ :: PlaylistName -> Path -> MPD ()-add_ "" path = getResponse_ ("add" <$> path)-add_ plname path = getResponse_ ("playlistadd" <$> plname <++> path)---- | Clear a playlist. Clears current playlist if no playlist is specified.--- If the specified playlist does not exist, it will be created.-clear :: PlaylistName -> MPD ()-clear "" = getResponse_ "clear"-clear pl = getResponse_ ("playlistclear" <$> pl)---- | Remove a song from a playlist.--- If no playlist is specified, current playlist is used.--- Note that a playlist position ('Pos') is required when operating on--- playlists other than the current.-delete :: PlaylistName -> PLIndex -> MPD ()-delete "" (Pos x) = getResponse_ ("delete" <$> x)-delete "" (ID x) = getResponse_ ("deleteid" <$> x)-delete plname (Pos x) = getResponse_ ("playlistdelete" <$> plname <++> x)-delete _ _ = fail "'delete' within a playlist doesn't accept a playlist ID"---- | Load an existing playlist.-load :: PlaylistName -> MPD ()-load plname = getResponse_ ("load" <$> plname)---- | Move a song to a given position.--- Note that a playlist position ('Pos') is required when operating on--- playlists other than the current.-move :: PlaylistName -> PLIndex -> Integer -> MPD ()-move "" (Pos from) to = getResponse_ ("move" <$> from <++> to)-move "" (ID from) to = getResponse_ ("moveid" <$> from <++> to)-move plname (Pos from) to =- getResponse_ ("playlistmove" <$> plname <++> from <++> to)-move _ _ _ = fail "'move' within a playlist doesn't accept a playlist ID"---- | Delete existing playlist.-rm :: PlaylistName -> MPD ()-rm plname = getResponse_ ("rm" <$> plname)---- | Rename an existing playlist.-rename :: PlaylistName -- ^ Original playlist- -> PlaylistName -- ^ New playlist name- -> MPD ()-rename plname new = getResponse_ ("rename" <$> plname <++> new)---- | Save the current playlist.-save :: PlaylistName -> MPD ()-save plname = getResponse_ ("save" <$> plname)---- | Swap the positions of two songs.--- Note that the positions must be of the same type, i.e. mixing 'Pos' and 'ID'--- will result in a no-op.-swap :: PLIndex -> PLIndex -> MPD ()-swap (Pos x) (Pos y) = getResponse_ ("swap" <$> x <++> y)-swap (ID x) (ID y) = getResponse_ ("swapid" <$> x <++> y)-swap _ _ = fail "'swap' cannot mix position and ID arguments"+add :: MonadMPD m => Path -> m [Path]+add x = add_ x >> listAll x --- | Shuffle the playlist.-shuffle :: MPD ()-shuffle = getResponse_ "shuffle"+-- | Add a song (or a whole directory) to the current playlist.+add_ :: MonadMPD m => Path -> m ()+add_ path = getResponse_ ("add" <$> path) --- | Retrieve metadata for songs in the current playlist.-playlistInfo :: Maybe PLIndex -> MPD [Song]-playlistInfo x = getResponse cmd >>= takeSongs- where cmd = case x of- Just (Pos x') -> "playlistinfo" <$> x'- Just (ID x') -> "playlistid" <$> x'- Nothing -> "playlistinfo"+-- | Clear the current playlist.+clear :: MonadMPD m => m ()+clear = getResponse_ "clear" --- | Retrieve metadata for files in a given playlist.-listPlaylistInfo :: PlaylistName -> MPD [Song]-listPlaylistInfo plname =- takeSongs =<< getResponse ("listplaylistinfo" <$> plname)+-- | Remove a song from the current playlist.+delete :: MonadMPD m => PLIndex -> m ()+delete (Pos x) = getResponse_ ("delete" <$> x)+delete (ID x) = getResponse_ ("deleteid" <$> x) --- | Retrieve a list of files in a given playlist.-listPlaylist :: PlaylistName -> MPD [Path]-listPlaylist plname =- liftM takeValues $ getResponse ("listplaylist" <$> plname)+-- | Move a song to a given position in the current playlist.+move :: MonadMPD m => PLIndex -> Integer -> m ()+move (Pos from) to = getResponse_ ("move" <$> from <++> to)+move (ID from) to = getResponse_ ("moveid" <$> from <++> to) -- | Retrieve file paths and positions of songs in the current playlist. -- Note that this command is only included for completeness sake; it's -- deprecated and likely to disappear at any time, please use 'playlistInfo' -- instead.-playlist :: MPD [(PLIndex, Path)]+playlist :: MonadMPD m => m [(PLIndex, Path)] playlist = mapM f =<< getResponse "playlist" where f s | (pos, name) <- breakChar ':' s , Just pos' <- parseNum pos = return (Pos pos', name) | otherwise = throwError . Unexpected $ show s +-- | Search for songs in the current playlist with strict matching.+playlistFind :: MonadMPD m => Query -> m [Song]+playlistFind q = takeSongs =<< getResponse ("playlistfind" <$> q)++-- | Retrieve metadata for songs in the current playlist.+playlistInfo :: MonadMPD m => Maybe (Either PLIndex (Int, Int)) -> m [Song]+playlistInfo x = getResponse cmd >>= takeSongs+ where cmd = case x of+ Nothing -> "playlistinfo"+ Just (Left (Pos x')) -> "playlistinfo" <$> x'+ Just (Left (ID x')) -> "playlistid" <$> x'+ Just (Right range) -> "playlistinfo" <$> range++-- | Search case-insensitively with partial matches for songs in the+-- current playlist.+playlistSearch :: MonadMPD m => Query -> m [Song]+playlistSearch q = takeSongs =<< getResponse ("playlistsearch" <$> q)+ -- | Retrieve a list of changed songs currently in the playlist since -- a given playlist version.-plChanges :: Integer -> MPD [Song]+plChanges :: MonadMPD m => Integer -> m [Song] plChanges version = takeSongs =<< getResponse ("plchanges" <$> version) -- | Like 'plChanges' but only returns positions and ids.-plChangesPosId :: Integer -> MPD [(PLIndex, PLIndex)]+plChangesPosId :: MonadMPD m => Integer -> m [(PLIndex, PLIndex)] plChangesPosId plver = getResponse ("plchangesposid" <$> plver) >>=- mapM f . splitGroups [("cpos",id)] . toAssoc+ mapM f . splitGroups [("cpos",id)] . toAssocList where f xs | [("cpos", x), ("Id", y)] <- xs , Just (x', y') <- pair parseNum (x, y) = return (Pos x', ID y') | otherwise = throwError . Unexpected $ show xs --- | Search for songs in the current playlist with strict matching.-playlistFind :: Query -> MPD [Song]-playlistFind q = takeSongs =<< getResponse ("playlistfind" <$> q)+-- | Shuffle the playlist.+shuffle :: MonadMPD m => Maybe (Int, Int) -- ^ Optional range (start, end)+ -> m ()+shuffle range = getResponse_ ("shuffle" <$> range) --- | Search case-insensitively with partial matches for songs in the--- current playlist.-playlistSearch :: Query -> MPD [Song]-playlistSearch q = takeSongs =<< getResponse ("playlistsearch" <$> q)+-- | Swap the positions of two songs.+-- Note that the positions must be of the same type, i.e. mixing 'Pos' and 'ID'+-- will result in a no-op.+swap :: MonadMPD m => PLIndex -> PLIndex -> m ()+swap (Pos x) (Pos y) = getResponse_ ("swap" <$> x <++> y)+swap (ID x) (ID y) = getResponse_ ("swapid" <$> x <++> y)+swap _ _ = fail "'swap' cannot mix position and ID arguments" --- | Get the currently playing song.-currentSong :: MPD (Maybe Song)-currentSong = do- cs <- status- if stState cs == Stopped- then return Nothing- else getResponse1 "currentsong" >>=- fmap Just . runParser parseSong . toAssoc+--+-- Stored playlists+-- +-- | Retrieve a list of files in a given playlist.+listPlaylist :: MonadMPD m => PlaylistName -> m [Path]+listPlaylist plname =+ liftM takeValues $ getResponse ("listplaylist" <$> plname)++-- | Retrieve metadata for files in a given playlist.+listPlaylistInfo :: MonadMPD m => PlaylistName -> m [Song]+listPlaylistInfo plname =+ takeSongs =<< getResponse ("listplaylistinfo" <$> plname)++-- | Retreive a list of stored playlists.+listPlaylists :: MonadMPD m => m [PlaylistName]+listPlaylists = (go [] . toAssocList) `liftM` getResponse "listplaylists"+ where+ -- After each playlist name we get a timestamp+ go acc [] = acc+ go acc ((_, b):_:xs) = go (b : acc) xs+ go _ _ = error "listPlaylists: bug"++-- | Load an existing playlist.+load :: MonadMPD m => PlaylistName -> m ()+load plname = getResponse_ ("load" <$> plname)++-- | Like 'playlistAdd' but returns a list of the files added.+playlistAdd :: MonadMPD m => PlaylistName -> Path -> m [Path]+playlistAdd plname path = playlistAdd_ plname path >> listAll path++-- | Add a song (or a whole directory) to a stored playlist.+-- Will create a new playlist if the one specified does not already exist.+playlistAdd_ :: MonadMPD m => PlaylistName -> Path -> m ()+playlistAdd_ plname path = getResponse_ ("playlistadd" <$> plname <++> path)++-- | Clear a playlist. If the specified playlist does not exist, it will be+-- created.+playlistClear :: MonadMPD m => PlaylistName -> m ()+playlistClear = getResponse_ . ("playlistclear" <$>)++-- | Remove a song from a playlist.+playlistDelete :: MonadMPD m => PlaylistName+ -> Integer -- ^ Playlist position+ -> m ()+playlistDelete name pos = getResponse_ ("playlistdelete" <$> name <++> pos)++-- | Move a song to a given position in the playlist specified.+playlistMove :: MonadMPD m => PlaylistName -> Integer -> Integer -> m ()+playlistMove name from to =+ getResponse_ ("playlistmove" <$> name <++> from <++> to)++-- | Rename an existing playlist.+rename :: MonadMPD m+ => PlaylistName -- ^ Original playlist+ -> PlaylistName -- ^ New playlist name+ -> m ()+rename plname new = getResponse_ ("rename" <$> plname <++> new)++-- | Delete existing playlist.+rm :: MonadMPD m => PlaylistName -> m ()+rm plname = getResponse_ ("rm" <$> plname)++-- | Save the current playlist.+save :: MonadMPD m => PlaylistName -> m ()+save plname = getResponse_ ("save" <$> plname)+ ----- Playback commands+-- The music database -- --- | Set crossfading between songs.-crossfade :: Seconds -> MPD ()-crossfade secs = getResponse_ ("crossfade" <$> secs)+-- | Count the number of entries matching a query.+count :: MonadMPD m => Query -> m Count+count query = getResponse ("count" <$> query) >>= runParser parseCount --- | Begin\/continue playing.-play :: Maybe PLIndex -> MPD ()-play Nothing = getResponse_ "play"-play (Just (Pos x)) = getResponse_ ("play" <$> x)-play (Just (ID x)) = getResponse_ ("playid" <$> x)+-- | Search the database for entries exactly matching a query.+find :: MonadMPD m => Query -> m [Song]+find query = getResponse ("find" <$> query) >>= takeSongs --- | Pause playing.-pause :: Bool -> MPD ()-pause = getResponse_ . ("pause" <$>)+-- | Adds songs matching a query to the current playlist.+findAdd :: MonadMPD m => Query -> m ()+findAdd q = getResponse_ ("findadd" <$> q) --- | Stop playing.-stop :: MPD ()-stop = getResponse_ "stop"+-- | List all metadata of metadata (sic).+list :: MonadMPD m+ => Meta -- ^ Metadata to list+ -> Query -> m [String]+list mtype query = liftM takeValues $ getResponse ("list" <$> mtype <++> query) --- | Play the next song.-next :: MPD ()-next = getResponse_ "next"+-- | List the songs (without metadata) in a database directory recursively.+listAll :: MonadMPD m => Path -> m [Path]+listAll path = liftM (map snd . filter ((== "file") . fst) . toAssocList)+ (getResponse $ "listall" <$> path) --- | Play the previous song.-previous :: MPD ()-previous = getResponse_ "previous"+-- Helper for lsInfo and listAllInfo.+lsInfo' :: MonadMPD m => String -> Path -> m [Either Path Song]+lsInfo' cmd path =+ liftM (extractEntries (Just . Right, const Nothing, Just . Left)) $+ takeEntries =<< getResponse (cmd <$> path) --- | Seek to some point in a song.--- Seeks in current song if no position is given.-seek :: Maybe PLIndex -> Seconds -> MPD ()-seek (Just (Pos x)) time = getResponse_ ("seek" <$> x <++> time)-seek (Just (ID x)) time = getResponse_ ("seekid" <$> x <++> time)-seek Nothing time = do- st <- status- unless (stState st == Stopped) (seek (stSongID st) time)+-- | Recursive 'lsInfo'.+listAllInfo :: MonadMPD m => Path -> m [Either Path Song]+listAllInfo = lsInfo' "listallinfo" --- | Set random playing.-random :: Bool -> MPD ()-random = getResponse_ . ("random" <$>)+-- | Non-recursively list the contents of a database directory.+lsInfo :: MonadMPD m => Path -> m [Either Path Song]+lsInfo = lsInfo' "lsinfo" --- | Set repeating.-repeat :: Bool -> MPD ()-repeat = getResponse_ . ("repeat" <$>)+-- | Search the database using case insensitive matching.+search :: MonadMPD m => Query -> m [Song]+search query = getResponse ("search" <$> query) >>= takeSongs --- | Set the volume (0-100 percent).-setVolume :: Int -> MPD ()-setVolume = getResponse_ . ("setvol" <$>)+-- | Update the server's database.+-- If no paths are given, all paths will be scanned.+-- Unreadable or non-existent paths are silently ignored.+update :: MonadMPD m => [Path] -> m ()+update [] = getResponse_ "update"+update [x] = getResponse_ ("update" <$> x)+update xs = getResponses (map ("update" <$>) xs) >> return () --- | Increase or decrease volume by a given percent, e.g.--- 'volume 10' will increase the volume by 10 percent, while--- 'volume (-10)' will decrease it by the same amount.--- Note that this command is only included for completeness sake ; it's--- deprecated and may disappear at any time, please use 'setVolume' instead.-volume :: Int -> MPD ()-volume = getResponse_ . ("volume" <$>)+-- | Like 'update' but also rescans unmodified files.+rescan :: MonadMPD m => [Path] -> m ()+rescan [] = getResponse_ "rescan"+rescan [x] = getResponse_ ("rescan" <$> x)+rescan xs = getResponses (map ("rescan" <$>) xs) >> return () ----- Miscellaneous commands+-- Stickers -- --- | Clear the current error message in status.-clearError :: MPD ()-clearError = getResponse_ "clearerror"+-- | Reads a sticker value for the specified object.+stickerGet :: MonadMPD m => ObjectType+ -> String -- ^ Object URI+ -> String -- ^ Sticker name+ -> m [String]+stickerGet typ uri name = takeValues `liftM` getResponse ("sticker get" <$> typ <++> uri <++> name) --- | Retrieve a list of available commands.-commands :: MPD [String]-commands = liftM takeValues (getResponse "commands")+-- | Adds a sticker value to the specified object.+stickerSet :: MonadMPD m => ObjectType+ -> String -- ^ Object URI+ -> String -- ^ Sticker name+ -> String -- ^ Sticker value+ -> m ()+stickerSet typ uri name value =+ getResponse_ ("sticker set" <$> typ <++> uri <++> name <++> value) --- | Retrieve a list of unavailable (due to access restrictions) commands.-notCommands :: MPD [String]-notCommands = liftM takeValues (getResponse "notcommands")+-- | Delete a sticker value from the specified object.+stickerDelete :: MonadMPD m => ObjectType+ -> String -- ^ Object URI+ -> String -- ^ Sticker name+ -> m ()+stickerDelete typ uri name =+ getResponse_ ("sticker delete" <$> typ <++> uri <++> name) --- | Retrieve a list of available song metadata.-tagTypes :: MPD [String]-tagTypes = liftM takeValues (getResponse "tagtypes")+-- | Lists the stickers for the specified object.+stickerList :: MonadMPD m => ObjectType+ -> String -- ^ Object URI+ -> m [(String, String)] -- ^ Sticker name\/sticker value+stickerList typ uri =+ toAssocList `liftM` getResponse ("sticker list" <$> typ <++> uri) --- | Retrieve a list of supported urlhandlers.-urlHandlers :: MPD [String]-urlHandlers = liftM takeValues (getResponse "urlhandlers")+-- | Searches the sticker database for stickers with the specified name, below+-- the specified path.+stickerFind :: MonadMPD m => ObjectType+ -> String -- ^ Path+ -> String -- ^ Sticker name+ -> m [(String, String)] -- ^ URI\/sticker value+stickerFind typ uri name =+ toAssocList `liftM`+ getResponse ("sticker find" <$> typ <++> uri <++> name) --- XXX should the password be quoted? Change "++" to "<$>" if so.+--+-- Connection+--++-- XXX should the password be quoted? Change "++" to "<$>" if so. If+-- it should, it also needs to be fixed in N.M.Core. -- | Send password to server to authenticate session. -- Password is sent as plain text.-password :: String -> MPD ()+password :: MonadMPD m => String -> m () password = getResponse_ . ("password " ++) -- | Check that the server is still responding.-ping :: MPD ()+ping :: MonadMPD m => m () ping = getResponse_ "ping" --- | Get server statistics.-stats :: MPD Stats-stats = getResponse "stats" >>= runParser parseStats+--+-- Audio output devices+-- --- | Get the server's status.-status :: MPD Status-status = getResponse "status" >>= runParser parseStatus+-- | Turn off an output device.+disableOutput :: MonadMPD m => Int -> m ()+disableOutput = getResponse_ . ("disableoutput" <$>) +-- | Turn on an output device.+enableOutput :: MonadMPD m => Int -> m ()+enableOutput = getResponse_ . ("enableoutput" <$>)++-- | Retrieve information for all output devices.+outputs :: MonadMPD m => m [Device]+outputs = getResponse "outputs" >>= runParser parseOutputs+ --+-- Reflection+--++-- | Retrieve a list of available commands.+commands :: MonadMPD m => m [String]+commands = liftM takeValues (getResponse "commands")++-- | Retrieve a list of unavailable (due to access restrictions) commands.+notCommands :: MonadMPD m => m [String]+notCommands = liftM takeValues (getResponse "notcommands")++-- | Retrieve a list of available song metadata.+tagTypes :: MonadMPD m => m [String]+tagTypes = liftM takeValues (getResponse "tagtypes")++-- | Retrieve a list of supported urlhandlers.+urlHandlers :: MonadMPD m => m [String]+urlHandlers = liftM takeValues (getResponse "urlhandlers")++-- | Retreive a list of decoder plugins with associated suffix and mime types.+decoders :: MonadMPD m => m [(String, [(String, String)])]+decoders = (takeDecoders . toAssocList) `liftM` getResponse "decoders"+ where+ takeDecoders [] = []+ takeDecoders ((_, p):xs) =+ let (info, rest) = break ((==) "plugin" . fst) xs+ in (p, info) : takeDecoders rest++-- -- Extensions\/shortcuts. -- -- | Like 'update', but returns the update job id.-updateId :: [Path] -> MPD Integer+updateId :: MonadMPD m => [Path] -> m Integer updateId paths = liftM (read . head . takeValues) cmd where cmd = case paths of [] -> getResponse "update"@@ -477,15 +519,16 @@ xs -> getResponses $ map ("update" <$>) xs -- | Toggles play\/pause. Plays if stopped.-toggle :: MPD ()+toggle :: MonadMPD m => m () toggle = status >>= \st -> case stState st of Playing -> pause True _ -> play Nothing -- | Add a list of songs\/folders to a playlist. -- Should be more efficient than running 'add' many times.-addMany :: PlaylistName -> [Path] -> MPD ()+addMany :: MonadMPD m => PlaylistName -> [Path] -> m () addMany _ [] = return ()-addMany plname [x] = add_ plname x+addMany "" [x] = add_ x+addMany plname [x] = playlistAdd_ plname x addMany plname xs = getResponses (map cmd xs) >> return () where cmd x = case plname of "" -> "add" <$> x@@ -494,9 +537,9 @@ -- | Delete a list of songs from a playlist. -- If there is a duplicate then no further songs will be deleted, so -- take care to avoid them (see 'prune' for this).-deleteMany :: PlaylistName -> [PLIndex] -> MPD ()+deleteMany :: MonadMPD m => PlaylistName -> [PLIndex] -> m () deleteMany _ [] = return ()-deleteMany plname [x] = delete plname x+deleteMany plname [(Pos x)] = playlistDelete plname x deleteMany "" xs = getResponses (map cmd xs) >> return () where cmd (Pos x) = "delete" <$> x cmd (ID x) = "deleteid" <$> x@@ -506,7 +549,7 @@ -- | Returns all songs and directories that match the given partial -- path name.-complete :: String -> MPD [Either Path Song]+complete :: MonadMPD m => String -> m [Either Path Song] complete path = do xs <- liftM matches . lsInfo $ dropFileName path case xs of@@ -518,9 +561,11 @@ -- | Crop playlist. -- The bounds are inclusive.--- If 'Nothing' or 'ID' is passed the cropping will leave your playlist alone+-- If 'Nothing' is passed the cropping will leave your playlist alone -- on that side.-crop :: Maybe PLIndex -> Maybe PLIndex -> MPD ()+-- Using 'ID' will automatically find the absolute playlist position and use+-- that as the cropping boundary.+crop :: MonadMPD m => Maybe PLIndex -> Maybe PLIndex -> m () crop x y = do pl <- playlistInfo Nothing let x' = case x of Just (Pos p) -> fromInteger p@@ -535,11 +580,11 @@ where findByID i = findIndex ((==) i . (\(ID j) -> j) . fromJust . sgIndex) -- | Remove duplicate playlist entries.-prune :: MPD ()+prune :: MonadMPD m => m () prune = findDuplicates >>= deleteMany "" -- Find duplicate playlist entries.-findDuplicates :: MPD [PLIndex]+findDuplicates :: MonadMPD m => m [PLIndex] findDuplicates = liftM (map ((\(ID x) -> ID x) . fromJust . sgIndex) . flip dups ([],[])) $ playlistInfo Nothing@@ -550,105 +595,89 @@ mSong x = let m = sgFilePath x in any ((==) m . sgFilePath) -- | List directories non-recursively.-lsDirs :: Path -> MPD [Path]+lsDirs :: MonadMPD m => Path -> m [Path] lsDirs path = liftM (extractEntries (const Nothing,const Nothing, Just)) $ takeEntries =<< getResponse ("lsinfo" <$> path) -- | List files non-recursively.-lsFiles :: Path -> MPD [Path]+lsFiles :: MonadMPD m => Path -> m [Path] lsFiles path = liftM (extractEntries (Just . sgFilePath, const Nothing, const Nothing)) $ takeEntries =<< getResponse ("lsinfo" <$> path) -- | List all playlists.-lsPlaylists :: MPD [PlaylistName]+lsPlaylists :: MonadMPD m => m [PlaylistName] lsPlaylists = liftM (extractEntries (const Nothing, Just, const Nothing)) $ takeEntries =<< getResponse "lsinfo" --- | Search the database for songs relating to an artist.-findArtist :: Artist -> MPD [Song]-findArtist x = find [Match Artist x]---- | Search the database for songs relating to an album.-findAlbum :: Album -> MPD [Song]-findAlbum x = find [Match Album x]---- | Search the database for songs relating to a song title.-findTitle :: Title -> MPD [Song]-findTitle x = find [Match Title x]- -- | List the artists in the database.-listArtists :: MPD [Artist]+listArtists :: MonadMPD m => m [Artist] listArtists = liftM takeValues (getResponse "list artist") -- | List the albums in the database, optionally matching a given -- artist.-listAlbums :: Maybe Artist -> MPD [Album]+listAlbums :: MonadMPD m => Maybe Artist -> m [Album] listAlbums artist = liftM takeValues $ getResponse ("list album" <$> fmap ("artist" <++>) artist) -- | List the songs in an album of some artist.-listAlbum :: Artist -> Album -> MPD [Song]-listAlbum artist album = find [Match Artist artist, Match Album album]---- | Search the database for songs relating to an artist using 'search'.-searchArtist :: Artist -> MPD [Song]-searchArtist x = search [Match Artist x]---- | Search the database for songs relating to an album using 'search'.-searchAlbum :: Album -> MPD [Song]-searchAlbum x = search [Match Album x]---- | Search the database for songs relating to a song title.-searchTitle :: Title -> MPD [Song]-searchTitle x = search [Match Title x]+listAlbum :: MonadMPD m => Artist -> Album -> m [Song]+listAlbum artist album = find (Artist =? artist <&> Album =? album) -- | Retrieve the current playlist. -- Equivalent to @playlistinfo Nothing@.-getPlaylist :: MPD [Song]+getPlaylist :: MonadMPD m => m [Song] getPlaylist = playlistInfo Nothing +-- | Increase or decrease volume by a given percent, e.g.+-- 'volume 10' will increase the volume by 10 percent, while+-- 'volume (-10)' will decrease it by the same amount.+volume :: MonadMPD m => Int -> m ()+volume n = do+ current <- (fromIntegral . stVolume) `liftM` status+ setVolume . round $ (fromIntegral n / 100) * current + current+ -- -- Miscellaneous functions. -- -- Run getResponse but discard the response.-getResponse_ :: String -> MPD ()+getResponse_ :: MonadMPD m => String -> m () getResponse_ x = getResponse x >> return () -- Get the lines of the daemon's response to a list of commands.-getResponses :: [String] -> MPD [String]+getResponses :: MonadMPD m => [String] -> m [String] getResponses cmds = getResponse . concat $ intersperse "\n" cmds' where cmds' = "command_list_begin" : cmds ++ ["command_list_end"] -- Helper that throws unexpected error if input is empty.-failOnEmpty :: [String] -> MPD [String]+failOnEmpty :: MonadMPD m => [String] -> m [String] failOnEmpty [] = throwError $ Unexpected "Non-empty response expected." failOnEmpty xs = return xs -- A wrapper for getResponse that fails on non-empty responses.-getResponse1 :: String -> MPD [String]+getResponse1 :: MonadMPD m => String -> m [String] getResponse1 x = getResponse x >>= failOnEmpty -- -- Parsing. -- --- Run 'toAssoc' and return only the values.+-- Run 'toAssocList' and return only the values. takeValues :: [String] -> [String]-takeValues = snd . unzip . toAssoc+takeValues = snd . unzip . toAssocList data EntryType = SongEntry Song | PLEntry String | DirEntry String- deriving Show+ deriving (Eq, Show) -- Separate the result of an lsinfo\/listallinfo call into directories, -- playlists, and songs.-takeEntries :: [String] -> MPD [EntryType]-takeEntries = mapM toEntry . splitGroups wrappers . toAssoc . reverse+takeEntries :: MonadMPD m => [String] -> m [EntryType]+takeEntries = mapM toEntry . splitGroups wrappers . toAssocList where toEntry xs@(("file",_):_) = liftM SongEntry $ runParser parseSong xs toEntry (("directory",d):_) = return $ DirEntry d@@ -659,12 +688,14 @@ -- Extract a subset of songs, directories, and playlists. extractEntries :: (Song -> Maybe a, String -> Maybe a, String -> Maybe a) -> [EntryType] -> [a]-extractEntries (fSong,fPlayList,fDir) = catMaybes . map f+extractEntries (fSong,fPlayList,fDir) = mapMaybe f where f (SongEntry s) = fSong s f (PLEntry pl) = fPlayList pl f (DirEntry d) = fDir d -- Build a list of song instances from a response.-takeSongs :: [String] -> MPD [Song]-takeSongs = mapM (runParser parseSong) . splitGroups [("file",id)] . toAssoc+takeSongs :: MonadMPD m => [String] -> m [Song]+takeSongs = mapM (runParser parseSong)+ . splitGroups [("file",id)]+ . toAssocList
+ Network/MPD/Commands/Arg.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE TypeSynonymInstances #-}++-- | Module : Network.MPD.Commands.Arg+-- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010+-- License : LGPL (see LICENSE)+-- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>+-- Stability : alpha+--+-- Prepare command arguments.++module Network.MPD.Commands.Arg (Args(..), MPDArg(..), (<++>), (<$>)) where++import Network.MPD.Utils (showBool)++-- | Arguments for getResponse are accumulated as strings in values of+-- this type after being converted from whatever type (an instance of+-- MPDArg) they were to begin with.+newtype Args = Args [String]+ deriving Show++-- | A uniform interface for argument preparation+-- The basic idea is that one should be able+-- to magically prepare an argument for use with+-- an MPD command, without necessarily knowing/\caring+-- how it needs to be represented internally.+class Show a => MPDArg a where+ prep :: a -> Args+ -- Note that because of this, we almost+ -- never have to actually provide+ -- an implementation of 'prep'+ prep = Args . return . show++-- | Groups together arguments to getResponse.+infixl 3 <++>+(<++>) :: (MPDArg a, MPDArg b) => a -> b -> Args+x <++> y = Args $ xs ++ ys+ where Args xs = prep x+ Args ys = prep y++-- | Converts a command name and a string of arguments into the string+-- to hand to getResponse.+infix 2 <$>+(<$>) :: (MPDArg a) => String -> a -> String+x <$> y = x ++ " " ++ unwords (filter (not . null) y')+ where Args y' = prep y++instance MPDArg Args where prep = id++instance MPDArg String where+ -- We do this to avoid mangling+ -- non-ascii characters with 'show'+ prep x = Args ['"' : x ++ "\""]++instance (MPDArg a) => MPDArg (Maybe a) where+ prep Nothing = Args []+ prep (Just x) = prep x++instance (MPDArg a, MPDArg b) => MPDArg (a, b) where+ prep (x, y) = Args [show x ++ ":" ++ show y]++instance MPDArg Int+instance MPDArg Integer+instance MPDArg Bool where prep = Args . return . showBool
+ Network/MPD/Commands/Parse.hs view
@@ -0,0 +1,171 @@+{-# LANGUAGE FlexibleContexts #-}++-- | Module : Network.MPD.Commands.Parse+-- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010+-- License : LGPL (see LICENSE)+-- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>+-- Stability : alpha+--+-- Parsers for MPD data types.++module Network.MPD.Commands.Parse where++import Network.MPD.Commands.Types++import Control.Monad.Error+import Network.MPD.Utils+import Network.MPD.Core (MonadMPD, MPDError(Unexpected))++-- | Builds a 'Count' instance from an assoc. list.+parseCount :: [String] -> Either String Count+parseCount = foldM f defaultCount . toAssocList+ where f :: Count -> (String, String) -> Either String Count+ f a ("songs", x) = return $ parse parseNum+ (\x' -> a { cSongs = x'}) a x+ f a ("playtime", x) = return $ parse parseNum+ (\x' -> a { cPlaytime = x' }) a x+ f _ x = Left $ show x++-- | Builds a list of 'Device' instances from an assoc. list+parseOutputs :: [String] -> Either String [Device]+parseOutputs = mapM (foldM f defaultDevice)+ . splitGroups [("outputid",id)]+ . toAssocList+ where f a ("outputid", x) = return $ parse parseNum+ (\x' -> a { dOutputID = x' }) a x+ f a ("outputname", x) = return a { dOutputName = x }+ f a ("outputenabled", x) = return $ parse parseBool+ (\x' -> a { dOutputEnabled = x'}) a x+ f _ x = fail $ show x++-- | Builds a 'Stats' instance from an assoc. list.+parseStats :: [String] -> Either String Stats+parseStats = foldM f defaultStats . toAssocList+ where+ f a ("artists", x) = return $ parse parseNum+ (\x' -> a { stsArtists = x' }) a x+ f a ("albums", x) = return $ parse parseNum+ (\x' -> a { stsAlbums = x' }) a x+ f a ("songs", x) = return $ parse parseNum+ (\x' -> a { stsSongs = x' }) a x+ f a ("uptime", x) = return $ parse parseNum+ (\x' -> a { stsUptime = x' }) a x+ f a ("playtime", x) = return $ parse parseNum+ (\x' -> a { stsPlaytime = x' }) a x+ f a ("db_playtime", x) = return $ parse parseNum+ (\x' -> a { stsDbPlaytime = x' }) a x+ f a ("db_update", x) = return $ parse parseNum+ (\x' -> a { stsDbUpdate = x' }) a x+ f _ x = fail $ show x++-- | Builds a 'Song' instance from an assoc. list.+parseSong :: [(String, String)] -> Either String Song+parseSong xs = foldM f defaultSong xs+ where f a ("Artist", x) = return a { sgArtist = x }+ f a ("Album", x) = return a { sgAlbum = x }+ f a ("Title", x) = return a { sgTitle = x }+ f a ("Genre", x) = return a { sgGenre = x }+ f a ("Name", x) = return a { sgName = x }+ f a ("Composer", x) = return a { sgComposer = x }+ f a ("Performer", x) = return a { sgPerformer = x }+ f a ("Date", x) = return $ parse parseDate+ (\x' -> a { sgDate = x' }) a x+ f a ("Track", x) = return $ parse parseTuple+ (\x' -> a { sgTrack = x'}) a x+ f a ("Disc", x) = return a { sgDisc = parseTuple x }+ f a ("file", x) = return a { sgFilePath = x }+ f a ("Time", x) = return $ parse parseNum+ (\x' -> a { sgLength = x'}) a x+ f a ("Id", x) = return $ parse parseNum+ (\x' -> a { sgIndex = Just (ID x') }) a x+ -- We prefer Id but take Pos if no Id has been found.+ f a ("Pos", x) =+ maybe (return $ parse parseNum+ (\x' -> a { sgIndex = Just (Pos x') }) a x)+ (const $ return a)+ (sgIndex a)+ -- Collect auxiliary keys+ f a (k, v) = return a { sgAux = (k, v) : sgAux a }++ parseTuple s = let (x, y) = breakChar '/' s in+ -- Handle incomplete values. For example, songs might+ -- have a track number, without specifying the total+ -- number of tracks, in which case the resulting+ -- tuple will have two identical parts.+ case (parseNum x, parseNum y) of+ (Just x', Nothing) -> Just (x', x')+ (Just x', Just y') -> Just (x', y')+ _ -> Nothing++-- | Builds a 'Status' instance from an assoc. list.+parseStatus :: [String] -> Either String Status+parseStatus = foldM f defaultStatus . toAssocList+ where f a ("state", x)+ = return $ parse state (\x' -> a { stState = x' }) a x+ f a ("volume", x)+ = return $ parse parseNum (\x' -> a { stVolume = x' }) a x+ f a ("repeat", x)+ = return $ parse parseBool (\x' -> a { stRepeat = x' }) a x+ f a ("random", x)+ = return $ parse parseBool (\x' -> a { stRandom = x' }) a x+ f a ("playlist", x)+ = return $ parse parseNum (\x' -> a { stPlaylistVersion = x' }) a x+ f a ("playlistlength", x)+ = return $ parse parseNum (\x' -> a { stPlaylistLength = x' }) a x+ f a ("xfade", x)+ = return $ parse parseNum (\x' -> a { stXFadeWidth = x' }) a x+ f a ("song", x)+ = return $ parse parseNum (\x' -> a { stSongPos = Just (Pos x') }) a x+ f a ("songid", x)+ = return $ parse parseNum (\x' -> a { stSongID = Just (ID x') }) a x+ f a ("time", x)+ = return $ parse time (\x' -> a { stTime = x' }) a x+ f a ("bitrate", x)+ = return $ parse parseNum (\x' -> a { stBitrate = x' }) a x+ f a ("audio", x)+ = return $ parse audio (\x' -> a { stAudio = x' }) a x+ f a ("updating_db", x)+ = return $ parse parseNum (\x' -> a { stUpdatingDb = x' }) a x+ f a ("error", x)+ = return a { stError = x }+ f a ("single", x)+ = return $ parse parseBool (\x' -> a { stSingle = x' }) a x+ f a ("consume", x)+ = return $ parse parseBool (\x' -> a { stConsume = x' }) a x+ f a ("nextsong", x)+ = return $ parse parseNum (\x' -> a { stNextSongPos = Just (Pos x') }) a x+ f a ("nextsongid", x)+ = return $ parse parseNum (\x' -> a { stNextSongID = Just (ID x') }) a x+ f _ x+ = fail $ show x++ state "play" = Just Playing+ state "pause" = Just Paused+ state "stop" = Just Stopped+ state _ = Nothing++ time = pair parseNum . breakChar ':'++ audio s = let (u, u') = breakChar ':' s+ (v, w) = breakChar ':' u' in+ case (parseNum u, parseNum v, parseNum w) of+ (Just a, Just b, Just c) -> Just (a, b, c)+ _ -> Nothing++-- | Run a parser and lift the result into the 'MPD' monad+runParser :: (MonadMPD m, MonadError MPDError m)+ => (input -> Either String a) -> input -> m a+runParser f = either (throwError . Unexpected) return . f++-- | A helper that runs a parser on a string and, depending on the+-- outcome, either returns the result of some command applied to the+-- result, or a default value. Used when building structures.+parse :: (String -> Maybe a) -> (a -> b) -> b -> String -> b+parse parser f x = maybe x f . parser++-- | A helper for running a parser returning Maybe on a pair of strings.+-- Returns Just if both strings where parsed successfully, Nothing otherwise.+pair :: (String -> Maybe a) -> (String, String) -> Maybe (a, a)+pair p (x, y) = case (p x, p y) of+ (Just a, Just b) -> Just (a, b)+ _ -> Nothing
+ Network/MPD/Commands/Query.hs view
@@ -0,0 +1,56 @@+-- | Module : Network.MPD.Commands.Query+-- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010+-- License : LGPL (see LICENSE)+-- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>+-- Stability : alpha+--+-- Query interface.++module Network.MPD.Commands.Query (Query, (=?), (<&>), anything) where++import Network.MPD.Commands.Arg+import Network.MPD.Commands.Types++import Data.Monoid++-- | An interface for creating MPD queries.+--+-- For example, to match any song where the value of artist is \"Foo\", we+-- use:+--+-- > Artist =? "Foo"+--+-- We can also compose queries, thus narrowing the search. For example, to+-- match any song where the value of artist is \"Foo\" and the value of album+-- is \"Bar\", we use:+--+-- > Artist =? "Foo" <&> Album =? "Bar"+newtype Query = Query [Match] deriving Show++-- A single query clause, comprising a metadata key and a desired value.+data Match = Match Meta String++instance Show Match where+ show (Match meta query) = show meta ++ " \"" ++ query ++ "\""+ showList xs _ = unwords $ map show xs++instance Monoid Query where+ mempty = Query []+ Query a `mappend` Query b = Query (a ++ b)++instance MPDArg Query where+ prep = foldl (<++>) (Args []) . f+ where f (Query ms) = map (\(Match m q) -> Args [show m] <++> q) ms++-- | An empty query. Matches anything.+anything :: Query+anything = mempty++-- | Create a query.+(=?) :: Meta -> String -> Query+m =? s = Query [Match m s]++-- | Combine queries.+infixr 6 <&>+(<&>) :: Query -> Query -> Query+(<&>) = mappend
+ Network/MPD/Commands/Types.hs view
@@ -0,0 +1,194 @@+-- | Module : Network.MPD.Commands.Types+-- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010+-- License : LGPL (see LICENSE)+-- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>+-- Stability : alpha+--+-- Various MPD data structures and types++module Network.MPD.Commands.Types where++import Network.MPD.Commands.Arg (MPDArg(prep), Args(Args))++type Artist = String+type Album = String+type Title = String++-- | Used for commands which require a playlist name.+-- If empty, the current playlist is used.+type PlaylistName = String++-- | Used for commands which require a path within the database.+-- If empty, the root path is used.+type Path = String++-- | Available metadata types\/scope modifiers, used for searching the+-- database for entries with certain metadata values.+data Meta = Artist | Album | Title | Track | Name | Genre | Date+ | Composer | Performer | Disc | Any | Filename+ deriving Show++instance MPDArg Meta++-- | Object types.+data ObjectType = SongObj+ deriving (Eq, Show)++instance MPDArg ObjectType where+ prep SongObj = Args ["song"]++type Seconds = Integer++-- | Represents a song's playlist index.+data PLIndex = Pos Integer -- ^ A playlist position index (starting from 0)+ | ID Integer -- ^ A playlist ID number that more robustly+ -- identifies a song.+ deriving (Show, Eq)++-- | Represents the different playback states.+data State = Playing+ | Stopped+ | Paused+ deriving (Show, Eq)++-- | Represents the various MPD subsystems.+data Subsystem+ = Database -- ^ The song database+ | Update -- ^ Database updates+ | StoredPlaylist -- ^ Stored playlists+ | Playlist -- ^ The current playlist+ | Player -- ^ The player+ | Mixer -- ^ The volume mixer+ | Output -- ^ Audio outputs+ | Options -- ^ Playback options+ deriving (Eq, Show)++instance MPDArg Subsystem where+ prep Database = Args ["database"]+ prep Update = Args ["update"]+ prep StoredPlaylist = Args ["stored_playlist"]+ prep Playlist = Args ["playlist"]+ prep Player = Args ["player"]+ prep Mixer = Args ["mixer"]+ prep Output = Args ["output"]+ prep Options = Args ["options"]++data ReplayGainMode+ = Off -- ^ Disable replay gain+ | TrackMode -- ^ Per track mode+ | AlbumMode -- ^ Per album mode+ deriving (Eq, Show)++instance MPDArg ReplayGainMode where+ prep Off = Args ["off"]+ prep TrackMode = Args ["track"]+ prep AlbumMode = Args ["album"]++-- | Represents the result of running 'count'.+data Count =+ Count { cSongs :: Integer -- ^ Number of songs matching the query+ , cPlaytime :: Seconds -- ^ Total play time of matching songs+ }+ deriving (Eq, Show)++defaultCount :: Count+defaultCount = Count { cSongs = 0, cPlaytime = 0 }++-- | Represents an output device.+data Device =+ Device { dOutputID :: Int -- ^ Output's ID number+ , dOutputName :: String -- ^ Output's name as defined in the MPD+ -- configuration file+ , dOutputEnabled :: Bool }+ deriving (Eq, Show)++defaultDevice :: Device+defaultDevice =+ Device { dOutputID = 0, dOutputName = "", dOutputEnabled = False }++-- | Container for database statistics.+data Stats =+ Stats { stsArtists :: Integer -- ^ Number of artists.+ , stsAlbums :: Integer -- ^ Number of albums.+ , stsSongs :: Integer -- ^ Number of songs.+ , stsUptime :: Seconds -- ^ Daemon uptime in seconds.+ , stsPlaytime :: Seconds -- ^ Total playing time.+ , stsDbPlaytime :: Seconds -- ^ Total play time of all the songs in+ -- the database.+ , stsDbUpdate :: Integer -- ^ Last database update in UNIX time.+ }+ deriving (Eq, Show)++defaultStats :: Stats+defaultStats =+ Stats { stsArtists = 0, stsAlbums = 0, stsSongs = 0, stsUptime = 0+ , stsPlaytime = 0, stsDbPlaytime = 0, stsDbUpdate = 0 }++-- | Represents a single song item.+data Song =+ Song { sgArtist, sgAlbum, sgTitle, sgFilePath, sgGenre, sgName, sgComposer+ , sgPerformer :: String+ , sgLength :: Seconds -- ^ Length in seconds+ , sgDate :: Int -- ^ Year+ , sgTrack :: (Int, Int) -- ^ Track number\/total tracks+ , sgDisc :: Maybe (Int, Int) -- ^ Position in set\/total in set+ , sgIndex :: Maybe PLIndex+ , sgAux :: [(String, String)] } -- ^ Auxiliary song fields+ deriving (Eq, Show)++defaultSong :: Song+defaultSong =+ Song { sgArtist = "", sgAlbum = "", sgTitle = ""+ , sgGenre = "", sgName = "", sgComposer = ""+ , sgPerformer = "", sgDate = 0, sgTrack = (0,0)+ , sgDisc = Nothing, sgFilePath = "", sgLength = 0+ , sgIndex = Nothing, sgAux = [] }++-- | Container for MPD status.+data Status =+ Status { stState :: State+ -- | A percentage (0-100)+ , stVolume :: Int+ , stRepeat :: Bool+ , stRandom :: Bool+ -- | A value that is incremented by the server every time the+ -- playlist changes.+ , stPlaylistVersion :: Integer+ -- | The number of items in the current playlist.+ , stPlaylistLength :: Integer+ -- | Current song's position in the playlist.+ , stSongPos :: Maybe PLIndex+ -- | Current song's playlist ID.+ , stSongID :: Maybe PLIndex+ -- | Next song's position in the playlist.+ , stNextSongPos :: Maybe PLIndex+ -- | Next song's playlist ID.+ , stNextSongID :: Maybe PLIndex+ -- | Time elapsed\/total time.+ , stTime :: (Seconds, Seconds)+ -- | Bitrate (in kilobytes per second) of playing song (if any).+ , stBitrate :: Int+ -- | Crossfade time.+ , stXFadeWidth :: Seconds+ -- | Samplerate\/bits\/channels for the chosen output device+ -- (see mpd.conf).+ , stAudio :: (Int, Int, Int)+ -- | Job ID of currently running update (if any).+ , stUpdatingDb :: Integer+ -- | If True, MPD will play only one song and stop after finishing it.+ , stSingle :: Bool+ -- | If True, a song will be removed after it has been played.+ , stConsume :: Bool+ -- | Last error message (if any).+ , stError :: String }+ deriving (Eq, Show)++defaultStatus :: Status+defaultStatus =+ Status { stState = Stopped, stVolume = 0, stRepeat = False+ , stRandom = False, stPlaylistVersion = 0, stPlaylistLength = 0+ , stSongPos = Nothing, stSongID = Nothing, stTime = (0,0)+ , stNextSongPos = Nothing, stNextSongID = Nothing+ , stBitrate = 0, stXFadeWidth = 0, stAudio = (0,0,0)+ , stUpdatingDb = 0, stSingle = False, stConsume = False+ , stError = "" }
Network/MPD/Core.hs view
@@ -1,187 +1,200 @@-{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE FlexibleContexts, GeneralizedNewtypeDeriving #-} -- | Module : Network.MPD.Core--- Copyright : (c) Ben Sinclair 2005-2008+-- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010 -- License : LGPL (see LICENSE)--- Maintainer : bsinclai@turing.une.edu.au+-- Maintainer : Joachim Fasting <joachim.fasting@gmail.com> -- Stability : alpha--- Portability : not Haskell 98 (uses MultiParamTypeClasses) ----- Core functionality.+-- The core datatypes and operations are defined here, including the+-- primary instance of the 'MonadMPD' class, 'MPD'. module Network.MPD.Core (+ -- * Classes+ MonadMPD(..), -- * Data types- MPD(..), Conn(..), MPDError(..), ACKType(..), Response,+ MPD, MPDError(..), ACKType(..), Response, Host, Port, Password,+ -- * Running+ withMPDEx, -- * Interacting- getResponse, close, reconnect, kill,+ getResponse, kill, ) where -import Control.Monad (liftM)-import Control.Monad.Error (Error(..), MonadError(..))-import Control.Monad.Trans-import Prelude hiding (repeat)+import Network.MPD.Core.Class+import Network.MPD.Core.Error++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(..), put, get, evalStateT)+import qualified Data.Foldable as F import Data.List (isPrefixOf)-import Data.Maybe-import System.IO+import Network (PortID(..), withSocketsDo, connectTo)+import System.IO (Handle, hPutStrLn, hReady, hClose, hFlush)+import System.IO.Error (isEOFError)+import qualified System.IO.UTF8 as U -- -- Data types. -- --- A class of transports with which to connect to MPD servers.-data Conn = Conn { cOpen :: IO () -- Open connection- , cClose :: IO () -- Close connection- , cSend :: String -> IO (Response String) -- Write to connection- , cGetPW :: IO (Maybe String) } -- Password function---- | The MPDError type is used to signal errors, both from the MPD and--- otherwise.-data MPDError = NoMPD -- ^ MPD not responding- | TimedOut -- ^ The connection timed out- | Unexpected String -- ^ MPD returned an unexpected response.- -- This is a bug, either in the library or- -- in MPD itself.- | Custom String -- ^ Used for misc. errors- | ACK ACKType String -- ^ ACK type and a message from the- -- server- deriving Eq--instance Show MPDError where- show NoMPD = "Could not connect to MPD"- show TimedOut = "MPD connection timed out"- show (Unexpected s) = "MPD returned an unexpected response: " ++ s- show (Custom s) = s- show (ACK _ s) = s---- | Represents various MPD errors (aka. ACKs).-data ACKType = InvalidArgument -- ^ Invalid argument passed (ACK 2)- | InvalidPassword -- ^ Invalid password supplied (ACK 3)- | Auth -- ^ Authentication required (ACK 4)- | UnknownCommand -- ^ Unknown command (ACK 5)- | FileNotFound -- ^ File or directory not found ACK 50)- | PlaylistMax -- ^ Playlist at maximum size (ACK 51)- | System -- ^ A system error (ACK 52)- | PlaylistLoad -- ^ Playlist loading failed (ACK 53)- | Busy -- ^ Update already running (ACK 54)- | NotPlaying -- ^ An operation requiring playback- -- got interrupted (ACK 55)- | FileExists -- ^ File already exists (ACK 56)- | UnknownACK -- ^ An unknown ACK (aka. bug)- deriving (Eq)+type Host = String+type Port = Integer --- | A response is either an 'MPDError' or some result.-type Response a = Either MPDError a+--+-- IO based MPD client implementation.+-- --- Export the type name but not the constructor or the field.--- | The MPD monad is basically a reader and error monad--- combined.+-- | The main implementation of an MPD client. It actually connects+-- to a server and interacts with it. -- -- To use the error throwing\/catching capabilities: ----- > import Control.Monad.Error+-- > import Control.Monad.Error (throwError, catchError) -- -- To run IO actions within the MPD monad: ----- > import Control.Monad.Trans-data MPD a = MPD { runMPD :: Conn -> IO (Response a) }+-- > import Control.Monad.Trans (liftIO)+newtype MPD a =+ MPD { runMPD :: ErrorT MPDError+ (StateT (Maybe Handle)+ (ReaderT (Host, Port, Password) IO)) a+ } deriving (Functor, Monad, MonadIO, MonadError MPDError) -instance Functor MPD where- fmap f m = MPD $ \conn -> either Left (Right . f) `liftM` runMPD m conn+instance Applicative MPD where+ (<*>) = ap+ pure = return -instance Monad MPD where- return a = MPD $ \_ -> return $ Right a- m >>= f = MPD $ \conn -> runMPD m conn >>=- either (return . Left) (flip runMPD conn . f)- fail err = MPD $ \_ -> return . Left $ Custom err+instance MonadMPD MPD where+ open = mpdOpen+ close = mpdClose+ send = mpdSend+ getPassword = MPD $ ask >>= \(_,_,pw) -> return pw -instance MonadIO MPD where- liftIO m = MPD $ \_ -> liftM Right m+-- | A response is either an 'MPDError' or some result.+type Response = Either MPDError -instance MonadError MPDError MPD where- throwError e = MPD $ \_ -> return (Left e)- catchError m h = MPD $ \conn ->- runMPD m conn >>= either (flip runMPD conn . h) (return . Right)+-- | The most configurable API for running an MPD action.+withMPDEx :: Host -> Port -> Password -> MPD a -> IO (Response a)+withMPDEx host port pw x = withSocketsDo $+ runReaderT (evalStateT (runErrorT . runMPD $ open >> x) Nothing)+ (host, port, pw) -instance Error MPDError where- noMsg = Custom "An error occurred"- strMsg = Custom+mpdOpen :: MPD ()+mpdOpen = MPD $ do+ (host, port, _) <- ask+ runMPD close+ handle <- liftIO (safeConnectTo host port)+ put handle+ F.forM_ handle (const $ runMPD checkConn >>= flip unless (runMPD close))+ where+ safeConnectTo host port =+ (Just <$> connectTo host (PortNumber $ fromInteger port))+ `catch` const (return Nothing) ------ Basic connection functions---+ checkConn =+ isPrefixOf "OK MPD" <$> send "" --- | Refresh a connection.-reconnect :: MPD ()-reconnect = MPD $ \conn -> Right `liftM` cOpen conn+mpdClose :: MPD ()+mpdClose =+ MPD $ get >>= F.mapM_ (liftIO . sendClose) >> put Nothing+ where+ sendClose handle =+ (hPutStrLn handle "close" >> hReady handle >> hClose handle)+ `catch` whenEOF (return ()) --- | Kill the server. Obviously, the connection is then invalid.-kill :: MPD ()-kill = getResponse "kill" `catchError` cleanup >> return ()+ whenEOF result err+ | isEOFError err = result+ | otherwise = ioError err++mpdSend :: String -> MPD String+mpdSend str = send' `catchError` handler where- cleanup TimedOut = MPD $ \conn -> cClose conn >> return (Right [])- cleanup x = throwError x >> return []+ handler TimedOut = mpdOpen >> send'+ handler err = throwError err --- | Close an MPD connection.-close :: MPD ()-close = MPD $ \conn -> cClose conn >> return (Right ())+ send' = MPD $ get >>= maybe (throwError NoMPD) go + go handle = do+ unless (null str) $+ liftIO $ U.hPutStrLn handle str >> hFlush handle+ liftIO ((Right <$> getLines handle []) `catch` (return . Left))+ >>= either (\err -> if isEOFError err then+ put Nothing >> throwError TimedOut+ else liftIO (ioError err))+ return++ getLines handle acc = do+ l <- U.hGetLine handle+ if "OK" `isPrefixOf` l || "ACK" `isPrefixOf` l+ then return . unlines $ reverse (l:acc)+ else getLines handle (l:acc)++ ----- Sending messages and handling responses.+-- Other operations. -- --- | Send a command to the MPD and return the result.-getResponse :: String -> MPD [String]-getResponse cmd = MPD f+ignore :: (Monad m) => m a -> m ()+ignore x = x >> return ()++-- | Kill the server. Obviously, the connection is then invalid.+kill :: (MonadMPD m) => m ()+kill = ignore (send "kill") `catchError` cleanup where- f conn = catchAuth . either Left parseResponse =<< cSend conn cmd- where- catchAuth (Left (ACK Auth _)) = tryPassword conn (f conn)- catchAuth x = return x+ cleanup e = if e == TimedOut then close else throwError e --- Send a password to MPD and run an action on success.-tryPassword :: Conn -> IO (Response a) -> IO (Response a)-tryPassword conn cont = do- resp <- cGetPW conn >>= maybe failAuth (cSend conn . ("password " ++))- case resp of- Left e -> return $ Left e- Right x -> either (return . Left) (const cont) $ parseResponse x- where failAuth = return . Left $ ACK Auth "Password required"+-- | Send a command to the MPD server and return the result.+getResponse :: (MonadMPD m) => String -> m [String]+getResponse cmd = (send cmd >>= parseResponse) `catchError` sendpw+ where+ sendpw e@(ACK Auth _) = do+ pw <- getPassword+ if null pw then throwError e+ else send ("password " ++ pw) >>= parseResponse+ >> send cmd >>= parseResponse+ sendpw e =+ throwError e -- Consume response and return a Response.-parseResponse :: String -> Response [String]-parseResponse s | null xs = Left $ NoMPD- | isPrefixOf "ACK" (head xs) = Left $ parseAck s- | otherwise = Right $ takeWhile ("OK" /=) xs- where xs = lines s+parseResponse :: (MonadError MPDError m) => String -> m [String]+parseResponse s+ | null xs = throwError $ NoMPD+ | isPrefixOf "ACK" (head xs) = throwError $ parseAck s+ | otherwise = return $ Prelude.takeWhile ("OK" /=) xs+ where+ xs = lines s -- Turn MPD ACK into the corresponding 'MPDError' 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+ 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 -- Break an ACK into (error code, current command, message). -- ACKs are of the form: -- ACK [error@command_listNum] {current_command} message_text\n-splitAck :: String -> (String, String, String)-splitAck s = (code, cmd, msg)- where (code, notCode) = between (== '[') (== '@') s- (cmd, notCmd) = between (== '{') (== '}') notCode- msg = drop 1 . snd $ break (== ' ') notCmd+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 - -- take whatever is between 'f' and 'g'.- between f g xs = let (_, y) = break f xs- in break g (drop 1 y)+ -- take whatever is between 'f' and 'g'.+ between a b xs = let (_, y) = break (== a) xs+ in break (== b) (drop 1 y)
+ Network/MPD/Core/Class.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE FlexibleContexts #-}++-- | Module : Network.MPD.Core.Class+-- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010+-- License : LGPL (see LICENSE)+-- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>+-- Stability : alpha+--+-- The MPD typeclass.++module Network.MPD.Core.Class where++import Network.MPD.Core.Error (MPDError)++import Control.Monad.Error (MonadError)++type Password = String++-- | A typeclass to allow for multiple implementations of a connection+-- to an MPD server.+class (Monad m, MonadError MPDError m) => MonadMPD m where+ -- | Open (or re-open) a connection to the MPD server.+ open :: m ()+ -- | Close the connection.+ close :: m ()+ -- | Send a string to the server and return its response.+ send :: String -> m String+ -- | Produce a password to send to the server should it ask for+ -- one.+ getPassword :: m Password
+ Network/MPD/Core/Error.hs view
@@ -0,0 +1,50 @@+-- | Module : Network.MPD.Core.Error+-- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010+-- License : LGPL (see LICENSE)+-- Maintainer : Joachim Fasting <joachim.fasting@gmail.com>+-- Stability : alpha+--+-- MPD errors.++module Network.MPD.Core.Error where++import Control.Monad.Error (Error(..))++-- | The MPDError type is used to signal errors, both from the MPD and+-- otherwise.+data MPDError = NoMPD -- ^ MPD not responding+ | TimedOut -- ^ The connection timed out+ | Unexpected String -- ^ MPD returned an unexpected response.+ -- This is a bug, either in the library or+ -- in MPD itself.+ | Custom String -- ^ Used for misc. errors+ | ACK ACKType String -- ^ ACK type and a message from the+ -- server+ deriving Eq++instance Show MPDError where+ show NoMPD = "Could not connect to MPD"+ show TimedOut = "MPD connection timed out"+ show (Unexpected s) = "MPD returned an unexpected response: " ++ s+ show (Custom s) = s+ show (ACK _ s) = s++instance Error MPDError where+ noMsg = Custom "An error occurred"+ strMsg = Custom++-- | Represents various MPD errors (aka. ACKs).+data ACKType = InvalidArgument -- ^ Invalid argument passed (ACK 2)+ | InvalidPassword -- ^ Invalid password supplied (ACK 3)+ | Auth -- ^ Authentication required (ACK 4)+ | UnknownCommand -- ^ Unknown command (ACK 5)+ | FileNotFound -- ^ File or directory not found ACK 50)+ | PlaylistMax -- ^ Playlist at maximum size (ACK 51)+ | System -- ^ A system error (ACK 52)+ | PlaylistLoad -- ^ Playlist loading failed (ACK 53)+ | Busy -- ^ Update already running (ACK 54)+ | NotPlaying -- ^ An operation requiring playback+ -- got interrupted (ACK 55)+ | FileExists -- ^ File already exists (ACK 56)+ | UnknownACK -- ^ An unknown ACK (aka. bug)+ deriving Eq
− Network/MPD/Parse.hs
@@ -1,156 +0,0 @@--- | Module : Network.MPD.Parse--- Copyright : (c) Ben Sinclair 2005-2008--- License : LGPL (see LICENSE)--- Maintainer : bsinclai@turing.une.edu.au--- Stability : alpha--- Portability : Haskell 98------ Various data types and parsing functions for them.--module Network.MPD.Parse where--import Network.MPD.Types--import Control.Monad.Error-import Network.MPD.Utils-import Network.MPD.Core (MPD, MPDError(Unexpected))---- | Builds a 'Count' instance from an assoc. list.-parseCount :: [String] -> Either String Count-parseCount = foldM f empty . toAssoc- where f a ("songs", x) = parse parseNum- (\x' -> a { cSongs = x'}) x- f a ("playtime", x) = parse parseNum- (\x' -> a { cPlaytime = x' }) x- f _ x = Left $ show x- empty = Count { cSongs = 0, cPlaytime = 0 }---- | Builds a list of 'Device' instances from an assoc. list-parseOutputs :: [String] -> Either String [Device]-parseOutputs = mapM (foldM f empty) . splitGroups [("outputid",id)] . toAssoc- where f a ("outputid", x) = parse parseNum (\x' -> a { dOutputID = x' }) x- f a ("outputname", x) = return a { dOutputName = x }- f a ("outputenabled", x) = parse parseBool- (\x' -> a { dOutputEnabled = x'}) x- f _ x = fail $ show x- empty = Device 0 "" False---- | Builds a 'Stats' instance from an assoc. list.-parseStats :: [String] -> Either String Stats-parseStats = foldM f defaultStats . toAssoc- where- f a ("artists", x) = parse parseNum (\x' -> a { stsArtists = x' }) x- f a ("albums", x) = parse parseNum (\x' -> a { stsAlbums = x' }) x- f a ("songs", x) = parse parseNum (\x' -> a { stsSongs = x' }) x- f a ("uptime", x) = parse parseNum (\x' -> a { stsUptime = x' }) x- f a ("playtime", x) = parse parseNum (\x' -> a { stsPlaytime = x' }) x- f a ("db_playtime", x) = parse parseNum- (\x' -> a { stsDbPlaytime = x' }) x- f a ("db_update", x) = parse parseNum (\x' -> a { stsDbUpdate = x' }) x- f _ x = fail $ show x- defaultStats =- Stats { stsArtists = 0, stsAlbums = 0, stsSongs = 0, stsUptime = 0- , stsPlaytime = 0, stsDbPlaytime = 0, stsDbUpdate = 0 }----- | Builds a 'Song' instance from an assoc. list.-parseSong :: [(String, String)] -> Either String Song-parseSong xs = foldM f song xs- where f a ("Artist", x) = return a { sgArtist = x }- f a ("Album", x) = return a { sgAlbum = x }- f a ("Title", x) = return a { sgTitle = x }- f a ("Genre", x) = return a { sgGenre = x }- f a ("Name", x) = return a { sgName = x }- f a ("Composer", x) = return a { sgComposer = x }- f a ("Performer", x) = return a { sgPerformer = x }- f a ("Date", x) = parse parseDate (\x' -> a { sgDate = x' }) x- f a ("Track", x) = parse parseTuple (\x' -> a { sgTrack = x'}) x- f a ("Disc", x) = parse parseTuple (\x' -> a { sgDisc = x'}) x- f a ("file", x) = return a { sgFilePath = x }- f a ("Time", x) = parse parseNum (\x' -> a { sgLength = x'}) x- f a ("Id", x) = parse parseNum- (\x' -> a { sgIndex = Just (ID x') }) x- -- We prefer Id but take Pos if no Id has been found.- f a ("Pos", x) =- maybe (parse parseNum (\x' -> a { sgIndex = Just (Pos x') }) x)- (const $ return a)- (sgIndex a)- -- Catch unrecognised keys- f _ x = fail $ show x-- parseTuple s = let (x, y) = breakChar '/' s in- -- Handle incomplete values. For example, songs might- -- have a track number, without specifying the total- -- number of tracks, in which case the resulting- -- tuple will have two identical parts.- case (parseNum x, parseNum y) of- (Just x', Nothing) -> Just (x', x')- (Just x', Just y') -> Just (x', y')- _ -> Nothing-- song = Song { sgArtist = "", sgAlbum = "", sgTitle = ""- , sgGenre = "", sgName = "", sgComposer = ""- , sgPerformer = "", sgDate = 0, sgTrack = (0,0)- , sgDisc = (0,0), sgFilePath = "", sgLength = 0- , sgIndex = Nothing }---- | Builds a 'Status' instance from an assoc. list.-parseStatus :: [String] -> Either String Status-parseStatus = foldM f empty . toAssoc- where f a ("state", x) = parse state (\x' -> a { stState = x'}) x- f a ("volume", x) = parse parseNum (\x' -> a { stVolume = x'}) x- f a ("repeat", x) = parse parseBool- (\x' -> a { stRepeat = x' }) x- f a ("random", x) = parse parseBool- (\x' -> a { stRandom = x' }) x- f a ("playlist", x) = parse parseNum- (\x' -> a { stPlaylistVersion = x'}) x- f a ("playlistlength", x) = parse parseNum- (\x' -> a { stPlaylistLength = x'}) x- f a ("xfade", x) = parse parseNum- (\x' -> a { stXFadeWidth = x'}) x- f a ("song", x) = parse parseNum- (\x' -> a { stSongPos = Just (Pos x') }) x- f a ("songid", x) = parse parseNum- (\x' -> a { stSongID = Just (ID x') }) x- f a ("time", x) = parse time (\x' -> a { stTime = x' }) x- f a ("bitrate", x) = parse parseNum- (\x' -> a { stBitrate = x'}) x- f a ("audio", x) = parse audio (\x' -> a { stAudio = x' }) x- f a ("updating_db", x) = parse parseNum- (\x' -> a { stUpdatingDb = x' }) x- f a ("error", x) = return a { stError = x }- f _ x = fail $ show x-- state "play" = Just Playing- state "pause" = Just Paused- state "stop" = Just Stopped- state _ = Nothing-- time s = pair parseNum $ breakChar ':' s-- audio s = let (u, u') = breakChar ':' s- (v, w) = breakChar ':' u' in- case (parseNum u, parseNum v, parseNum w) of- (Just a, Just b, Just c) -> Just (a, b, c)- _ -> Nothing-- empty = Status Stopped 0 False False 0 0 Nothing Nothing (0,0) 0 0- (0,0,0) 0 ""---- | Run a parser and lift the result into the 'MPD' monad-runParser :: (input -> Either String a) -> input -> MPD a-runParser f = either (throwError . Unexpected) return . f---- | A helper that runs a parser on a string and, depending, on the--- outcome, either returns the result of some command applied to the--- result, or fails. Used when building structures.-parse :: Monad m => (String -> Maybe a) -> (a -> b) -> String -> m b-parse p g x = maybe (fail x) (return . g) (p x)---- | A helper for running a parser returning Maybe on a pair of strings.--- Returns Just if both strings where parsed successfully, Nothing otherwise.-pair :: (String -> Maybe a) -> (String, String) -> Maybe (a, a)-pair p (x, y) = case (p x, p y) of- (Just a, Just b) -> Just (a, b)- _ -> Nothing
− Network/MPD/SocketConn.hs
@@ -1,83 +0,0 @@--- | Module : Network.MPD.SocketConn--- Copyright : (c) Ben Sinclair 2005-2008--- License : LGPL (see LICENSE)--- Maintainer : bsinclai@turing.une.edu.au--- Stability : alpha--- Portability : Haskell 98------ Connection over a network socket.--module Network.MPD.SocketConn (withMPDEx) where--import Network.MPD.Core hiding (close)-import Data.IORef (IORef, newIORef, readIORef, writeIORef)-import Network-import System.IO-import Control.Monad (liftM, unless)-import Data.List (isPrefixOf)-import System.IO.Error (isEOFError)-import Control.Exception (finally)-import qualified System.IO.UTF8 as U---- | Run an MPD action against a server.-withMPDEx :: String -- ^ Host name.- -> Integer -- ^ Port number.- -> IO (Maybe String) -- ^ An action that supplies passwords.- -> MPD a -- ^ The action to run.- -> IO (Response a)-withMPDEx host port getpw m = do- hR <- newIORef Nothing- let open' = open host port hR- close' = close hR- send' = send hR- cautiousSend' x =- send' x >>= either (\e -> case e of TimedOut -> open' >> send' x- _ -> return $ Left e)- (return . Right)- open'- runMPD m (Conn open' close' cautiousSend' getpw) `finally` close'--open :: String -> Integer -> IORef (Maybe Handle) -> IO ()-open host port hR = withSocketsDo $ do- close hR- h <- safeConnectTo host port- writeIORef hR h- maybe (return ()) (\_ -> checkConn hR >>= flip unless (close hR)) h--close :: IORef (Maybe Handle) -> IO ()-close hR = readIORef hR >>= maybe (return ()) sendClose >> writeIORef hR Nothing- where sendClose h = catch (hPutStrLn h "close" >> hReady h >> hClose h)- (whenEOF $ writeIORef hR Nothing)--send :: IORef (Maybe Handle) -> String -> IO (Response String)-send hR str = do- hM <- readIORef hR- case hM of- Nothing -> return $ Left NoMPD- Just h -> do- unless (null str) (U.hPutStrLn h str >> hFlush h)- catch (getLines h [])- (whenEOF $ writeIORef hR Nothing >> return (Left TimedOut))- where- getLines handle acc = do- l <- U.hGetLine handle- if "OK" `isPrefixOf` l || "ACK" `isPrefixOf` l- then return . Right . unlines . reverse $ l:acc- else getLines handle (l:acc)------- Helpers------- Return a value if the error is an EOFError, otherwise throw the error again.-whenEOF :: IO a -> IOError -> IO a-whenEOF result err = if isEOFError err then result else ioError err--safeConnectTo :: String -> Integer -> IO (Maybe Handle)-safeConnectTo host port =- catch (liftM Just . connectTo host . PortNumber $ fromInteger port)- (const $ return Nothing)---- Check that an MPD daemon is at the other end of a connection.-checkConn :: IORef (Maybe Handle) -> IO Bool-checkConn c = liftM (either (const False) (isPrefixOf "OK MPD")) $ send c ""
− Network/MPD/Types.hs
@@ -1,94 +0,0 @@--- | Module : Network.MPD.Types--- Copyright : (c) Ben Sinclair 2005-2008--- License : LGPL (see LICENSE)--- Maintainer : bsinclai@turing.une.edu.au--- Stability : alpha--- Portability : Haskell 98------ Various MPD data structures and types--module Network.MPD.Types where--type Seconds = Integer---- | Represents a song's playlist index.-data PLIndex = Pos Integer -- ^ A playlist position index (starting from 0)- | ID Integer -- ^ A playlist ID number that more robustly- -- identifies a song.- deriving (Show, Eq)---- | Represents the different playback states.-data State = Playing- | Stopped- | Paused- deriving (Show, Eq)---- | Represents the result of running 'count'.-data Count =- Count { cSongs :: Integer -- ^ Number of songs matching the query- , cPlaytime :: Seconds -- ^ Total play time of matching songs- }- deriving (Eq, Show)---- | Represents an output device.-data Device =- Device { dOutputID :: Int -- ^ Output's ID number- , dOutputName :: String -- ^ Output's name as defined in the MPD- -- configuration file- , dOutputEnabled :: Bool }- deriving (Eq, Show)---- | Container for database statistics.-data Stats =- Stats { stsArtists :: Integer -- ^ Number of artists.- , stsAlbums :: Integer -- ^ Number of albums.- , stsSongs :: Integer -- ^ Number of songs.- , stsUptime :: Seconds -- ^ Daemon uptime in seconds.- , stsPlaytime :: Seconds -- ^ Total playing time.- , stsDbPlaytime :: Seconds -- ^ Total play time of all the songs in- -- the database.- , stsDbUpdate :: Integer -- ^ Last database update in UNIX time.- }- deriving (Eq, Show)---- | Represents a single song item.-data Song =- Song { sgArtist, sgAlbum, sgTitle, sgFilePath, sgGenre, sgName, sgComposer- , sgPerformer :: String- , sgLength :: Seconds -- ^ Length in seconds- , sgDate :: Int -- ^ Year- , sgTrack :: (Int, Int) -- ^ Track number\/total tracks- , sgDisc :: (Int, Int) -- ^ Position in set\/total in set- , sgIndex :: Maybe PLIndex }- deriving (Eq, Show)---- | Container for MPD status.-data Status =- Status { stState :: State- -- | A percentage (0-100)- , stVolume :: Int- , stRepeat :: Bool- , stRandom :: Bool- -- | A value that is incremented by the server every time the- -- playlist changes.- , stPlaylistVersion :: Integer- -- | The number of items in the current playlist.- , stPlaylistLength :: Integer- -- | Current song's position in the playlist.- , stSongPos :: Maybe PLIndex- -- | Current song's playlist ID.- , stSongID :: Maybe PLIndex- -- | Time elapsed\/total time.- , stTime :: (Seconds, Seconds)- -- | Bitrate (in kilobytes per second) of playing song (if any).- , stBitrate :: Int- -- | Crossfade time.- , stXFadeWidth :: Seconds- -- | Samplerate\/bits\/channels for the chosen output device- -- (see mpd.conf).- , stAudio :: (Int, Int, Int)- -- | Job ID of currently running update (if any).- , stUpdatingDb :: Integer- -- | Last error message (if any).- , stError :: String }- deriving (Eq, Show)
Network/MPD/Utils.hs view
@@ -1,18 +1,18 @@ -- | Module : Network.MPD.Utils--- Copyright : (c) Ben Sinclair 2005-2008+-- Copyright : (c) Ben Sinclair 2005-2009, Joachim Fasting 2010 -- License : LGPL (see LICENSE)--- Maintainer : bsinclai@turing.une.edu.au+-- Maintainer : Joachim Fasting <joachim.fasting@gmail.com> -- Stability : alpha--- Portability : Haskell 98 -- -- Utilities. module Network.MPD.Utils ( parseDate, parseNum, parseBool, showBool,- breakChar, toAssoc, splitGroups+ breakChar, toAssoc, toAssocList, splitGroups ) where import Data.Char (isDigit)+import Data.Maybe (fromMaybe) -- Break a string by character, removing the separator. breakChar :: Char -> String -> (String, String)@@ -42,13 +42,15 @@ "0" -> Just False _ -> Nothing --- Break up a list of strings into an assoc. list, separating at--- the first ':'.-toAssoc :: [String] -> [(String, String)]-toAssoc = map f- where f x = let (k,v) = break (== ':') x in- (k,dropWhile (== ' ') $ drop 1 v)+-- Break a string into an key-value pair, separating at the first ':'.+toAssoc :: String -> (String, String)+toAssoc x = (k, dropWhile (== ' ') $ drop 1 v)+ where+ (k,v) = break (== ':') x +toAssocList :: [String] -> [(String, String)]+toAssocList = map toAssoc+ -- Takes an assoc. list with recurring keys and groups each cycle of -- keys with their values together. There can be several keys that -- begin cycles, being listed as the first tuple component in the@@ -63,7 +65,7 @@ splitGroups [] _ = [] splitGroups _ [] = [] splitGroups wrappers (x@(k,_):xs) =- maybe (splitGroups wrappers xs) id $ do- f <- k `lookup` wrappers- let (us,vs) = break (\(k',_) -> k' `elem` map fst wrappers) xs- return $ (f $ x:us) : splitGroups wrappers vs+ fromMaybe (splitGroups wrappers xs) $ do+ f <- k `lookup` wrappers+ let (us,vs) = break (\(k',_) -> k' `elem` map fst wrappers) xs+ return $ f (x:us) : splitGroups wrappers vs
− README
@@ -1,50 +0,0 @@-- libmpd : An MPD client library for Haskell- ----------------------------------------------About MPD:- MPD is a daemon for playing music that is controlled over network- sockets. Its website is at http://www.musicpd.org/.--Dependencies:- The Glasgow Haskell Compiler >= 6.6 http://haskell.org/ghc- Cabal >= 1.2 http://haskell.org/cabal- network http://hackage.haskell.org/cgi-bin/hackage-scripts/package/network- mtl http://hackage.haskell.org/cgi-bin/hackage-scripts/package/mtl- filepath http://hackage.haskell.org/cgi-bin/hackage-scripts/package/filepath--Building:- $ runhaskell Setup configure --prefix=$HOME- $ runhaskell Setup build- $ runhaskell Setup install --user-- This assumes you have Cabal installed. Cabal is installed by- default with newer GHCs.--API compatibility:- This library covers all the functionality provided by MPD version 0.13.0.--Use:- Import the Network.MPD module and specify the libmpd package- (-package libmpd) when compiling, or add it to the `Build-Depends'- list in your `_.cabal' file.--Limitations:- Some parts, most notably parsing, are not yet as good as they could be.- The library is thus ill-suited for applications where speed is a major- concern.--Platforms:- libmpd has been confirmed to work on:- Linux/x86- Seems to work (as in compile) on Windows XP as well.--Latest Sources:- darcs get http://turing.une.edu.au/~bsinclai/code/libmpd-haskell--License:- LGPL--Authors:- Ben Sinclair <bsinclai@turing.une.edu.au>- Joachim Fasting <joachim.fasting@gmail.com>
+ README.md view
@@ -0,0 +1,99 @@+# libmpd-haskell: a client library for MPD++## About++libmpd-haskell is a client library for [MPD] written in [Haskell] that <br />+aims to provide a safe and flexible yet consistent and intuitive <br />+interface to MPD's external API.++[MPD]: http://www.musicpd.org+[Haskell]: http://www.haskell.org++## Getting+* [Latest release]+* `git clone git://github.com/bens/libmpd-haskell.git`++[Latest release]: http://hackage.haskell.org/package/libmpd+[GIT repository]: git://github.com/bens/libmpd-haskell.git++## Building+The preferred method of building libmpd-haskell is using [cabal-install], which+takes care of dependency resolution and other minutiae.++To install libmpd-haskell, simply run:++`cd libmpd-haskell && cabal install`++To use the deprecated base 3, run:++`cabal install -f old_base`++[cabal-install]: http://hackage.haskell.org/package/cabal-install++## Compiler support+We try to support the two last major versions of GHC, but only the latest+version is actually tested for.++## MPD API compliance+We try to comply with the latest version of the MPD protocol specification;+any deviation from this is a bug.++## Usage++With GHCi:++ > import Network.MPD+ > withMPD $ lsInfo ""+ Right [Left "Tool", Left "Tom Waits",...]+ > withMPD $ add "Tom Waits/Big Time"+ Right ["Tom Waits/Big Time/01 - 16 Shells from a Thirty-Ough-Six.mp3",...]++## Development++To start developing libmpd-haskell you'll first need a clone of the+source code repository:++`git clone git://github.com/joachifm/libmpd-haskell`++To pull in new changes from upstream, use:++`git pull origin master`++When writing or modifying code, please try to conform to the+surrounding style. If you introduce new functionality, please include+a test case or at least document the expected behavior.++### Submitting patches+To submit a patch, use `git format-patch` and email the resulting+file to one of the developers or upload it to the [bug tracker].++Alternatively you can create your own fork of the [repository]+and send a pull request.++### Submitting bug reports+See our [bug tracker].++### Resources+* [API documentation]+* [Code coverage]+* [Protocol reference]+* [Using GitHub]+* \#libmpd-haskell @ irc.freenode.net++[bug tracker]: http://trac.haskell.org/libmpd/+[GitHub]: http://www.github.com+[repository]: http://www.github.com/bens/libmpd-haskell+[API documentation]: http://projects.haskell.org/libmpd/doc/+[Code coverage]: http://projects.haskell.org/libmpd/coverage/hpc_index.html+[Protocol reference]: http://www.musicpd.org/doc/protocol/+[Using GitHub]: http://help.github.com++## License+LGPL version 2.1++## Authors+Ben Sinclair \<ben.d.sinclair@gmail.com\>++Joachim Fasting \<joachim.fasting@gmail.com\>++Daniel Schoepe \<daniel.schoepe@googlemail.com\>
− TODO
@@ -1,2 +0,0 @@-TODO:- - improve parsing performance
libmpd.cabal view
@@ -1,30 +1,71 @@ Name: libmpd-Version: 0.3.1+Version: 0.4.0 License: LGPL License-file: LICENSE-Copyright: Ben Sinclair 2005-2008+Copyright: Ben Sinclair 2005-2009, Joachim Fasting 2010 Author: Ben Sinclair-Maintainer: bsinclai@turing.une.edu.au+Maintainer: Joachim Fasting <joachim.fasting@gmail.com> Stability: beta-Homepage: http://turing.une.edu.au/~bsinclai/code/libmpd-haskell.html+Homepage: http://projects.haskell.org/libmpd/+Bug-reports: http://trac.haskell.org/libmpd/newticket Synopsis: An MPD client library. Description: A client library for MPD, the Music Player Daemon (<http://www.musicpd.org/>). Category: Network, Sound-Tested-With: GHC == 6.6.1, GHC == 6.8.2+Tested-With: GHC == 6.10.1, GHC == 6.12.1 Build-Type: Simple-Cabal-Version: >= 1.2-Extra-Source-Files: TODO README ChangeLog tests/Properties.hs tests/Commands.hs- tests/Main.hs tests/Displayable.hs tests/run-tests- tests/coverage+Cabal-Version: >= 1.6 && < 1.9+Extra-Source-Files: README.md ChangeLog tests/Properties.hs tests/Commands.hs+ tests/Main.hs tests/Displayable.hs tests/run-tests.lhs+ tests/coverage.lhs +flag test+ Description: Build test driver+ Default: False++flag coverage+ Description: Build driver with hpc instrumentation+ Default: False++flag old_base+ Description: Use base version 3+ Default: False++Source-Repository head+ type: git+ location: git://github.com/joachifm/libmpd-haskell.git+ Library- Build-Depends: base >= 2.1.1, network >= 2.0.1, mtl >= 1.0.1,- filepath >= 1.0, utf8-string >= 0.3.1- Exposed-Modules: Network.MPD- Other-Modules: Network.MPD.Core, Network.MPD.Commands,- Network.MPD.Parse, Network.MPD.SocketConn,- Network.MPD.Types, Network.MPD.Utils+ if flag(old_base)+ Build-Depends: base >= 3 && < 4+ else+ Build-Depends: base >= 4 && < 5 - ghc-options: -Wall+ Build-Depends: network >= 2.1 && < 2.3,+ mtl >= 1.1 && < 1.2, filepath >= 1.0 && < 1.2,+ utf8-string >= 0.3.1 && < 0.4+ Exposed-Modules: Network.MPD, Network.MPD.Core+ Other-Modules: Network.MPD.Core.Class,+ Network.MPD.Core.Error,+ Network.MPD.Commands,+ Network.MPD.Commands.Arg,+ Network.MPD.Commands.Parse,+ Network.MPD.Commands.Query,+ Network.MPD.Commands.Types,+ Network.MPD.Utils ghc-prof-options: -auto-all -prof++ if flag(test)+ Buildable: False++Executable test+ Hs-Source-Dirs: . tests+ Main-Is: tests/Main.hs+ Build-Depends: base, network, mtl, filepath, utf8-string, QuickCheck ==2.1.*+ ghc-options: -Wall -Werror -fno-warn-warnings-deprecations++ if flag(coverage)+ ghc-options: -fhpc++ if !flag(test)+ Buildable: False
tests/Commands.hs view
@@ -11,24 +11,29 @@ module Commands (main) where +import Arbitrary () import Displayable import Network.MPD.Commands-import Network.MPD.Core (Response, MPDError(..))+import Network.MPD.Core (MPDError(..), Response, ACKType(..)) import StringConn -import Control.Monad import Prelude hiding (repeat)+import Data.Char (isPrint, isSpace)+import Data.Maybe (fromJust, isJust) import Text.Printf+import qualified Test.QuickCheck as QC+import Test.QuickCheck ((==>)) -main = mapM_ (\(n, f) -> f >>= \x -> printf "%-14s: %s\n" n x) tests+main = mapM_ (\(n, f) -> printf "%-25s : " n >> f) tests where tests = [("enableOutput", testEnableOutput) ,("disableOutput", testDisableOutput) ,("outputs", testOutputs) ,("update0", testUpdate0) ,("update1", testUpdate1) ,("updateMany", testUpdateMany)- ,("find", testFind)- ,("find / complex query", testFindComplex)+ ,("prop_find", mycheck prop_find 100)+ ,("prop_lsFiles", mycheck prop_lsFiles 100)+ ,("prop_lsDirs", mycheck prop_lsDirs 100) ,("list(Nothing)", testListNothing) ,("list(Just)", testListJust) ,("listAll", testListAll)@@ -44,7 +49,7 @@ ,("clear / current", testClearCurrent) ,("plChangesPosId 0", testPlChangesPosId_0) ,("plChangesPosId 1", testPlChangesPosId_1)- ,("plChangesPosId wierd", testPlChangesPosId_Wierd)+ ,("plChangesPosId weird", testPlChangesPosId_Weird) ,("currentSong(_)", testCurrentSongStopped) ,("currentSong(>)", testCurrentSongPlaying) ,("delete0", testDelete0)@@ -84,12 +89,16 @@ ,("repeat", testRepeat) ,("setVolume", testSetVolume) ,("volume", testVolume)+ ,("consume", testConsume)+ ,("single", testSingle) ,("clearError", testClearError) ,("commands", testCommands) ,("notCommands", testNotCommands) ,("tagTypes", testTagTypes) ,("urlHandlers", testUrlHandlers) ,("password", testPassword)+ ,("passwordSucceeds", testPasswordSucceeds)+ ,("passwordFails", testPasswordFails) ,("ping", testPing) ,("stats", testStats) ,("updateId0", testUpdateId0)@@ -102,19 +111,31 @@ ,("deleteMany1", testDeleteMany1) ] -test a b c = liftM (showResult b) $ testMPD a b (return Nothing) c+test :: (Show a, Eq a)+ => [(Expect, Response String)] -> Response a -> StringMPD a -> IO ()+test a b c = putStrLn . showResult $ testMPD a b "" c +test_ :: [(Expect, Response String)] -> StringMPD () -> IO () test_ a b = test a (Right ()) b -showResult :: (Show a) => Response a -> Result a -> String-showResult _ Ok = "passed"-showResult expectedResult (Failure result mms) =- "*** FAILURE ***" ++- concatMap (\(x,y) -> "\n expected request: " ++ show x ++- "\n actual request: " ++ show y) mms ++- "\n expected result: " ++ show expectedResult ++- "\n actual result: " ++ show result+mycheck :: QC.Testable a => a -> Int -> IO ()+mycheck a n = QC.quickCheckWith QC.stdArgs { QC.maxSize = n } a +showResult :: Show a => Result a -> String+showResult Ok =+ "OK, passed."+showResult (BadRequest TooManyRequests) = unlines+ ["*** FAILURE ***"+ ," more requests were made than expected."]+showResult (BadRequest (UnexpectedRequest expected actual)) = unlines+ ["*** FAILURE ***"+ ," expected request: " ++ show expected+ ," actual request: " ++ show actual]+showResult (BadResult expected actual) = unlines+ ["*** FAILURE ***"+ ," expected result: " ++ show expected+ ," actual result: " ++ show actual]+ -- -- Admin commands --@@ -144,29 +165,57 @@ -- Database commands -- -testFind =- test [("find Artist \"Foo\"", Right resp)] (Right [obj])- (find [Match Artist "Foo"])- where obj = empty { sgArtist = "Foo", sgTitle = "Bar"- , sgFilePath = "dir/Foo-Bar.ogg", sgLength = 60 }- resp = display obj ++ "OK"+prop_find :: Song -> Meta -> QC.Property+prop_find song meta = isJust (sgDisc song) ==> result == Ok+ where+ result = testMPD [("find "++ show meta ++ " \"" ++ query ++ "\""+ ,Right (display song ++ "OK"))]+ (Right [song])+ ""+ (find (meta =? query))+ query = case meta of+ Artist -> sgArtist song+ Album -> sgAlbum song+ Title -> sgTitle song+ Track -> show $ fst $ sgTrack song+ Name -> sgName song+ Genre -> sgGenre song+ Date -> show $ sgDate song+ Composer -> sgComposer song+ Performer -> sgPerformer song+ Disc -> show $ fromJust $ sgDisc song+ Filename -> sgFilePath song+ Any -> "Foo" -testFindComplex =- test [("find Artist \"Foo\" Album \"Bar\"", Right resp)] (Right [obj])- (find [Match Artist "Foo", Match Album "Bar"])- where obj = empty { sgFilePath = "dir/Foo/Bar/Baz.ogg", sgArtist = "Foo"- , sgAlbum = "Bar", sgTitle = "Baz" }- resp = display obj ++ "OK"+prop_lsFiles :: [Song] -> Bool+prop_lsFiles ss = result == Ok+ where+ result =+ testMPD [("lsinfo \"\"", Right (concatMap display ss ++ "OK"))]+ (Right (map sgFilePath ss))+ ""+ (lsFiles "") +prop_lsDirs :: [Path] -> QC.Property+prop_lsDirs ds = all (all goodChar) ds ==> result == Ok+ where+ goodChar c = ('\n' /= c) && (isPrint c)+ asDir d = "directory: " ++ d ++ "\n"+ result =+ testMPD [("lsinfo \"\"", Right (concatMap asDir ds ++ "OK"))]+ (Right $ map (dropWhile isSpace) ds)+ ""+ (lsDirs "")+ testListNothing = test [("list Title", Right "Title: Foo\nTitle: Bar\nOK")] (Right ["Foo", "Bar"])- (list Title [])+ (list Title anything) testListJust = test [("list Title Artist \"Muzz\"", Right "Title: Foo\nOK")] (Right ["Foo"])- (list Title [Match Artist "Muzz"])+ (list Title (Artist =? "Muzz")) testListAll = test [("listall \"\"", Right "directory: FooBand\n\@@ -178,25 +227,28 @@ (listAll "") testLsInfo =- test [("lsinfo \"\"", Right "directory: Foo\ndirectory: Bar\nOK")]- (Right [Left "Bar", Left "Foo"])+ test [("lsinfo \"\"",+ Right $ "directory: Foo\n" ++ display song ++ "playlist: Quux\nOK")]+ (Right [Left "Foo", Right song]) (lsInfo "")+ where+ song = empty { sgFilePath = "Bar.ogg" } testListAllInfo = test [("listallinfo \"\"", Right "directory: Foo\ndirectory: Bar\nOK")]- (Right [Left "Bar", Left "Foo"])+ (Right [Left "Foo", Left "Bar"]) (listAllInfo "") testSearch = test [("search Artist \"oo\"", Right resp)] (Right [obj])- (search [Match Artist "oo"])+ (search (Artist =? "oo")) where obj = empty { sgArtist = "Foo", sgTitle = "Bar" , sgFilePath = "dir/Foo-Bar.ogg", sgLength = 60 } resp = display obj ++ "OK" testCount = test [("count Title \"Foo\"", Right resp)] (Right obj)- (count [Match Title "Foo"])+ (count (Title =? "Foo")) where obj = Count 1 60 resp = display obj ++ "OK" @@ -208,22 +260,22 @@ test [("add \"foo\"", Right "OK"), ("listall \"foo\"", Right "file: Foo\nfile: Bar\nOK")] (Right ["Foo", "Bar"])- (add "" "foo")+ (add "foo") -testAdd_ = test_ [("add \"foo\"", Right "OK")] (add_ "" "foo")+testAdd_ = test_ [("add \"foo\"", Right "OK")] (add_ "foo") testAdd_pl = test_ [("playlistadd \"foo\" \"bar\"", Right "OK")]- (add_ "foo" "bar")+ (playlistAdd_ "foo" "bar") testAddId = test [("addid \"dir/Foo-Bar.ogg\"", Right "Id: 20\nOK")] (Right 20)- (addId "dir/Foo-Bar.ogg")+ (addId "dir/Foo-Bar.ogg" Nothing) testClearPlaylist = test_ [("playlistclear \"foo\"", Right "OK")]- (clear "foo")+ (playlistClear "foo") -testClearCurrent = test_ [("clear", Right "OK")] (clear "")+testClearCurrent = test_ [("clear", Right "OK")] clear testPlChangesPosId_0 = test [("plchangesposid 10", Right "OK")]@@ -235,15 +287,14 @@ (Right [(Pos 0, ID 20)]) (plChangesPosId 10) -testPlChangesPosId_Wierd =+testPlChangesPosId_Weird = test [("plchangesposid 10", Right "cpos: foo\nId: bar\nOK")] (Left $ Unexpected "[(\"cpos\",\"foo\"),(\"Id\",\"bar\")]") (plChangesPosId 10) testCurrentSongStopped =- test [("status", Right resp)] (Right Nothing)- (currentSong)- where obj = empty { stState = Stopped, stPlaylistVersion = 253 }+ test [("status", Right resp)] (Right Nothing) currentSong+ where obj = empty { stState = Stopped, stPlaylistVersion = 253 } resp = display obj ++ "OK" testCurrentSongPlaying =@@ -262,19 +313,19 @@ , stState = Playing } resp2 = display estatus ++ "OK" -testDelete0 = test_ [("delete 1", Right "OK")] (delete "" (Pos 1))+testDelete0 = test_ [("delete 1", Right "OK")] (delete (Pos 1)) -testDelete1 = test_ [("deleteid 1", Right "OK")] (delete "" (ID 1))+testDelete1 = test_ [("deleteid 1", Right "OK")] (delete (ID 1)) -testDelete2 = test_ [("playlistdelete \"foo\" 1", Right "OK")] (delete "foo" (Pos 1))+testDelete2 = test_ [("playlistdelete \"foo\" 1", Right "OK")] (playlistDelete "foo" 1) testLoad = test_ [("load \"foo\"", Right "OK")] (load "foo") -testMove0 = test_ [("move 1 2", Right "OK")] (move "" (Pos 1) 2)+testMove0 = test_ [("move 1 2", Right "OK")] (move (Pos 1) 2) -testMove1 = test_ [("moveid 1 2", Right "OK")] (move "" (ID 1) 2)+testMove1 = test_ [("moveid 1 2", Right "OK")] (move (ID 1) 2) -testMove2 = test_ [("playlistmove \"foo\" 1 2", Right "OK")] (move "foo" (Pos 1) 2)+testMove2 = test_ [("playlistmove \"foo\" 1 2", Right "OK")] (playlistMove "foo" 1 2) testRm = test_ [("rm \"foo\"", Right "OK")] (rm "foo") @@ -286,7 +337,7 @@ testSwap1 = test_ [("swapid 1 2", Right "OK")] (swap (ID 1) (ID 2)) -testShuffle = test_ [("shuffle", Right "OK")] shuffle+testShuffle = test_ [("shuffle", Right "OK")] (shuffle Nothing) testPlaylistInfo0 = test [("playlistinfo", Right resp)] (Right [obj]) (playlistInfo Nothing)@@ -295,13 +346,13 @@ resp = display obj ++ "OK" testPlaylistInfoPos = test [("playlistinfo 1", Right resp)] (Right [obj])- (playlistInfo . Just $ Pos 1)+ (playlistInfo (Just (Left (Pos 1)))) where obj = empty { sgFilePath = "dir/Foo-Bar.ogg", sgLength = 60 , sgArtist = "Foo", sgTitle = "Bar" } resp = display obj ++ "OK" testPlaylistInfoId = test [("playlistid 1", Right resp)] (Right [obj])- (playlistInfo . Just $ ID 1)+ (playlistInfo (Just (Left (ID 1)))) where obj = empty { sgFilePath = "dir/Foo-Bar.ogg", sgLength = 60 , sgArtist = "Foo", sgTitle = "Bar" } resp = display obj ++ "OK"@@ -335,13 +386,13 @@ testPlaylistFind = test [("playlistfind Artist \"Foo\"", Right resp)] (Right [obj])- (playlistFind [Match Artist "Foo"])+ (playlistFind (Artist =? "Foo")) where obj = empty { sgFilePath = "dir/Foo/Bar.ogg", sgArtist = "Foo" } resp = display obj ++ "OK" testPlaylistSearch = test [("playlistsearch Artist \"Foo\"", Right resp)] (Right [obj])- (playlistSearch [Match Artist "Foo"])+ (playlistSearch (Artist =? "Foo")) where obj = empty { sgFilePath = "dir/Foo/Bar.ogg", sgArtist = "Foo" } resp = display obj ++ "OK" @@ -381,8 +432,10 @@ testSetVolume = test_ [("setvol 10", Right "OK")] (setVolume 10) -testVolume = test_ [("volume 10", Right "OK")] (volume 10)+testConsume = test_ [("consume 1", Right "OK")] (consume True) +testSingle = test_ [("single 1", Right "OK")] (single True)+ -- -- Miscellaneous commands --@@ -411,6 +464,27 @@ testPassword = test_ [("password foo", Right "OK")] (password "foo") +testPasswordSucceeds =+ putStrLn . showResult $ testMPD convo expected_resp "foo" cmd+ where+ convo = [("lsinfo \"/\"", Right "ACK [4@0] {play} you don't have \+ \permission for \"play\"")+ ,("password foo", Right "OK")+ ,("lsinfo \"/\"", Right "directory: /bar\nOK")]+ expected_resp = Right [Left "/bar"]+ cmd = lsInfo "/"++testPasswordFails =+ putStrLn . showResult $ testMPD convo expected_resp "foo" cmd+ where+ convo = [("play", Right "ACK [4@0] {play} you don't have \+ \permission for \"play\"")+ ,("password foo",+ Right "ACK [3@0] {password} incorrect password")]+ expected_resp =+ Left $ ACK InvalidPassword " incorrect password"+ cmd = play Nothing+ testPing = test_ [("ping", Right "OK")] ping testStats = test [("stats", Right resp)] (Right obj) stats@@ -457,3 +531,8 @@ testDeleteMany1 = test_ [("playlistdelete \"foo\" 1", Right "OK")] (deleteMany "foo" [Pos 1])++testVolume = test_ [("status", Right st), ("setvol 90", Right "OK")] (volume (-10))+ where st = display empty { stVolume = 100 }++
tests/Displayable.hs view
@@ -1,7 +1,7 @@ module Displayable (Displayable(..)) where +import Network.MPD.Commands.Types import Network.MPD.Utils-import Network.MPD.Types -- | A uniform interface for types that -- can be turned into raw responses@@ -11,23 +11,20 @@ -- string instance Displayable Count where- empty = Count { cSongs = 0, cPlaytime = 0 }- display s = unlines $+ empty = defaultCount+ display s = unlines ["songs: " ++ show (cSongs s) ,"playtime: " ++ show (cPlaytime s)] instance Displayable Device where- empty = Device 0 "" False- display d = unlines $+ empty = defaultDevice+ display d = unlines ["outputid: " ++ show (dOutputID d) ,"outputname: " ++ dOutputName d ,"outputenabled: " ++ showBool (dOutputEnabled d)] instance Displayable Song where- empty = Song { sgArtist = "", sgAlbum = "", sgTitle = "", sgFilePath = ""- , sgGenre = "", sgName = "", sgComposer = ""- , sgPerformer = "", sgLength = 0, sgDate = 0- , sgTrack = (0,0), sgDisc = (0,0), sgIndex = Nothing }+ empty = defaultSong display s = unlines $ ["file: " ++ sgFilePath s ,"Artist: " ++ sgArtist s@@ -39,15 +36,14 @@ ,"Performer: " ++ sgPerformer s ,"Date: " ++ show (sgDate s) ,"Track: " ++ (let (x,y) = sgTrack s in show x++"/"++show y)- ,"Disc: " ++ (let (x,y) = sgDisc s in show x++"/"++show y)+ ,"Disc: " ++ (case sgDisc s of Just (x,y) -> show x++"/"++show y; _ -> "") ,"Time: " ++ show (sgLength s)] ++ maybe [] (\x -> [case x of Pos n -> "Pos: " ++ show n ID n -> "Id: " ++ show n]) (sgIndex s) instance Displayable Stats where- empty = Stats { stsArtists = 0, stsAlbums = 0, stsSongs = 0, stsUptime = 0- , stsPlaytime = 0, stsDbPlaytime = 0, stsDbUpdate = 0 }- display s = unlines $+ empty = defaultStats+ display s = unlines ["artists: " ++ show (stsArtists s) ,"albums: " ++ show (stsAlbums s) ,"songs: " ++ show (stsSongs s)@@ -57,12 +53,7 @@ ,"db_update: " ++ show (stsDbUpdate s)] instance Displayable Status where- empty = Status { stState = Stopped, stVolume = 0, stRepeat = False- , stRandom = False, stPlaylistVersion = 0- , stPlaylistLength = 0, stSongPos = Nothing- , stSongID = Nothing, stTime = (0, 0), stBitrate = 0- , stXFadeWidth = 0, stAudio = (0, 0, 0)- , stUpdatingDb = 0, stError = "" }+ empty = defaultStatus display s = unlines $ ["state: " ++ (case stState s of Playing -> "play" Paused -> "pause"@@ -80,7 +71,9 @@ ,"audio: " ++ (let (x, y, z) = stAudio s in show x ++ ":" ++ show y ++ ":" ++ show z) ,"updating_db: " ++ show (stUpdatingDb s)- ,"error: " ++ show (stError s)]+ ,"error: " ++ show (stError s)+ ,"single: " ++ showBool (stSingle s)+ ,"consume: " ++ showBool (stConsume s)] ++ maybe [] (\x -> [case x of Pos n -> "song: " ++ show n _ -> undefined]) (stSongPos s) ++ maybe [] (\x -> [case x of ID n -> "songid: " ++ show n
tests/Main.hs view
@@ -4,4 +4,9 @@ import qualified Commands import qualified Properties -main = Properties.main >> Commands.main+main = do+ putStrLn "*** Properties ***"+ Properties.main++ putStrLn "\n*** Unit Tests ***"+ Commands.main
tests/Properties.hs view
@@ -1,14 +1,14 @@ {-# OPTIONS_GHC -fno-warn-orphans -fno-warn-missing-methods #-} module Properties (main) where +import Arbitrary import Displayable -import Network.MPD.Parse-import Network.MPD.Types+import Network.MPD.Commands.Parse+import Network.MPD.Commands.Types import Network.MPD.Utils import Control.Monad-import Data.Char import Data.List import Data.Maybe import System.Environment@@ -29,8 +29,6 @@ ,("showBool", mytest prop_showBool) ,("toAssoc / reversible", mytest prop_toAssoc_rev)- ,("toAssoc / integrity",- mytest prop_toAssoc_integrity) ,("parseNum", mytest prop_parseNum) ,("parseDate / simple", mytest prop_parseDate_simple)@@ -42,82 +40,30 @@ ,("parseStats", mytest prop_parseStats)] mytest :: Testable a => a -> Int -> IO ()-mytest a n = check defaultConfig { configMaxTest = n } a--instance Arbitrary Char where- arbitrary = choose ('\0', '\128')---- an assoc. string is a string of the form "key: value".-newtype AssocString = AS String- deriving Show--instance Arbitrary AssocString where- arbitrary = do- key <- arbitrary- val <- arbitrary- return . AS $ key ++ ": " ++ val--newtype IntegralString = IS String- deriving Show--instance Arbitrary IntegralString where- arbitrary = fmap (IS . show) (arbitrary :: Gen Integer)--newtype BoolString = BS String- deriving Show--instance Arbitrary BoolString where- arbitrary = fmap BS $ elements ["1", "0"]---- Positive integers.-newtype PosInt = PI Integer--instance Show PosInt where- show (PI x) = show x--instance Arbitrary PosInt where- arbitrary = (PI . abs) `fmap` arbitrary---- Simple date representation, like "2004" and "1998".-newtype SimpleDateString = SDS String- deriving Show--instance Arbitrary SimpleDateString where- arbitrary = (SDS . show) `fmap` (arbitrary :: Gen PosInt)---- Complex date representations, like "2004-20-30".-newtype ComplexDateString = CDS String- deriving Show--instance Arbitrary ComplexDateString where- arbitrary = do- -- eww...- [y,m,d] <- replicateM 3 (arbitrary :: Gen PosInt)- return . CDS . concat . intersperse "-" $ map show [y,m,d]--prop_parseDate_simple :: SimpleDateString -> Bool-prop_parseDate_simple (SDS x) = isJust $ parseDate x--prop_parseDate_complex :: ComplexDateString -> Bool-prop_parseDate_complex (CDS x) = isJust $ parseDate x+mytest a n = quickCheckWith stdArgs { maxSize = n } a -prop_toAssoc_rev :: [AssocString] -> Bool-prop_toAssoc_rev x = toAssoc (fromAssoc r) == r- where r = toAssoc (fromAS x)- fromAssoc = map (\(a, b) -> a ++ ": " ++ b)+prop_parseDate_simple :: YearString -> Bool+prop_parseDate_simple (YS x) = isJust $ parseDate x -prop_toAssoc_integrity :: [AssocString] -> Bool-prop_toAssoc_integrity x = length (toAssoc $ fromAS x) == length x+prop_parseDate_complex :: DateString -> Bool+prop_parseDate_complex (DS x) = isJust $ parseDate x -fromAS :: [AssocString] -> [String]-fromAS s = [x | AS x <- s]+-- Conversion to an association list.+prop_toAssoc_rev :: AssocString -> Bool+prop_toAssoc_rev x = k == k' && v == v'+ where+ AS str k v = x+ (k',v') = toAssoc str prop_parseBool_rev :: BoolString -> Bool prop_parseBool_rev (BS x) = showBool (fromJust $ parseBool x) == x prop_parseBool :: BoolString -> Bool-prop_parseBool (BS "1") = fromJust $ parseBool "1"-prop_parseBool (BS x) = not (fromJust $ parseBool x)+prop_parseBool (BS xs) =+ case parseBool xs of+ Nothing -> False+ Just True -> xs == "1"+ Just False -> xs == "0" prop_showBool :: Bool -> Bool prop_showBool True = showBool True == "1"@@ -133,56 +79,27 @@ prop_splitGroups_integrity xs = not (null xs) ==> sort (concat $ splitGroups [(fst $ head xs, id)] xs) == sort xs -prop_parseNum :: IntegralString -> Bool-prop_parseNum (IS xs@"") = parseNum xs == Nothing-prop_parseNum (IS xs@('-':_)) = fromMaybe 0 (parseNum xs) <= 0-prop_parseNum (IS xs) = fromMaybe 0 (parseNum xs) >= 0+prop_parseNum :: Integer -> Bool+prop_parseNum x =+ case show x of+ (xs@"") -> parseNum xs == Nothing+ (xs@('-':_)) -> fromMaybe 0 (parseNum xs) <= 0+ (xs) -> fromMaybe 0 (parseNum xs) >= 0 -------------------------------------------------------------------------- -- Parsers -------------------------------------------------------------------------- --- MPD fields can't contain newlines and the parser skips initial spaces.-field :: Gen String-field = (filter (/= '\n') . dropWhile isSpace) `fmap` arbitrary--instance Arbitrary Count where- arbitrary = liftM2 Count arbitrary arbitrary- prop_parseCount :: Count -> Bool prop_parseCount c = Right c == (parseCount . lines $ display c) -instance Arbitrary Device where- arbitrary = liftM3 Device arbitrary field arbitrary- prop_parseOutputs :: [Device] -> Bool prop_parseOutputs ds = Right ds == (parseOutputs . lines $ concatMap display ds) -instance Arbitrary Song where- arbitrary = do- [file,artist,album,title,genre,name,cmpsr,prfmr] <- replicateM 8 field- date <- abs `fmap` arbitrary- len <- abs `fmap` arbitrary- track <- two $ abs `fmap` arbitrary- disc <- two $ abs `fmap` arbitrary- idx <- oneof [return Nothing- ,liftM (Just . Pos) $ abs `fmap` arbitrary- ,liftM (Just . ID) $ abs `fmap` arbitrary]- return $ Song { sgArtist = artist, sgAlbum = album, sgTitle = title- , sgFilePath = file, sgGenre = genre, sgName = name- , sgComposer = cmpsr, sgPerformer = prfmr, sgLength = len- , sgDate = date, sgTrack = track, sgDisc = disc- , sgIndex = idx }- prop_parseSong :: Song -> Bool-prop_parseSong s = Right s == (parseSong . toAssoc . lines $ display s)--instance Arbitrary Stats where- arbitrary = do- [arts,albs,sngs,upt,plt,dbplt,dbupd] <- replicateM 7 (fmap abs $ arbitrary)- return $ Stats arts albs sngs upt plt dbplt dbupd+prop_parseSong s = Right s == (parseSong . toAssocList . lines $ display s) prop_parseStats :: Stats -> Bool prop_parseStats s = Right s == (parseStats . lines $ display s)
− tests/coverage
@@ -1,16 +0,0 @@-#!/bin/sh--# Generate code coverage reports with hpc.-# Needs to be run from within './tests'.-# Non-relevant modules are excluded from the final-# reports.--rm -f *.tix-mkdir -p build report-ghc -i.. -fhpc --make Main.hs -odir=build -hidir=build && \- ./Main && \- hpc markup Main --destdir=report --exclude=Main --exclude=Properties \- --exclude=Displayable --exclude=Commands \- --exclude=StringConn && \- hpc report Main --exclude=Main --exclude=Properties --exclude=Commands \- --exclude=Displayable --exclude=StringConn
+ tests/coverage.lhs view
@@ -0,0 +1,40 @@+#!/usr/bin/env runhaskell++Generate coverage reports with HPC.+Usage: runhaskell coverage.lhs++\begin{code}+import Control.Monad (when)+import System.Cmd (system)+import System.Exit (ExitCode(..), exitWith)+import System.FilePath ((</>))+import System.Directory (doesDirectoryExist, getDirectoryContents, removeDirectory, removeFile)++excludeModules =+ ["Main", "Arbitrary", "Properties", "Displayable", "Commands", "StringConn"]++main = do+ -- Cleanup from previous runs+ hpcExists <- doesDirectoryExist ".hpc"+ when hpcExists $ do+ cs <- getDirectoryContents ".hpc"+ let fs = map (".hpc" </>) (filter (`notElem` [".", ".."]) cs)+ mapM_ removeFile fs+ removeDirectory ".hpc"+ removeFile "test.tix"++ -- Build and generate coverage report+ run "runhaskell Setup clean"+ run "runhaskell Setup configure -f test -f coverage --disable-optimization --user"+ run "runhaskell Setup build"+ run "dist/build/test/test"+ run ("hpc markup test.tix --destdir=report " ++ exclude)+ run ("hpc report test.tix " ++ exclude)+ where+ exclude = unwords (map ("--exclude=" ++) excludeModules)++run x = system x >>= catchFailure+ where+ catchFailure (ExitFailure _) = exitWith (ExitFailure 1)+ catchFailure _ = return ()+\end{code}
− tests/run-tests
@@ -1,15 +0,0 @@-#!/bin/sh--# Run tests and signal error on failure.-# Saves output in 'test.log', unless no failures are detected.--runhaskell -itests tests/Main.hs | tee test.log-if $(grep Falsifiable test.log >/dev/null) \- || $(grep "\*\*\* FAILURE \*\*\*" test.log >/dev/null) ; then- #echo "Failure detected!"- exit 1-else- rm test.log-fi-runhaskell Setup configure --disable-optimization -f test && \-runhaskell Setup build && runhaskell Setup haddock
+ tests/run-tests.lhs view
@@ -0,0 +1,50 @@+#!/usr/bin/env runhaskell++Compile and run tests.+Usage: runhaskell run-tests.lhs [arguments]++\begin{code}+import Control.Monad (when)+import Data.Maybe (isJust)+import System.Directory (removeFile)+import System.Environment (getArgs)+import System.Exit (ExitCode(..), exitWith)+import System.IO (hGetContents)+import System.Process+import Text.Regex (matchRegex, mkRegex)+++main = do+ -- Build the test binary+ run "runhaskell Setup configure -f test --disable-optimization --user"+ run "runhaskell Setup build"++ -- Generate library haddocks+ run "runhaskell Setup configure --user"+ run "runhaskell Setup haddock --hyperlink"++ -- Run test suite and save output to test.log+ args <- getArgs+ (_, oh, _, ph) <- runInteractiveProcess "dist/build/test/test" args+ Nothing Nothing+ waitForProcess ph+ result <- hGetContents oh+ putStr result+ writeFile "test.log" result++ -- Detect failure+ let re = mkRegex "Falsifiable|\\*\\*\\* FAILURE \\*\\*\\*"+ when (isJust $ matchRegex re result) $ do+ putStrLn "Failure detected: see test.log"+ exitWith (ExitFailure 1)++ -- Cleanup+ removeFile "test.log"++-- A wrapper for 'system' that halts the program if the command fails.+run x = system x >>= catchFailure+ where+ catchFailure (ExitFailure _) = exitWith (ExitFailure 1)+ catchFailure _ = return ()++\end{code}