1 {-# LANGUAGE PatternGuards #-}
3 libmpd for Haskell, an MPD client library.
4 Copyright (C) 2005-2008 Ben Sinclair <bsinclai@turing.une.edu.au>
6 This library is free software; you can redistribute it and/or
7 modify it under the terms of the GNU Lesser General Public
8 License as published by the Free Software Foundation; either
9 version 2.1 of the License, or (at your option) any later version.
11 This library is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 Lesser General Public License for more details.
16 You should have received a copy of the GNU Lesser General Public
17 License along with this library; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
21 -- | Module : Network.MPD.Commands
22 -- Copyright : (c) Ben Sinclair 2005-2008
24 -- Maintainer : bsinclai@turing.une.edu.au
26 -- Portability : unportable (uses PatternGuards)
28 -- Interface to the user commands supported by MPD.
30 module Network
.MPD
.Commands
(
31 -- * Command related data types
32 State
(..), Status
(..), Stats
(..),
35 Artist
, Album
, Title
, Seconds
, PlaylistName
, Path
,
36 PLIndex
(..), Song
(..), Count
(..),
39 disableOutput
, enableOutput
, kill
, outputs
, update
,
41 -- * Database commands
42 find, list, listAll
, listAllInfo
, lsInfo
, search
, count
,
44 -- * Playlist commands
46 add
, add_
, addId
, clear
, currentSong
, delete, load
, move
,
47 playlistInfo
, listPlaylist
, listPlaylistInfo
, playlist
, plChanges
,
48 plChangesPosId
, playlistFind
, playlistSearch
, rm
, rename
, save
, shuffle
,
51 -- * Playback commands
52 crossfade
, next, pause
, play
, previous
, random, repeat, seek
, setVolume
,
55 -- * Miscellaneous commands
56 clearError
, close
, commands
, notCommands
, password
, ping
, reconnect
, stats
,
57 status
, tagTypes
, urlHandlers
,
59 -- * Extensions\/shortcuts
60 addMany
, deleteMany
, complete
, crop
, prune
, lsDirs
, lsFiles
, lsPlaylists
,
61 findArtist
, findAlbum
, findTitle
, listArtists
, listAlbums
, listAlbum
,
62 searchArtist
, searchAlbum
, searchTitle
, getPlaylist
, toggle
, updateId
65 import Network
.MPD
.Core
66 import Network
.MPD
.Utils
67 import Network
.MPD
.Parse
69 import Control
.Monad
(foldM, liftM, unless)
70 import Control
.Monad
.Error
(throwError
)
71 import Prelude
hiding (repeat)
72 import Data
.List
(findIndex, intercalate
, isPrefixOf)
74 import System
.FilePath (dropFileName
)
84 -- | Used for commands which require a playlist name.
85 -- If empty, the current playlist is used.
86 type PlaylistName
= String
88 -- | Used for commands which require a path within the database.
89 -- If empty, the root path is used.
92 -- | Available metadata types\/scope modifiers, used for searching the
93 -- database for entries with certain metadata values.
94 data Meta
= Artist | Album | Title | Track | Name | Genre | Date
95 | Composer | Performer | Disc | Any | Filename
98 -- | A query is composed of a scope modifier and a query string.
100 -- To match entries where album equals \"Foo\", use:
102 -- > Query Album "Foo"
104 -- To match entries where album equals \"Foo\" and artist equals \"Bar\", use:
106 -- > MultiQuery [Query Album "Foo", Query Artist "Bar"]
107 data Query
= Query Meta
String -- ^ Simple query.
108 | MultiQuery
[Query
] -- ^ Query with multiple conditions.
110 instance Show Query
where
111 show (Query meta query
) = show meta
++ " " ++ show query
112 show (MultiQuery xs
) = show xs
113 showList xs _
= unwords $ map show xs
115 -- | Represents a song's playlist index.
116 data PLIndex
= Pos
Integer -- ^ A playlist position index (starting from 0)
117 | ID
Integer -- ^ A playlist ID number that more robustly
118 -- identifies a song.
121 -- | Represents the different playback states.
127 -- | Container for MPD status.
129 Status
{ stState
:: State
130 -- | A percentage (0-100)
134 -- | A value that is incremented by the server every time the
136 , stPlaylistVersion
:: Integer
137 -- | The number of items in the current playlist.
138 , stPlaylistLength
:: Integer
139 -- | Current song's position in the playlist.
140 , stSongPos
:: Maybe PLIndex
141 -- | Current song's playlist ID.
142 , stSongID
:: Maybe PLIndex
143 -- | Time elapsed\/total time.
144 , stTime
:: (Seconds
, Seconds
)
145 -- | Bitrate (in kilobytes per second) of playing song (if any).
148 , stXFadeWidth
:: Seconds
149 -- | Samplerate\/bits\/channels for the chosen output device
151 , stAudio
:: (Int, Int, Int)
152 -- | Job ID of currently running update (if any).
153 , stUpdatingDb
:: Integer
154 -- | Last error message (if any).
155 , stError
:: String }
158 -- | Container for database statistics.
160 Stats
{ stsArtists
:: Integer -- ^ Number of artists.
161 , stsAlbums
:: Integer -- ^ Number of albums.
162 , stsSongs
:: Integer -- ^ Number of songs.
163 , stsUptime
:: Seconds
-- ^ Daemon uptime in seconds.
164 , stsPlaytime
:: Seconds
-- ^ Total playing time.
165 , stsDbPlaytime
:: Seconds
-- ^ Total play time of all the songs in
167 , stsDbUpdate
:: Integer -- ^ Last database update in UNIX time.
171 -- | Represents a single song item.
173 Song
{ sgArtist
, sgAlbum
, sgTitle
, sgFilePath
, sgGenre
, sgName
, sgComposer
174 , sgPerformer
:: String
175 , sgLength
:: Seconds
-- ^ Length in seconds
176 , sgDate
:: Int -- ^ Year
177 , sgTrack
:: (Int, Int) -- ^ Track number\/total tracks
178 , sgDisc
:: (Int, Int) -- ^ Position in set\/total in set
179 , sgIndex
:: Maybe PLIndex
}
182 -- Avoid the need for writing a proper 'elem' for use in 'prune'.
183 instance Eq Song
where
184 (==) x y
= sgFilePath x
== sgFilePath y
186 -- | Represents an output device.
188 Device
{ dOutputID
:: Int -- ^ Output's ID number
189 , dOutputName
:: String -- ^ Output's name as defined in the MPD
190 -- configuration file
191 , dOutputEnabled
:: Bool }
198 -- | Turn off an output device.
199 disableOutput
:: Int -> MPD
()
200 disableOutput
= getResponse_
. ("disableoutput " ++) . show
202 -- | Turn on an output device.
203 enableOutput
:: Int -> MPD
()
204 enableOutput
= getResponse_
. ("enableoutput " ++) . show
206 -- | Retrieve information for all output devices.
207 outputs
:: MPD
[Device
]
208 outputs
= getResponse
"outputs" >>=
209 mapM (foldM f
empty) . splitGroups
[("outputid",id)] . toAssoc
210 where f a
("outputid", x
) = parse parseNum
(\x
' -> a
{ dOutputID
= x
' }) x
211 f a
("outputname", x
) = return a
{ dOutputName
= x
}
212 f a
("outputenabled", x
) = parse parseBool
213 (\x
' -> a
{ dOutputEnabled
= x
'}) x
214 f _ x
= throwError
. Unexpected
$ show x
216 empty = Device
0 "" False
218 -- | Update the server's database.
219 -- If no paths are given, all paths will be scanned.
220 -- Unreadable or non-existent paths are silently ignored.
221 update
:: [Path
] -> MPD
()
222 update
[] = getResponse_
"update"
223 update
[x
] = getResponse_
("update " ++ show x
)
224 update xs
= getResponses
(map (("update " ++) . show) xs
) >> return ()
230 -- | List all metadata of metadata (sic).
231 list :: Meta
-- ^ Metadata to list
232 -> Maybe Query
-> MPD
[String]
233 list mtype query
= liftM takeValues
(getResponse cmd
)
234 where cmd
= "list " ++ show mtype
++ maybe "" ((" "++) . show) query
236 -- | Non-recursively list the contents of a database directory.
237 lsInfo
:: Path
-> MPD
[Either Path Song
]
238 lsInfo
= lsInfo
' "lsinfo"
240 -- | List the songs (without metadata) in a database directory recursively.
241 listAll
:: Path
-> MPD
[Path
]
242 listAll path
= liftM (map snd . filter ((== "file") . fst) . toAssoc
)
243 (getResponse
("listall " ++ show path
))
245 -- | Recursive 'lsInfo'.
246 listAllInfo
:: Path
-> MPD
[Either Path Song
]
247 listAllInfo
= lsInfo
' "listallinfo"
249 -- Helper for lsInfo and listAllInfo.
250 lsInfo
' :: String -> Path
-> MPD
[Either Path Song
]
251 lsInfo
' cmd path
= do
252 liftM (extractEntries
(Just
. Right
, const Nothing
, Just
. Left
)) $
253 takeEntries
=<< getResponse
(cmd
++ " " ++ show path
)
255 -- | Search the database for entries exactly matching a query.
256 find :: Query
-> MPD
[Song
]
257 find query
= getResponse
("find " ++ show query
) >>= takeSongs
259 -- | Search the database using case insensitive matching.
260 search
:: Query
-> MPD
[Song
]
261 search query
= getResponse
("search " ++ show query
) >>= takeSongs
263 -- | Count the number of entries matching a query.
264 count
:: Query
-> MPD Count
265 count query
= getResponse
("count " ++ show query
) >>= psrProc parseCount
266 where psrProc f
= either (throwError
. Unexpected
) return . f
272 -- Unless otherwise noted all playlist commands operate on the current
275 -- This might do better to throw an exception than silently return 0.
276 -- | Like 'add', but returns a playlist id.
277 addId
:: Path
-> MPD
Integer
278 addId p
= getResponse1
("addid " ++ show p
) >>=
279 parse parseNum
id . snd . head . toAssoc
281 -- | Like 'add_' but returns a list of the files added.
282 add
:: PlaylistName
-> Path
-> MPD
[Path
]
283 add plname x
= add_ plname x
>> listAll x
285 -- | Add a song (or a whole directory) to a playlist.
286 -- Adds to current if no playlist is specified.
287 -- Will create a new playlist if the one specified does not already exist.
288 add_
:: PlaylistName
-> Path
-> MPD
()
289 add_
"" = getResponse_
. ("add " ++) . show
290 add_ plname
= getResponse_
.
291 (("playlistadd " ++ show plname
++ " ") ++) . show
293 -- | Clear a playlist. Clears current playlist if no playlist is specified.
294 -- If the specified playlist does not exist, it will be created.
295 clear
:: PlaylistName
-> MPD
()
296 clear
= getResponse_
. cmd
297 where cmd
"" = "clear"
298 cmd pl
= "playlistclear " ++ show pl
300 -- | Remove a song from a playlist.
301 -- If no playlist is specified, current playlist is used.
302 -- Note that a playlist position ('Pos') is required when operating on
303 -- playlists other than the current.
304 delete :: PlaylistName
-> PLIndex
-> MPD
()
305 delete "" (Pos x
) = getResponse_
("delete " ++ show x
)
306 delete "" (ID x
) = getResponse_
("deleteid " ++ show x
)
307 delete plname
(Pos x
) =
308 getResponse_
("playlistdelete " ++ show plname
++ " " ++ show x
)
309 delete _ _
= fail "'delete' within a playlist doesn't accept a playlist ID"
311 -- | Load an existing playlist.
312 load
:: PlaylistName
-> MPD
()
313 load
= getResponse_
. ("load " ++) . show
315 -- | Move a song to a given position.
316 -- Note that a playlist position ('Pos') is required when operating on
317 -- playlists other than the current.
318 move
:: PlaylistName
-> PLIndex
-> Integer -> MPD
()
319 move
"" (Pos from
) to
=
320 getResponse_
("move " ++ show from
++ " " ++ show to
)
321 move
"" (ID from
) to
=
322 getResponse_
("moveid " ++ show from
++ " " ++ show to
)
323 move plname
(Pos from
) to
=
324 getResponse_
("playlistmove " ++ show plname
++ " " ++ show from
++
326 move _ _ _
= fail "'move' within a playlist doesn't accept a playlist ID"
328 -- | Delete existing playlist.
329 rm
:: PlaylistName
-> MPD
()
330 rm
= getResponse_
. ("rm " ++) . show
332 -- | Rename an existing playlist.
333 rename
:: PlaylistName
-- ^ Original playlist
334 -> PlaylistName
-- ^ New playlist name
337 getResponse_
("rename " ++ show plname
++ " " ++ show new
)
339 -- | Save the current playlist.
340 save
:: PlaylistName
-> MPD
()
341 save
= getResponse_
. ("save " ++) . show
343 -- | Swap the positions of two songs.
344 -- Note that the positions must be of the same type, i.e. mixing 'Pos' and 'ID'
345 -- will result in a no-op.
346 swap
:: PLIndex
-> PLIndex
-> MPD
()
347 swap
(Pos x
) (Pos y
) = getResponse_
("swap " ++ show x
++ " " ++ show y
)
348 swap
(ID x
) (ID y
) = getResponse_
("swapid " ++ show x
++ " " ++ show y
)
349 swap _ _
= fail "'swap' cannot mix position and ID arguments"
351 -- | Shuffle the playlist.
353 shuffle
= getResponse_
"shuffle"
355 -- | Retrieve metadata for songs in the current playlist.
356 playlistInfo
:: Maybe PLIndex
-> MPD
[Song
]
357 playlistInfo x
= getResponse cmd
>>= takeSongs
358 where cmd
= case x
of
359 Just
(Pos x
') -> "playlistinfo " ++ show x
'
360 Just
(ID x
') -> "playlistid " ++ show x
'
361 Nothing
-> "playlistinfo"
363 -- | Retrieve metadata for files in a given playlist.
364 listPlaylistInfo
:: PlaylistName
-> MPD
[Song
]
365 listPlaylistInfo plname
=
366 takeSongs
=<< (getResponse
. ("listplaylistinfo " ++) $ show plname
)
368 -- | Retrieve a list of files in a given playlist.
369 listPlaylist
:: PlaylistName
-> MPD
[Path
]
370 listPlaylist
= liftM takeValues
. getResponse
. ("listplaylist " ++) . show
372 -- | Retrieve file paths and positions of songs in the current playlist.
373 -- Note that this command is only included for completeness sake; it's
374 -- deprecated and likely to disappear at any time, please use 'playlistInfo'
376 playlist
:: MPD
[(PLIndex
, Path
)]
377 playlist
= mapM f
=<< getResponse
"playlist"
378 where f s |
(pos
, name
) <- breakChar
':' s
379 , Just pos
' <- parseNum pos
380 = return (Pos pos
', name
)
381 |
otherwise = throwError
. Unexpected
$ show s
383 -- | Retrieve a list of changed songs currently in the playlist since
384 -- a given playlist version.
385 plChanges
:: Integer -> MPD
[Song
]
387 takeSongs
=<< (getResponse
. ("plchanges " ++) $ show version
)
389 -- | Like 'plChanges' but only returns positions and ids.
390 plChangesPosId
:: Integer -> MPD
[(PLIndex
, PLIndex
)]
391 plChangesPosId plver
=
392 getResponse
("plchangesposid " ++ show plver
) >>=
393 mapM f
. splitGroups
[("cpos",id)] . toAssoc
394 where f xs |
[("cpos", x
), ("Id", y
)] <- xs
395 , Just
(x
', y
') <- pair parseNum
(x
, y
)
396 = return (Pos x
', ID y
')
397 |
otherwise = throwError
. Unexpected
$ show xs
399 -- | Search for songs in the current playlist with strict matching.
400 playlistFind
:: Query
-> MPD
[Song
]
401 playlistFind q
= takeSongs
=<< (getResponse
. ("playlistfind " ++) $ show q
)
403 -- | Search case-insensitively with partial matches for songs in the
405 playlistSearch
:: Query
-> MPD
[Song
]
407 takeSongs
=<< (getResponse
. ("playlistsearch " ++) $ show q
)
409 -- | Get the currently playing song.
410 currentSong
:: MPD
(Maybe Song
)
413 if stState cs
== Stopped
415 else getResponse1
"currentsong" >>= fmap Just
. takeSongInfo
. toAssoc
421 -- | Set crossfading between songs.
422 crossfade
:: Seconds
-> MPD
()
423 crossfade
= getResponse_
. ("crossfade " ++) . show
425 -- | Begin\/continue playing.
426 play
:: Maybe PLIndex
-> MPD
()
427 play Nothing
= getResponse_
"play"
428 play
(Just
(Pos x
)) = getResponse_
("play " ++ show x
)
429 play
(Just
(ID x
)) = getResponse_
("playid " ++ show x
)
432 pause
:: Bool -> MPD
()
433 pause
= getResponse_
. ("pause " ++) . showBool
437 stop
= getResponse_
"stop"
439 -- | Play the next song.
441 next = getResponse_
"next"
443 -- | Play the previous song.
445 previous
= getResponse_
"previous"
447 -- | Seek to some point in a song.
448 -- Seeks in current song if no position is given.
449 seek
:: Maybe PLIndex
-> Seconds
-> MPD
()
450 seek
(Just
(Pos x
)) time
=
451 getResponse_
("seek " ++ show x
++ " " ++ show time
)
452 seek
(Just
(ID x
)) time
=
453 getResponse_
("seekid " ++ show x
++ " " ++ show time
)
454 seek Nothing time
= do
456 unless (stState st
== Stopped
) (seek
(stSongID st
) time
)
458 -- | Set random playing.
459 random :: Bool -> MPD
()
460 random = getResponse_
. ("random " ++) . showBool
463 repeat :: Bool -> MPD
()
464 repeat = getResponse_
. ("repeat " ++) . showBool
466 -- | Set the volume (0-100 percent).
467 setVolume
:: Int -> MPD
()
468 setVolume
= getResponse_
. ("setvol " ++) . show
470 -- | Increase or decrease volume by a given percent, e.g.
471 -- 'volume 10' will increase the volume by 10 percent, while
472 -- 'volume (-10)' will decrease it by the same amount.
473 -- Note that this command is only included for completeness sake ; it's
474 -- deprecated and may disappear at any time, please use 'setVolume' instead.
475 volume
:: Int -> MPD
()
476 volume
= getResponse_
. ("volume " ++) . show
479 -- Miscellaneous commands
482 -- | Clear the current error message in status.
484 clearError
= getResponse_
"clearerror"
486 -- | Retrieve a list of available commands.
487 commands
:: MPD
[String]
488 commands
= liftM takeValues
(getResponse
"commands")
490 -- | Retrieve a list of unavailable (due to access restrictions) commands.
491 notCommands
:: MPD
[String]
492 notCommands
= liftM takeValues
(getResponse
"notcommands")
494 -- | Retrieve a list of available song metadata.
495 tagTypes
:: MPD
[String]
496 tagTypes
= liftM takeValues
(getResponse
"tagtypes")
498 -- | Retrieve a list of supported urlhandlers.
499 urlHandlers
:: MPD
[String]
500 urlHandlers
= liftM takeValues
(getResponse
"urlhandlers")
502 -- XXX should the password be quoted?
503 -- | Send password to server to authenticate session.
504 -- Password is sent as plain text.
505 password
:: String -> MPD
()
506 password
= getResponse_
. ("password " ++)
508 -- | Check that the server is still responding.
510 ping
= getResponse_
"ping"
512 -- | Get server statistics.
514 stats
= getResponse
"stats" >>= foldM f defaultStats
. toAssoc
516 f a
("artists", x
) = parse parseNum
(\x
' -> a
{ stsArtists
= x
' }) x
517 f a
("albums", x
) = parse parseNum
(\x
' -> a
{ stsAlbums
= x
' }) x
518 f a
("songs", x
) = parse parseNum
(\x
' -> a
{ stsSongs
= x
' }) x
519 f a
("uptime", x
) = parse parseNum
(\x
' -> a
{ stsUptime
= x
' }) x
520 f a
("playtime", x
) = parse parseNum
(\x
' -> a
{ stsPlaytime
= x
' }) x
521 f a
("db_playtime", x
) = parse parseNum
522 (\x
' -> a
{ stsDbPlaytime
= x
' }) x
523 f a
("db_update", x
) = parse parseNum
(\x
' -> a
{ stsDbUpdate
= x
' }) x
524 f _ x
= throwError
. Unexpected
$ show x
526 Stats
{ stsArtists
= 0, stsAlbums
= 0, stsSongs
= 0, stsUptime
= 0
527 , stsPlaytime
= 0, stsDbPlaytime
= 0, stsDbUpdate
= 0 }
529 -- | Get the server's status.
531 status
= getResponse
"status" >>= foldM f
empty . toAssoc
532 where f a
("state", x
) = parse state
(\x
' -> a
{ stState
= x
'}) x
533 f a
("volume", x
) = parse parseNum
(\x
' -> a
{ stVolume
= x
'}) x
534 f a
("repeat", x
) = parse parseBool
535 (\x
' -> a
{ stRepeat
= x
' }) x
536 f a
("random", x
) = parse parseBool
537 (\x
' -> a
{ stRandom
= x
' }) x
538 f a
("playlist", x
) = parse parseNum
539 (\x
' -> a
{ stPlaylistVersion
= x
'}) x
540 f a
("playlistlength", x
) = parse parseNum
541 (\x
' -> a
{ stPlaylistLength
= x
'}) x
542 f a
("xfade", x
) = parse parseNum
543 (\x
' -> a
{ stXFadeWidth
= x
'}) x
544 f a
("song", x
) = parse parseNum
545 (\x
' -> a
{ stSongPos
= Just
(Pos x
') }) x
546 f a
("songid", x
) = parse parseNum
547 (\x
' -> a
{ stSongID
= Just
(ID x
') }) x
548 f a
("time", x
) = parse time
(\x
' -> a
{ stTime
= x
' }) x
549 f a
("bitrate", x
) = parse parseNum
550 (\x
' -> a
{ stBitrate
= x
'}) x
551 f a
("audio", x
) = parse audio
(\x
' -> a
{ stAudio
= x
' }) x
552 f a
("updating_db", x
) = parse parseNum
553 (\x
' -> a
{ stUpdatingDb
= x
' }) x
554 f a
("error", x
) = return a
{ stError
= x
}
555 f _ x
= throwError
. Unexpected
$ show x
557 state
"play" = Just Playing
558 state
"pause" = Just Paused
559 state
"stop" = Just Stopped
562 time s
= pair parseNum
$ breakChar
':' s
564 audio s
= let (u
, u
') = breakChar
':' s
565 (v
, w
) = breakChar
':' u
' in
566 case (parseNum u
, parseNum v
, parseNum w
) of
567 (Just a
, Just b
, Just c
) -> Just
(a
, b
, c
)
570 empty = Status Stopped
0 False False 0 0 Nothing Nothing
(0,0) 0 0
574 -- Extensions\/shortcuts.
577 -- | Like 'update', but returns the update job id.
578 updateId
:: [Path
] -> MPD
Integer
579 updateId paths
= liftM (read . head . takeValues
) cmd
580 where cmd
= case map show paths
of
581 [] -> getResponse
"update"
582 [x
] -> getResponse
("update " ++ x
)
583 xs
-> getResponses
(map ("update " ++) xs
)
585 -- | Toggles play\/pause. Plays if stopped.
587 toggle
= status
>>= \st
-> case stState st
of Playing
-> pause
True
590 -- | Add a list of songs\/folders to a playlist.
591 -- Should be more efficient than running 'add' many times.
592 addMany
:: PlaylistName
-> [Path
] -> MPD
()
593 addMany _
[] = return ()
594 addMany plname
[x
] = add_ plname x
595 addMany plname xs
= getResponses
(map ((cmd
++) . show) xs
) >> return ()
596 where cmd
= case plname
of "" -> "add "
597 pl
-> "playlistadd " ++ show pl
++ " "
599 -- | Delete a list of songs from a playlist.
600 -- If there is a duplicate then no further songs will be deleted, so
601 -- take care to avoid them (see 'prune' for this).
602 deleteMany
:: PlaylistName
-> [PLIndex
] -> MPD
()
603 deleteMany _
[] = return ()
604 deleteMany plname
[x
] = delete plname x
605 deleteMany
"" xs
= getResponses
(map cmd xs
) >> return ()
606 where cmd
(Pos x
) = "delete " ++ show x
607 cmd
(ID x
) = "deleteid " ++ show x
608 deleteMany plname xs
= getResponses
(map cmd xs
) >> return ()
609 where cmd
(Pos x
) = "playlistdelete " ++ show plname
++ " " ++ show x
612 -- | Returns all songs and directories that match the given partial
614 complete
:: String -> MPD
[Either Path Song
]
616 xs
<- liftM matches
. lsInfo
$ dropFileName path
618 [Left dir
] -> complete
$ dir
++ "/"
621 matches
= filter (isPrefixOf path
. takePath
)
622 takePath
= either id sgFilePath
625 -- The bounds are inclusive.
626 -- If 'Nothing' or 'ID' is passed the cropping will leave your playlist alone
628 crop
:: Maybe PLIndex
-> Maybe PLIndex
-> MPD
()
630 pl
<- playlistInfo Nothing
631 let x
' = case x
of Just
(Pos p
) -> fromInteger p
632 Just
(ID i
) -> fromMaybe 0 (findByID i pl
)
634 -- ensure that no songs are deleted twice with 'max'.
635 ys
= case y
of Just
(Pos p
) -> drop (max (fromInteger p
) x
') pl
636 Just
(ID i
) -> maybe [] (flip drop pl
. max x
' . (+1))
639 deleteMany
"" . mapMaybe sgIndex
$ take x
' pl
++ ys
640 where findByID i
= findIndex ((==) i
. (\(ID j
) -> j
) . fromJust . sgIndex
)
642 -- | Remove duplicate playlist entries.
644 prune
= findDuplicates
>>= deleteMany
""
646 -- Find duplicate playlist entries.
647 findDuplicates
:: MPD
[PLIndex
]
649 liftM (map ((\(ID x
) -> ID x
) . fromJust . sgIndex
) . flip dups
([],[])) $
651 where dups
[] (_
, dup
) = dup
652 dups
(x
:xs
) (ys
, dup
)
653 | x `
elem` xs
&& x `
notElem` ys
= dups xs
(ys
, x
:dup
)
654 |
otherwise = dups xs
(x
:ys
, dup
)
656 -- | List directories non-recursively.
657 lsDirs
:: Path
-> MPD
[Path
]
659 liftM (extractEntries
(const Nothing
,const Nothing
, Just
)) $
660 takeEntries
=<< getResponse
("lsinfo " ++ show path
)
662 -- | List files non-recursively.
663 lsFiles
:: Path
-> MPD
[Path
]
665 liftM (extractEntries
(Just
. sgFilePath
, const Nothing
, const Nothing
)) $
666 takeEntries
=<< getResponse
("lsinfo " ++ show path
)
668 -- | List all playlists.
669 lsPlaylists
:: MPD
[PlaylistName
]
671 liftM (extractEntries
(const Nothing
, Just
, const Nothing
)) $
672 takeEntries
=<< getResponse
"lsinfo"
674 -- | Search the database for songs relating to an artist.
675 findArtist
:: Artist
-> MPD
[Song
]
676 findArtist
= find . Query Artist
678 -- | Search the database for songs relating to an album.
679 findAlbum
:: Album
-> MPD
[Song
]
680 findAlbum
= find . Query Album
682 -- | Search the database for songs relating to a song title.
683 findTitle
:: Title
-> MPD
[Song
]
684 findTitle
= find . Query Title
686 -- | List the artists in the database.
687 listArtists
:: MPD
[Artist
]
688 listArtists
= liftM takeValues
(getResponse
"list artist")
690 -- | List the albums in the database, optionally matching a given
692 listAlbums
:: Maybe Artist
-> MPD
[Album
]
693 listAlbums artist
= liftM takeValues
(getResponse
("list album" ++
694 maybe "" ((" artist " ++) . show) artist
))
696 -- | List the songs in an album of some artist.
697 listAlbum
:: Artist
-> Album
-> MPD
[Song
]
698 listAlbum artist album
= find (MultiQuery
[Query Artist artist
701 -- | Search the database for songs relating to an artist using 'search'.
702 searchArtist
:: Artist
-> MPD
[Song
]
703 searchArtist
= search
. Query Artist
705 -- | Search the database for songs relating to an album using 'search'.
706 searchAlbum
:: Album
-> MPD
[Song
]
707 searchAlbum
= search
. Query Album
709 -- | Search the database for songs relating to a song title.
710 searchTitle
:: Title
-> MPD
[Song
]
711 searchTitle
= search
. Query Title
713 -- | Retrieve the current playlist.
714 -- Equivalent to @playlistinfo Nothing@.
715 getPlaylist
:: MPD
[Song
]
716 getPlaylist
= playlistInfo Nothing
719 -- Miscellaneous functions.
722 -- Run getResponse but discard the response.
723 getResponse_
:: String -> MPD
()
724 getResponse_ x
= getResponse x
>> return ()
726 -- Get the lines of the daemon's response to a list of commands.
727 getResponses
:: [String] -> MPD
[String]
728 getResponses cmds
= getResponse
$ intercalate
"\n" cmds
'
729 where cmds
' = "command_list_begin" : cmds
++ ["command_list_end"]
731 -- Helper that throws unexpected error if input is empty.
732 failOnEmpty
:: [String] -> MPD
[String]
733 failOnEmpty
[] = throwError
$ Unexpected
"Non-empty response expected."
734 failOnEmpty xs
= return xs
736 -- A wrapper for getResponse that fails on non-empty responses.
737 getResponse1
:: String -> MPD
[String]
738 getResponse1 x
= getResponse x
>>= failOnEmpty
744 -- Run 'toAssoc' and return only the values.
745 takeValues
:: [String] -> [String]
746 takeValues
= snd . unzip . toAssoc
754 -- Separate the result of an lsinfo\/listallinfo call into directories,
755 -- playlists, and songs.
756 takeEntries
:: [String] -> MPD
[EntryType
]
757 takeEntries
= mapM toEntry
. splitGroups wrappers
. toAssoc
. reverse
759 toEntry xs
@(("file",_
):_
) = liftM SongEntry
$ takeSongInfo xs
760 toEntry
(("directory",d
):_
) = return $ DirEntry d
761 toEntry
(("playlist",pl
):_
) = return $ PLEntry pl
762 toEntry _
= error "takeEntries: splitGroups is broken"
763 wrappers
= [("file",id), ("directory",id), ("playlist",id)]
765 -- Extract a subset of songs, directories, and playlists.
766 extractEntries
:: (Song
-> Maybe a
, String -> Maybe a
, String -> Maybe a
)
767 -> [EntryType
] -> [a
]
768 extractEntries
(fSong
,fPlayList
,fDir
) = catMaybes . map f
770 f
(SongEntry s
) = fSong s
771 f
(PLEntry pl
) = fPlayList pl
772 f
(DirEntry d
) = fDir d
774 -- Build a list of song instances from a response.
775 takeSongs
:: [String] -> MPD
[Song
]
776 takeSongs
= mapM takeSongInfo
. splitGroups
[("file",id)] . toAssoc
778 -- Builds a song instance from an assoc. list.
779 takeSongInfo
:: [(String, String)] -> MPD Song
780 takeSongInfo xs
= foldM f song xs
781 where f a
("Artist", x
) = return a
{ sgArtist
= x
}
782 f a
("Album", x
) = return a
{ sgAlbum
= x
}
783 f a
("Title", x
) = return a
{ sgTitle
= x
}
784 f a
("Genre", x
) = return a
{ sgGenre
= x
}
785 f a
("Name", x
) = return a
{ sgName
= x
}
786 f a
("Composer", x
) = return a
{ sgComposer
= x
}
787 f a
("Performer", x
) = return a
{ sgPerformer
= x
}
788 f a
("Date", x
) = parse parseDate
(\x
' -> a
{ sgDate
= x
' }) x
789 f a
("Track", x
) = parse parseTuple
(\x
' -> a
{ sgTrack
= x
'}) x
790 f a
("Disc", x
) = parse parseTuple
(\x
' -> a
{ sgDisc
= x
'}) x
791 f a
("file", x
) = return a
{ sgFilePath
= x
}
792 f a
("Time", x
) = parse parseNum
(\x
' -> a
{ sgLength
= x
'}) x
793 f a
("Id", x
) = parse parseNum
794 (\x
' -> a
{ sgIndex
= Just
(ID x
') }) x
796 f a
("Pos", _
) = return a
797 -- Catch unrecognised keys
798 f _ x
= throwError
. Unexpected
$ show x
800 parseTuple s
= let (x
, y
) = breakChar
'/' s
in
801 -- Handle incomplete values. For example, songs might
802 -- have a track number, without specifying the total
803 -- number of tracks, in which case the resulting
804 -- tuple will have two identical parts.
805 case (parseNum x
, parseNum y
) of
806 (Just x
', Nothing
) -> Just
(x
', x
')
807 (Just x
', Just y
') -> Just
(x
', y
')
810 song
= Song
{ sgArtist
= "", sgAlbum
= "", sgTitle
= ""
811 , sgGenre
= "", sgName
= "", sgComposer
= ""
812 , sgPerformer
= "", sgDate
= 0, sgTrack
= (0,0)
813 , sgDisc
= (0,0), sgFilePath
= "", sgLength
= 0
814 , sgIndex
= Nothing
}