2 libmpd for Haskell, an MPD client library.
3 Copyright (C) 2005-2008 Ben Sinclair <bsinclai@turing.une.edu.au>
5 This library is free software; you can redistribute it and/or
6 modify it under the terms of the GNU Lesser General Public
7 License as published by the Free Software Foundation; either
8 version 2.1 of the License, or (at your option) any later version.
10 This library is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 Lesser General Public License for more details.
15 You should have received a copy of the GNU Lesser General Public
16 License along with this library; if not, write to the Free Software
17 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
20 -- | Module : Network.MPD.Commands
21 -- Copyright : (c) Ben Sinclair 2005-2008
23 -- Maintainer : bsinclai@turing.une.edu.au
25 -- Portability : Haskell 98
27 -- Interface to the user commands supported by MPD.
29 module Network
.MPD
.Commands
(
30 -- * Command related data types
31 State
(..), Status
(..), Stats
(..),
34 Artist
, Album
, Title
, Seconds
, PlaylistName
, Path
,
35 PLIndex
(..), Song
(..), Count
(..),
38 disableOutput
, enableOutput
, kill
, outputs
, update
,
40 -- * Database commands
41 find, list, listAll
, listAllInfo
, lsInfo
, search
, count
,
43 -- * Playlist commands
45 add
, add_
, addId
, clear
, currentSong
, delete, load
, move
,
46 playlistInfo
, listPlaylist
, listPlaylistInfo
, playlist
, plChanges
,
47 plChangesPosId
, playlistFind
, playlistSearch
, rm
, rename
, save
, shuffle
,
50 -- * Playback commands
51 crossfade
, next, pause
, play
, previous
, random, repeat, seek
, setVolume
,
54 -- * Miscellaneous commands
55 clearError
, close
, commands
, notCommands
, password
, ping
, reconnect
, stats
,
56 status
, tagTypes
, urlHandlers
,
58 -- * Extensions\/shortcuts
59 addMany
, deleteMany
, complete
, crop
, prune
, lsDirs
, lsFiles
, lsPlaylists
,
60 findArtist
, findAlbum
, findTitle
, listArtists
, listAlbums
, listAlbum
,
61 searchArtist
, searchAlbum
, searchTitle
, getPlaylist
, toggle
, updateId
64 import Network
.MPD
.Core
65 import Network
.MPD
.Utils
67 import Control
.Monad
(foldM, liftM, unless)
68 import Control
.Monad
.Error
(throwError
)
69 import Prelude
hiding (repeat)
70 import Data
.List
(findIndex, intersperse, isPrefixOf)
72 import System
.FilePath (dropFileName
)
81 type Seconds
= Integer
83 -- | Used for commands which require a playlist name.
84 -- If empty, the current playlist is used.
85 type PlaylistName
= String
87 -- | Used for commands which require a path within the database.
88 -- If empty, the root path is used.
91 -- | Available metadata types\/scope modifiers, used for searching the
92 -- database for entries with certain metadata values.
93 data Meta
= Artist | Album | Title | Track | Name | Genre | Date
94 | Composer | Performer | Disc | Any | Filename
96 instance Show Meta
where
97 show Artist
= "Artist"
104 show Composer
= "Composer"
105 show Performer
= "Performer"
108 show Filename
= "Filename"
110 -- | A query is composed of a scope modifier and a query string.
112 -- To match entries where album equals \"Foo\", use:
114 -- > Query Album "Foo"
116 -- To match entries where album equals \"Foo\" and artist equals \"Bar\", use:
118 -- > MultiQuery [Query Album "Foo", Query Artist "Bar"]
119 data Query
= Query Meta
String -- ^ Simple query.
120 | MultiQuery
[Query
] -- ^ Query with multiple conditions.
122 instance Show Query
where
123 show (Query meta query
) = show meta
++ " " ++ show query
124 show (MultiQuery xs
) = show xs
125 showList xs _
= unwords $ map show xs
127 -- | Represents a song's playlist index.
128 data PLIndex
= Pos
Integer -- ^ A playlist position index (starting from 0)
129 | ID
Integer -- ^ A playlist ID number that more robustly
130 -- identifies a song.
133 -- | Represents the different playback states.
139 -- | Container for MPD status.
141 Status
{ stState
:: State
142 -- | A percentage (0-100)
146 -- | A value that is incremented by the server every time the
148 , stPlaylistVersion
:: Integer
149 -- | The number of items in the current playlist.
150 , stPlaylistLength
:: Integer
151 -- | Current song's position in the playlist.
152 , stSongPos
:: Maybe PLIndex
153 -- | Current song's playlist ID.
154 , stSongID
:: Maybe PLIndex
155 -- | Time elapsed\/total time.
156 , stTime
:: (Seconds
, Seconds
)
157 -- | Bitrate (in kilobytes per second) of playing song (if any).
160 , stXFadeWidth
:: Seconds
161 -- | Samplerate\/bits\/channels for the chosen output device
163 , stAudio
:: (Int, Int, Int)
164 -- | Job ID of currently running update (if any).
165 , stUpdatingDb
:: Integer
166 -- | Last error message (if any).
167 , stError
:: String }
170 -- | Container for database statistics.
172 Stats
{ stsArtists
:: Integer -- ^ Number of artists.
173 , stsAlbums
:: Integer -- ^ Number of albums.
174 , stsSongs
:: Integer -- ^ Number of songs.
175 , stsUptime
:: Seconds
-- ^ Daemon uptime in seconds.
176 , stsPlaytime
:: Seconds
-- ^ Total playing time.
177 , stsDbPlaytime
:: Seconds
-- ^ Total play time of all the songs in
179 , stsDbUpdate
:: Integer -- ^ Last database update in UNIX time.
183 -- | Represents a single song item.
185 Song
{ sgArtist
, sgAlbum
, sgTitle
, sgFilePath
, sgGenre
, sgName
, sgComposer
186 , sgPerformer
:: String
187 , sgLength
:: Seconds
-- ^ Length in seconds
188 , sgDate
:: Int -- ^ Year
189 , sgTrack
:: (Int, Int) -- ^ Track number\/total tracks
190 , sgDisc
:: (Int, Int) -- ^ Position in set\/total in set
191 , sgIndex
:: Maybe PLIndex
}
194 -- Avoid the need for writing a proper 'elem' for use in 'prune'.
195 instance Eq Song
where
196 (==) x y
= sgFilePath x
== sgFilePath y
198 -- | Represents the result of running 'count'.
200 Count
{ cSongs
:: Integer -- ^ Number of songs matching the query
201 , cPlaytime
:: Seconds
-- ^ Total play time of matching songs
205 -- | Represents an output device.
207 Device
{ dOutputID
:: Int -- ^ Output's ID number
208 , dOutputName
:: String -- ^ Output's name as defined in the MPD
209 -- configuration file
210 , dOutputEnabled
:: Bool }
217 -- | Turn off an output device.
218 disableOutput
:: Int -> MPD
()
219 disableOutput
= getResponse_
. ("disableoutput " ++) . show
221 -- | Turn on an output device.
222 enableOutput
:: Int -> MPD
()
223 enableOutput
= getResponse_
. ("enableoutput " ++) . show
225 -- | Retrieve information for all output devices.
226 outputs
:: MPD
[Device
]
227 outputs
= liftM (map takeDevInfo
. splitGroups
. toAssoc
)
228 (getResponse
"outputs")
230 takeDevInfo xs
= Device
{
231 dOutputID
= takeNum
"outputid" xs
,
232 dOutputName
= takeString
"outputname" xs
,
233 dOutputEnabled
= takeBool
"outputenabled" xs
236 -- | Update the server's database.
237 -- If no paths are given, all paths will be scanned.
238 -- Unreadable or non-existent paths are silently ignored.
239 update
:: [Path
] -> MPD
()
240 update
[] = getResponse_
"update"
241 update
[x
] = getResponse_
("update " ++ show x
)
242 update xs
= getResponses
(map (("update " ++) . show) xs
) >> return ()
248 -- | List all metadata of metadata (sic).
249 list :: Meta
-- ^ Metadata to list
250 -> Maybe Query
-> MPD
[String]
251 list mtype query
= liftM takeValues
(getResponse cmd
)
252 where cmd
= "list " ++ show mtype
++ maybe "" ((" "++) . show) query
254 -- | Non-recursively list the contents of a database directory.
255 lsInfo
:: Path
-> MPD
[Either Path Song
]
256 lsInfo
= lsInfo
' "lsinfo"
258 -- | List the songs (without metadata) in a database directory recursively.
259 listAll
:: Path
-> MPD
[Path
]
260 listAll path
= liftM (map snd . filter ((== "file") . fst) . toAssoc
)
261 (getResponse
("listall " ++ show path
))
263 -- | Recursive 'lsInfo'.
264 listAllInfo
:: Path
-> MPD
[Either Path Song
]
265 listAllInfo
= lsInfo
' "listallinfo"
267 -- Helper for lsInfo and listAllInfo.
268 lsInfo
' :: String -> Path
-> MPD
[Either Path Song
]
269 lsInfo
' cmd path
= do
270 (dirs
,_
,songs
) <- takeEntries
=<< getResponse
(cmd
++ " " ++ show path
)
271 return (map Left dirs
++ map Right songs
)
273 -- | Search the database for entries exactly matching a query.
274 find :: Query
-> MPD
[Song
]
275 find query
= getResponse
("find " ++ show query
) >>= takeSongs
277 -- | Search the database using case insensitive matching.
278 search
:: Query
-> MPD
[Song
]
279 search query
= getResponse
("search " ++ show query
) >>= takeSongs
281 -- | Count the number of entries matching a query.
282 count
:: Query
-> MPD Count
283 count query
= liftM (takeCountInfo
. toAssoc
)
284 (getResponse
("count " ++ show query
))
285 where takeCountInfo xs
= Count
{ cSongs
= takeNum
"songs" xs
,
286 cPlaytime
= takeNum
"playtime" xs
}
292 -- Unless otherwise noted all playlist commands operate on the current
295 -- This might do better to throw an exception than silently return 0.
296 -- | Like 'add', but returns a playlist id.
297 addId
:: Path
-> MPD
Integer
298 addId
= liftM (takeNum
"Id" . toAssoc
) . getResponse
. ("addid " ++) . show
300 -- | Like 'add_' but returns a list of the files added.
301 add
:: PlaylistName
-> Path
-> MPD
[Path
]
302 add plname x
= add_ plname x
>> listAll x
304 -- | Add a song (or a whole directory) to a playlist.
305 -- Adds to current if no playlist is specified.
306 -- Will create a new playlist if the one specified does not already exist.
307 add_
:: PlaylistName
-> Path
-> MPD
()
308 add_
"" = getResponse_
. ("add " ++) . show
309 add_ plname
= getResponse_
.
310 (("playlistadd " ++ show plname
++ " ") ++) . show
312 -- | Clear a playlist. Clears current playlist if no playlist is specified.
313 -- If the specified playlist does not exist, it will be created.
314 clear
:: PlaylistName
-> MPD
()
315 clear
= getResponse_
. cmd
316 where cmd
"" = "clear"
317 cmd pl
= "playlistclear " ++ show pl
319 -- | Remove a song from a playlist.
320 -- If no playlist is specified, current playlist is used.
321 -- Note that a playlist position ('Pos') is required when operating on
322 -- playlists other than the current.
323 delete :: PlaylistName
-> PLIndex
-> MPD
()
324 delete "" (Pos x
) = getResponse_
("delete " ++ show x
)
325 delete "" (ID x
) = getResponse_
("deleteid " ++ show x
)
326 delete plname
(Pos x
) =
327 getResponse_
("playlistdelete " ++ show plname
++ " " ++ show x
)
328 delete _ _
= fail "'delete' within a playlist doesn't accept a playlist ID"
330 -- | Load an existing playlist.
331 load
:: PlaylistName
-> MPD
()
332 load
= getResponse_
. ("load " ++) . show
334 -- | Move a song to a given position.
335 -- Note that a playlist position ('Pos') is required when operating on
336 -- playlists other than the current.
337 move
:: PlaylistName
-> PLIndex
-> Integer -> MPD
()
338 move
"" (Pos from
) to
=
339 getResponse_
("move " ++ show from
++ " " ++ show to
)
340 move
"" (ID from
) to
=
341 getResponse_
("moveid " ++ show from
++ " " ++ show to
)
342 move plname
(Pos from
) to
=
343 getResponse_
("playlistmove " ++ show plname
++ " " ++ show from
++
345 move _ _ _
= fail "'move' within a playlist doesn't accept a playlist ID"
347 -- | Delete existing playlist.
348 rm
:: PlaylistName
-> MPD
()
349 rm
= getResponse_
. ("rm " ++) . show
351 -- | Rename an existing playlist.
352 rename
:: PlaylistName
-- ^ Original playlist
353 -> PlaylistName
-- ^ New playlist name
356 getResponse_
("rename " ++ show plname
++ " " ++ show new
)
358 -- | Save the current playlist.
359 save
:: PlaylistName
-> MPD
()
360 save
= getResponse_
. ("save " ++) . show
362 -- | Swap the positions of two songs.
363 -- Note that the positions must be of the same type, i.e. mixing 'Pos' and 'ID'
364 -- will result in a no-op.
365 swap
:: PLIndex
-> PLIndex
-> MPD
()
366 swap
(Pos x
) (Pos y
) = getResponse_
("swap " ++ show x
++ " " ++ show y
)
367 swap
(ID x
) (ID y
) = getResponse_
("swapid " ++ show x
++ " " ++ show y
)
368 swap _ _
= fail "'swap' cannot mix position and ID arguments"
370 -- | Shuffle the playlist.
372 shuffle
= getResponse_
"shuffle"
374 -- | Retrieve metadata for songs in the current playlist.
375 playlistInfo
:: Maybe PLIndex
-> MPD
[Song
]
376 playlistInfo x
= getResponse cmd
>>= takeSongs
377 where cmd
= case x
of
378 Just
(Pos x
') -> "playlistinfo " ++ show x
'
379 Just
(ID x
') -> "playlistid " ++ show x
'
380 Nothing
-> "playlistinfo"
382 -- | Retrieve metadata for files in a given playlist.
383 listPlaylistInfo
:: PlaylistName
-> MPD
[Song
]
384 listPlaylistInfo plname
=
385 takeSongs
=<< (getResponse
. ("listplaylistinfo " ++) $ show plname
)
387 -- | Retrieve a list of files in a given playlist.
388 listPlaylist
:: PlaylistName
-> MPD
[Path
]
389 listPlaylist
= liftM takeValues
. getResponse
. ("listplaylist " ++) . show
391 -- | Retrieve file paths and positions of songs in the current playlist.
392 -- Note that this command is only included for completeness sake; it's
393 -- deprecated and likely to disappear at any time, please use 'playlistInfo'
395 playlist
:: MPD
[(PLIndex
, Path
)]
396 playlist
= liftM (map f
) (getResponse
"playlist")
397 where f s
= let (pos
, name
) = break (== ':') s
398 in (Pos
$ read pos
, drop 1 name
)
400 -- | Retrieve a list of changed songs currently in the playlist since
401 -- a given playlist version.
402 plChanges
:: Integer -> MPD
[Song
]
404 takeSongs
=<< (getResponse
. ("plchanges " ++) $ show version
)
406 -- | Like 'plChanges' but only returns positions and ids.
407 plChangesPosId
:: Integer -> MPD
[(PLIndex
, PLIndex
)]
408 plChangesPosId plver
=
409 liftM (map takePosid
. splitGroups
. toAssoc
) (getResponse cmd
)
410 where cmd
= "plchangesposid " ++ show plver
411 takePosid xs
= (Pos
$ takeNum
"cpos" xs
, ID
$ takeNum
"Id" xs
)
413 -- | Search for songs in the current playlist with strict matching.
414 playlistFind
:: Query
-> MPD
[Song
]
415 playlistFind q
= takeSongs
=<< (getResponse
. ("playlistfind " ++) $ show q
)
417 -- | Search case-insensitively with partial matches for songs in the
419 playlistSearch
:: Query
-> MPD
[Song
]
421 takeSongs
=<< (getResponse
. ("playlistsearch " ++) $ show q
)
423 -- | Get the currently playing song.
424 currentSong
:: MPD
(Maybe Song
)
427 if stState currStatus
== Stopped
429 else do ls
<- liftM toAssoc
(getResponse
"currentsong")
430 if null ls
then return Nothing
431 else liftM Just
(takeSongInfo ls
)
437 -- | Set crossfading between songs.
438 crossfade
:: Seconds
-> MPD
()
439 crossfade
= getResponse_
. ("crossfade " ++) . show
441 -- | Begin\/continue playing.
442 play
:: Maybe PLIndex
-> MPD
()
443 play Nothing
= getResponse_
"play"
444 play
(Just
(Pos x
)) = getResponse_
("play " ++ show x
)
445 play
(Just
(ID x
)) = getResponse_
("playid " ++ show x
)
448 pause
:: Bool -> MPD
()
449 pause
= getResponse_
. ("pause " ++) . showBool
453 stop
= getResponse_
"stop"
455 -- | Play the next song.
457 next = getResponse_
"next"
459 -- | Play the previous song.
461 previous
= getResponse_
"previous"
463 -- | Seek to some point in a song.
464 -- Seeks in current song if no position is given.
465 seek
:: Maybe PLIndex
-> Seconds
-> MPD
()
466 seek
(Just
(Pos x
)) time
=
467 getResponse_
("seek " ++ show x
++ " " ++ show time
)
468 seek
(Just
(ID x
)) time
=
469 getResponse_
("seekid " ++ show x
++ " " ++ show time
)
470 seek Nothing time
= do
472 unless (stState st
== Stopped
) (seek
(stSongID st
) time
)
474 -- | Set random playing.
475 random :: Bool -> MPD
()
476 random = getResponse_
. ("random " ++) . showBool
479 repeat :: Bool -> MPD
()
480 repeat = getResponse_
. ("repeat " ++) . showBool
482 -- | Set the volume (0-100 percent).
483 setVolume
:: Int -> MPD
()
484 setVolume
= getResponse_
. ("setvol " ++) . show
486 -- | Increase or decrease volume by a given percent, e.g.
487 -- 'volume 10' will increase the volume by 10 percent, while
488 -- 'volume (-10)' will decrease it by the same amount.
489 -- Note that this command is only included for completeness sake ; it's
490 -- deprecated and may disappear at any time, please use 'setVolume' instead.
491 volume
:: Int -> MPD
()
492 volume
= getResponse_
. ("volume " ++) . show
495 -- Miscellaneous commands
498 -- | Clear the current error message in status.
500 clearError
= getResponse_
"clearerror"
502 -- | Retrieve a list of available commands.
503 commands
:: MPD
[String]
504 commands
= liftM takeValues
(getResponse
"commands")
506 -- | Retrieve a list of unavailable (due to access restrictions) commands.
507 notCommands
:: MPD
[String]
508 notCommands
= liftM takeValues
(getResponse
"notcommands")
510 -- | Retrieve a list of available song metadata.
511 tagTypes
:: MPD
[String]
512 tagTypes
= liftM takeValues
(getResponse
"tagtypes")
514 -- | Retrieve a list of supported urlhandlers.
515 urlHandlers
:: MPD
[String]
516 urlHandlers
= liftM takeValues
(getResponse
"urlhandlers")
518 -- XXX should the password be quoted?
519 -- | Send password to server to authenticate session.
520 -- Password is sent as plain text.
521 password
:: String -> MPD
()
522 password
= getResponse_
. ("password " ++)
524 -- | Check that the server is still responding.
526 ping
= getResponse_
"ping"
528 -- | Get server statistics.
530 stats
= liftM (parseStats
. toAssoc
) (getResponse
"stats")
531 where parseStats xs
=
532 Stats
{ stsArtists
= takeNum
"artists" xs
,
533 stsAlbums
= takeNum
"albums" xs
,
534 stsSongs
= takeNum
"songs" xs
,
535 stsUptime
= takeNum
"uptime" xs
,
536 stsPlaytime
= takeNum
"playtime" xs
,
537 stsDbPlaytime
= takeNum
"db_playtime" xs
,
538 stsDbUpdate
= takeNum
"db_update" xs
}
540 -- | Get the server's status.
542 status
= liftM (parseStatus
. toAssoc
) (getResponse
"status")
543 where parseStatus xs
=
544 Status
{ stState
= maybe Stopped parseState
$ lookup "state" xs
,
545 stVolume
= takeNum
"volume" xs
,
546 stRepeat
= takeBool
"repeat" xs
,
547 stRandom
= takeBool
"random" xs
,
548 stPlaylistVersion
= takeNum
"playlist" xs
,
549 stPlaylistLength
= takeNum
"playlistlength" xs
,
550 stXFadeWidth
= takeNum
"xfade" xs
,
551 stSongPos
= takeIndex Pos
"song" xs
,
552 stSongID
= takeIndex ID
"songid" xs
,
553 stTime
= maybe (0,0) parseTime
$ lookup "time" xs
,
554 stBitrate
= takeNum
"bitrate" xs
,
555 stAudio
= maybe (0,0,0) parseAudio
$ lookup "audio" xs
,
556 stUpdatingDb
= takeNum
"updating_db" xs
,
557 stError
= takeString
"error" xs
}
558 parseState x
= case x
of "play" -> Playing
561 parseTime x
= let (y
,_
:z
) = break (== ':') x
in (read y
, read z
)
563 let (u
,_
:u
') = break (== ':') x
; (v
,_
:w
) = break (== ':') u
' in
564 (read u
, read v
, read w
)
567 -- Extensions\/shortcuts.
570 -- | Like 'update', but returns the update job id.
571 updateId
:: [Path
] -> MPD
Integer
572 updateId paths
= liftM (read . head . takeValues
) cmd
573 where cmd
= case paths
of
574 [] -> getResponse
"update"
575 [x
] -> getResponse
("update " ++ x
)
576 xs
-> getResponses
(map ("update " ++) xs
)
578 -- | Toggles play\/pause. Plays if stopped.
580 toggle
= status
>>= \st
-> case stState st
of Playing
-> pause
True
583 -- | Add a list of songs\/folders to a playlist.
584 -- Should be more efficient than running 'add' many times.
585 addMany
:: PlaylistName
-> [Path
] -> MPD
()
586 addMany _
[] = return ()
587 addMany plname
[x
] = add_ plname x
588 addMany plname xs
= getResponses
(map ((cmd
++) . show) xs
) >> return ()
589 where cmd
= case plname
of "" -> "add "
590 pl
-> "playlistadd " ++ show pl
++ " "
592 -- | Delete a list of songs from a playlist.
593 -- If there is a duplicate then no further songs will be deleted, so
594 -- take care to avoid them (see 'prune' for this).
595 deleteMany
:: PlaylistName
-> [PLIndex
] -> MPD
()
596 deleteMany _
[] = return ()
597 deleteMany plname
[x
] = delete plname x
598 deleteMany
"" xs
= getResponses
(map cmd xs
) >> return ()
599 where cmd
(Pos x
) = "delete " ++ show x
600 cmd
(ID x
) = "deleteid " ++ show x
601 deleteMany plname xs
= getResponses
(map cmd xs
) >> return ()
602 where cmd
(Pos x
) = "playlistdelete " ++ show plname
++ " " ++ show x
605 -- | Returns all songs and directories that match the given partial
607 complete
:: String -> MPD
[Either Path Song
]
609 xs
<- liftM matches
. lsInfo
$ dropFileName path
611 [Left dir
] -> complete
$ dir
++ "/"
614 matches
= filter (isPrefixOf path
. takePath
)
615 takePath
= either id sgFilePath
618 -- The bounds are inclusive.
619 -- If 'Nothing' or 'ID' is passed the cropping will leave your playlist alone
621 crop
:: Maybe PLIndex
-> Maybe PLIndex
-> MPD
()
623 pl
<- playlistInfo Nothing
624 let x
' = case x
of Just
(Pos p
) -> fromInteger p
625 Just
(ID i
) -> maybe 0 id (findByID i pl
)
627 -- ensure that no songs are deleted twice with 'max'.
628 ys
= case y
of Just
(Pos p
) -> drop (max (fromInteger p
) x
') pl
629 Just
(ID i
) -> maybe [] (flip drop pl
. max x
' . (+1))
632 deleteMany
"" . mapMaybe sgIndex
$ take x
' pl
++ ys
633 where findByID i
= findIndex ((==) i
. (\(ID j
) -> j
) . fromJust . sgIndex
)
635 -- | Remove duplicate playlist entries.
637 prune
= findDuplicates
>>= deleteMany
""
639 -- Find duplicate playlist entries.
640 findDuplicates
:: MPD
[PLIndex
]
642 liftM (map ((\(ID x
) -> ID x
) . fromJust . sgIndex
) . flip dups
([],[])) $
644 where dups
[] (_
, dup
) = dup
645 dups
(x
:xs
) (ys
, dup
)
646 | x `
elem` xs
&& x `
notElem` ys
= dups xs
(ys
, x
:dup
)
647 |
otherwise = dups xs
(x
:ys
, dup
)
649 -- | List directories non-recursively.
650 lsDirs
:: Path
-> MPD
[Path
]
651 lsDirs path
= liftM (\(x
,_
,_
) -> x
) $
652 takeEntries
=<< getResponse
("lsinfo " ++ show path
)
654 -- | List files non-recursively.
655 lsFiles
:: Path
-> MPD
[Path
]
656 lsFiles path
= liftM (map sgFilePath
. (\(_
,_
,x
) -> x
)) $
657 takeEntries
=<< getResponse
("lsinfo " ++ show path
)
659 -- | List all playlists.
660 lsPlaylists
:: MPD
[PlaylistName
]
661 lsPlaylists
= liftM (\(_
,x
,_
) -> x
) $ takeEntries
=<< getResponse
"lsinfo"
663 -- | Search the database for songs relating to an artist.
664 findArtist
:: Artist
-> MPD
[Song
]
665 findArtist
= find . Query Artist
667 -- | Search the database for songs relating to an album.
668 findAlbum
:: Album
-> MPD
[Song
]
669 findAlbum
= find . Query Album
671 -- | Search the database for songs relating to a song title.
672 findTitle
:: Title
-> MPD
[Song
]
673 findTitle
= find . Query Title
675 -- | List the artists in the database.
676 listArtists
:: MPD
[Artist
]
677 listArtists
= liftM takeValues
(getResponse
"list artist")
679 -- | List the albums in the database, optionally matching a given
681 listAlbums
:: Maybe Artist
-> MPD
[Album
]
682 listAlbums artist
= liftM takeValues
(getResponse
("list album" ++
683 maybe "" ((" artist " ++) . show) artist
))
685 -- | List the songs in an album of some artist.
686 listAlbum
:: Artist
-> Album
-> MPD
[Song
]
687 listAlbum artist album
= find (MultiQuery
[Query Artist artist
690 -- | Search the database for songs relating to an artist using 'search'.
691 searchArtist
:: Artist
-> MPD
[Song
]
692 searchArtist
= search
. Query Artist
694 -- | Search the database for songs relating to an album using 'search'.
695 searchAlbum
:: Album
-> MPD
[Song
]
696 searchAlbum
= search
. Query Album
698 -- | Search the database for songs relating to a song title.
699 searchTitle
:: Title
-> MPD
[Song
]
700 searchTitle
= search
. Query Title
702 -- | Retrieve the current playlist.
703 -- Equivalent to @playlistinfo Nothing@.
704 getPlaylist
:: MPD
[Song
]
705 getPlaylist
= playlistInfo Nothing
708 -- Miscellaneous functions.
711 -- Run getResponse but discard the response.
712 getResponse_
:: String -> MPD
()
713 getResponse_ x
= getResponse x
>> return ()
715 -- Get the lines of the daemon's response to a list of commands.
716 getResponses
:: [String] -> MPD
[String]
717 getResponses cmds
= getResponse
. concat $ intersperse "\n" cmds
'
718 where cmds
' = "command_list_begin" : cmds
++ ["command_list_end"]
720 -- Helper that throws unexpected error if input is empty.
721 failOnEmpty
:: [String] -> MPD
[String]
722 failOnEmpty
[] = throwError
$ Unexpected
"Non-empty response expected."
723 failOnEmpty xs
= return xs
725 -- A wrapper for getResponse that fails on non-empty responses.
726 getResponse1
:: String -> MPD
[String]
727 getResponse1 x
= getResponse x
>>= failOnEmpty
729 -- getResponse1 for multiple commands.
730 getResponses1
:: [String] -> MPD
[String]
731 getResponses1 cmds
= getResponses cmds
>>= failOnEmpty
737 -- Run 'toAssoc' and return only the values.
738 takeValues
:: [String] -> [String]
739 takeValues
= snd . unzip . toAssoc
741 -- Separate the result of an lsinfo\/listallinfo call into directories,
742 -- playlists, and songs.
743 takeEntries
:: [String] -> MPD
([String], [String], [Song
])
745 ss
<- mapM takeSongInfo
. splitGroups
$ reverse filedata
746 return (dirs
, playlists
, ss
)
747 where (dirs
, playlists
, filedata
) = foldl split ([], [], []) $ toAssoc s
748 split (ds
, pls
, ss
) x
@(k
, v
) | k
== "directory" = (v
:ds
, pls
, ss
)
749 | k
== "playlist" = (ds
, v
:pls
, ss
)
750 |
otherwise = (ds
, pls
, x
:ss
)
752 -- Build a list of song instances from a response.
753 takeSongs
:: [String] -> MPD
[Song
]
754 takeSongs
= mapM takeSongInfo
. splitGroups
. toAssoc
756 -- Builds a song instance from an assoc. list.
757 takeSongInfo
:: [(String, String)] -> MPD Song
758 takeSongInfo xs
= foldM f song xs
759 where f a
("Artist", x
) = return a
{ sgArtist
= x
}
760 f a
("Album", x
) = return a
{ sgAlbum
= x
}
761 f a
("Title", x
) = return a
{ sgTitle
= x
}
762 f a
("Genre", x
) = return a
{ sgGenre
= x
}
763 f a
("Name", x
) = return a
{ sgName
= x
}
764 f a
("Composer", x
) = return a
{ sgComposer
= x
}
765 f a
("Performer", x
) = return a
{ sgPerformer
= x
}
766 f a
("Date", x
) = parse parseNum
(\x
' -> a
{ sgDate
= x
'}) x
767 f a
("Track", x
) = parse parseTuple
(\x
' -> a
{ sgTrack
= x
'}) x
768 f a
("Disc", x
) = parse parseTuple
(\x
' -> a
{ sgDisc
= x
'}) x
769 f a
("file", x
) = return a
{ sgFilePath
= x
}
770 f a
("Time", x
) = parse parseNum
(\x
' -> a
{ sgLength
= x
'}) x
771 f a
("Id", x
) = parse parseNum
772 (\x
' -> a
{ sgIndex
= Just
(ID x
') }) x
774 f a
("Pos", _
) = return a
775 -- Catch unrecognised keys
776 f _ x
= throwError
(Unexpected
(show x
))
778 parseTuple s
= let (x
, y
) = break (== '/') s
in
779 case (parseNum x
, parseNum
$ drop 1 y
) of
780 (Just x
', Just y
') -> Just
(x
', y
')
783 song
= Song
{ sgArtist
= "", sgAlbum
= "", sgTitle
= ""
784 , sgGenre
= "", sgName
= "", sgComposer
= ""
785 , sgPerformer
= "", sgDate
= 0, sgTrack
= (0,0)
786 , sgDisc
= (0,0), sgFilePath
= "", sgLength
= 0
787 , sgIndex
= Nothing
}
789 -- A helper that runs a parser on a string and, depending, on the outcome,
790 -- either returns the result of some command applied to the result, or throws
791 -- an Unexpected error. Used when building structures.
792 parse
:: (String -> Maybe a
) -> (a
-> b
) -> String -> MPD b
793 parse p g x
= maybe (throwError
$ Unexpected x
) (return . g
) (p x
)
795 -- Helpers for retrieving values from an assoc. list.
797 takeNum
:: (Read a
, Integral a
) => String -> [(String, String)] -> a
798 takeNum v
= maybe 0 (fromMaybe 0 . parseNum
) . lookup v
800 takeBool
:: String -> [(String, String)] -> Bool
801 takeBool v
= maybe False parseBool
. lookup v
803 takeString
:: String -> [(String, String)] -> String
804 takeString v
= fromMaybe "" . lookup v
806 takeIndex
:: (Integer -> PLIndex
) -> String -> [(String, String)]
808 takeIndex c v
= fmap (c
. fromMaybe 0 . parseNum
) . lookup v