Exploring the MPD Protocol
MPD is a popular music server for Linux. It's controlled with a custom protocol over TCP. This post explores that protocol.
Connecting to the Server
Since the MPD protocol uses TCP the first step is to connect to it with a TCP client. We will connect to an MPD server running on localhost using the default port: 6600.
(require '[aleph.tcp :as tcp])
(require '[manifold.deferred :as d])
(require '[manifold.stream :as s])
(defn wrap-duplex-stream
[s]
(let [out (s/stream)]
(s/connect out s)
(s/splice out s)))
(defn client
[host port]
(d/chain (tcp/client {:host host, :port port})
#(wrap-duplex-stream %)))
(def mpd-client @(client "localhost" 6600))
(-> @(s/take! mpd-client) slurp)
"OK MPD 0.23.4\n"
Once the connection is established we should see a message that starts with "OK MPD". Verifying that we see that message lets us know that an MPD server is running where we expect, and that we can connect to it.
Status
We can query that status of the server with the status
command.
@(s/put! mpd-client "status\n")
(-> @(s/take! mpd-client) slurp clojure.string/split-lines)
["repeat: 0" "random: 0" "single: 0" "consume: 0" "partition: default" "playlist: 45" "playlistlength: 4" "mixrampdb: 0" "state: play" "song: 0" "songid: 12" "time: 29:186" "elapsed: 28.708" "bitrate: 848" "duration: 185.843" "audio: 44100:16:2" "nextsong: 1" "nextsongid: 13" "OK"]
This command gives us all kinds of useful info:
Field | Description |
---|---|
state | play if playing, stop if stopped, or pause if paused |
repeat | If 1 repeat the current playlist or song |
random | If 1 next song will be random |
single | If 1 playback stops after current song, or repeats it if repeat is enabled |
consume | If 1 song is removed from playlist after it is played |
elapsed | Total time elapsed in current song in seconds |
duration | Duration of current song in seconds |
songid | The ID of the current song |
We also see that a line with "OK" in it's used as a delimiter.
Current Queue
We can also query the current queue with playlistinfo
command:
@(s/put! mpd-client "playlistinfo\n")
(-> @(s/take! mpd-client) slurp clojure.string/split-lines)
["file: Arctic Flowers/Weaver/01 - Magdalene.flac" "Last-Modified: 2018-06-27T04:52:46Z" "Format: 44100:16:2" "Title: Magdalene" "Artist: Arctic Flowers" "Date: 2014" "Album: Weaver" "Track: 1" "AlbumArtist: Arctic Flowers" "Time: 186" "duration: 185.843" "Pos: 0" "Id: 12" "file: Arctic Flowers/Weaver/02 - Tell My Horse.flac" "Last-Modified: 2018-06-27T04:52:46Z" "Format: 44100:16:2" "Title: Tell My Horse" "Artist: Arctic Flowers" "Date: 2014" "Album: Weaver" "Track: 2" "AlbumArtist: Arctic Flowers" "Time: 158" "duration: 157.701" "Pos: 1" "Id: 13" "file: Arctic Flowers/Weaver/03 - Ex Oblivione.flac" "Last-Modified: 2018-06-27T04:52:47Z" "Format: 44100:16:2" "Title: Ex Oblivione" "Artist: Arctic Flowers" "Date: 2014" "Album: Weaver" "Track: 3" "AlbumArtist: Arctic Flowers" "Time: 184" "duration: 184.118" "Pos: 2" "Id: 14" "file: Arctic Flowers/Weaver/08 - Dirges Dwell.flac" "Last-Modified: 2018-06-27T04:52:46Z" "Format: 44100:16:2" "Title: Dirges Dwell" "Artist: Arctic Flowers" "Date: 2014" "Album: Weaver" "Track: 8" "AlbumArtist: Arctic Flowers" "Time: 239" "duration: 238.543" "Pos: 3" "Id: 19" "OK"]
This gives us all of the songs in the current queue in the order they appear. Additionally it has more information about each of the songs:
Field | Description |
---|---|
Title | Title of the song |
Album | Album the song is from |
Artist | Artist the song is by |
Date | When the album was released |
Controlling Playback
We'll also need some commands to control playback:
;; Play next song in the playlist.
@(s/put! mpd-client "next\n")
;; Play previous song in the playlist.
@(s/put! mpd-client "previous\n")
;; Play a song by ID.
@(s/put! mpd-client "playid 12\n")
;; Seek to a position in the current song.
@(s/put! mpd-client "seekcur 120\n")
;; Stop playback.
@(s/put! mpd-client "stop\n")
;; Toggle between pause/play.
@(s/put! mpd-client "pause\n")
The Database
The database can be queried with the find
and search
commands. A combination of an ancient modified since and window will let us consume the entire database in batches.
@(s/put! mpd-client "find \"(modified-since '-2619216000')\" window 10:15\n")
(-> @(s/take! mpd-client) slurp clojure.string/split-lines)
["file: Agalloch/Marrow of the Spirit/01 - They Escaped the Weight of Darkness.flac" "Last-Modified: 2014-10-28T15:18:50Z" "Format: 44100:16:2" "Title: They Escaped the Weight of Darkness" "Artist: Agalloch" "ArtistSort: Agalloch" "Album: Marrow of the Spirit" "AlbumArtist: Agalloch" "AlbumArtistSort: Agalloch" "Date: 2010-11-29" "Track: 1" "Track: 1" "Disc: 1" "Disc: 1" "MUSICBRAINZ_TRACKID: 83102c80-5668-46c1-a914-e429324cd6f0" "MUSICBRAINZ_ALBUMID: 1363039e-ece8-3612-a53d-31711accb5f9" "MUSICBRAINZ_ARTISTID: 3d46727d-9367-47b8-8b8b-f7b6767f7d57" "MUSICBRAINZ_ALBUMARTISTID: 3d46727d-9367-47b8-8b8b-f7b6767f7d57" "Label: Viva Hate Records" "OriginalDate: 2010-11-23" "Time: 221" "duration: 221.026" "file: Agalloch/Marrow of the Spirit/02 - Into the Painted Grey.flac" "Last-Modified: 2014-10-28T15:21:12Z" "Format: 44100:16:2" "Title: Into the Painted Grey" "Artist: Agalloch" "ArtistSort: Agalloch" "Album: Marrow of the Spirit" "AlbumArtist: Agalloch" "AlbumArtistSort: Agalloch" "Date: 2010-11-29" "Track: 2" "Track: 2" "Disc: 1" "Disc: 1" "MUSICBRAINZ_TRACKID: 2e05f116-ba3f-41d5-80b1-c1c7c8a7e2c8" "MUSICBRAINZ_ALBUMID: 1363039e-ece8-3612-a53d-31711accb5f9" "MUSICBRAINZ_ARTISTID: 3d46727d-9367-47b8-8b8b-f7b6767f7d57" "MUSICBRAINZ_ALBUMARTISTID: 3d46727d-9367-47b8-8b8b-f7b6767f7d57" "Label: Viva Hate Records" "OriginalDate: 2010-11-23" "Time: 745" "duration: 745.040" "file: Agalloch/Marrow of the Spirit/03 - The Watcher’s Monolith.flac" "Last-Modified: 2014-10-28T15:23:15Z" "Format: 44100:16:2" "Title: The Watcher’s Monolith" "Artist: Agalloch" "ArtistSort: Agalloch" "Album: Marrow of the Spirit" "AlbumArtist: Agalloch" "AlbumArtistSort: Agalloch" "Date: 2010-11-29" "Track: 3" "Track: 3" "Disc: 1" "Disc: 1" "MUSICBRAINZ_TRACKID: b10e7809-53a6-4d29-bbb1-58d2987c08d8" "MUSICBRAINZ_ALBUMID: 1363039e-ece8-3612-a53d-31711accb5f9" "MUSICBRAINZ_ARTISTID: 3d46727d-9367-47b8-8b8b-f7b6767f7d57" "MUSICBRAINZ_ALBUMARTISTID: 3d46727d-9367-47b8-8b8b-f7b6767f7d57" "Label: Viva Hate Records" "OriginalDate: 2010-11-23" "Time: 706" "duration: 706.346" "file: Agalloch/Marrow of the Spirit/04 - Black Lake Niðstång.flac" "Last-Modified: 2014-10-28T15:26:52Z" "Format: 44100:16:2" "Title: Black Lake Niðstång" "Artist: Agalloch" "ArtistSort: Agalloch" "Album: Marrow of the Spirit" "AlbumArtist: Agalloch" "AlbumArtistSort: Agalloch" "Date: 2010-11-29" "Track: 4" "Track: 4" "Disc: 1" "Disc: 1" "MUSICBRAINZ_TRACKID: 106fe000-7d29-44cc-9c9f-f75aa80c34ac" "MUSICBRAINZ_ALBUMID: 1363039e-ece8-3612-a53d-31711accb5f9" "MUSICBRAINZ_ARTISTID: 3d46727d-9367-47b8-8b8b-f7b6767f7d57" "MUSICBRAINZ_ALBUMARTISTID: 3d46727d-9367-47b8-8b8b-f7b6767f7d57" "Label: Viva Hate Records" "OriginalDate: 2010-11-23" "Time: 1054" "duration: 1054.373" "file: Agalloch/Marrow of the Spirit/05 - Ghosts of the Midwinter Fires.flac" "Last-Modified: 2014-10-28T15:29:31Z" "Format: 44100:16:2" "Title: Ghosts of the Midwinter Fires" "Artist: Agalloch" "ArtistSort: Agalloch" "Album: Marrow of the Spirit" "AlbumArtist: Agalloch" "AlbumArtistSort: Agalloch" "Date: 2010-11-29" "Track: 5" "Track: 5" "Disc: 1" "Disc: 1" "MUSICBRAINZ_TRACKID: 6ff472e0-0319-4a94-9339-d3bdb3fb2884" "MUSICBRAINZ_ALBUMID: 1363039e-ece8-3612-a53d-31711accb5f9" "MUSICBRAINZ_ARTISTID: 3d46727d-9367-47b8-8b8b-f7b6767f7d57" "MUSICBRAINZ_ALBUMARTISTID: 3d46727d-9367-47b8-8b8b-f7b6767f7d57" "Label: Viva Hate Records" "OriginalDate: 2010-11-23" "Time: 580" "duration: 579.760" "OK"]
Queue Management
We'll need some commands to add or remove items from the queue:
;; Add a song to the queue.
@(s/put! mpd-client "addid \"Agalloch/Marrow of the Spirit/05 - Ghosts of the Midwinter Fires.flac\"\n")
;; Remove a song from the queue.
@(s/put! mpd-client "deleteid 12\n")
Conclusion
Overall the protocol is pretty straightforwards, and the MPD server is straightforward to interact with.