z The Flat Field Z
[ @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ]

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:

FieldDescription
stateplay if playing, stop if stopped, or pause if paused
repeatIf 1 repeat the current playlist or song
randomIf 1 next song will be random
singleIf 1 playback stops after current song, or repeats it if repeat is enabled
consumeIf 1 song is removed from playlist after it is played
elapsedTotal time elapsed in current song in seconds
durationDuration of current song in seconds
songidThe 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:

FieldDescription
TitleTitle of the song
AlbumAlbum the song is from
ArtistArtist the song is by
DateWhen 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.