elmpd

A tight, ergonomic, async client library for mpd.

This manual corresponds to elmpd version 1.0.0.

Table of Contents


1 Introduction

The Music Player Daemon (or MPD) is a “flexible, powerful, server-side application for playing music.” It offers an API for interacting with the daemon & building client applications.

elmpd is an Emacs Lisp package for talking to an MPD instance over that protocol. It is a small, asynchronous, minimalist implementation designed for lightweight apps.

The reader may also be interested in libmpdel, libmpdee, and the mpc package which ships with Emacs beginning in version 23.2.

Instructions for obtaining & installing the package may be found in the README. The motivation for yet another Emacs Lisp MPD client library may be found below. The reader eager to begin coding should proceed to Using the Package.


2 Why Another MPD Client Library?

elmpd was borne of experimentation with the other client packages listed above; as I fine-tuned my workflow, I found myself wanting less functionality: rather than interacting with a fully-featured client, I just wanted to skip to the next song while I was editing code with a quick key chord, for example. I customize my mode line heavily, and I wanted just a little bit of logic to add the current track to the mode line & keep it up-to-date as MPD progressed through the playlist. I have written a companion daemon to MPD that maintains ratings & play counts; I just needed a little function that would let me rate the current track while I was, say, reading mail.

My next move was to read through a number of client libraries for inspiration, both in C & Emacs Lisp. Many of them had strong opinions on how one should talk to MPD. Having been programming MPD for a while I had come to appreciate its simplicity (after all, one can program it from bash by simply echoing commands to /dev/tcp/$host/$port). My experience with async Rust inspired me to see how simple I could make this. Each elmpd connection consumes a socket & optionally a callback– that’s it (no buffer, no transaction queue). Put another way, if other libraries are Gnus (featureful, encourages you to read your e-mail in a certain way), then elmpd is Mailutils (small utilities that leave it up to the user to assemble them into something useful).


3 Using the Package


3.1 Connecting to an MPD Instance

Create an MPD connection by calling elmpd-connect. This will return an elmpd-connection instance immediately; asynchronously, it will be parsing the MPD greeting message, perhaps sending an initial password, and if so requested, sending the “idle” command.

API: elmpd-connect kwargs

Connect to an MPD server according to kwargs, which shall be a list of keyword arguments:

:name The name by which to refer to the underlying network process; it will be modified as necessary to make it unique. If not specified, it defaults to “*elmpd-connection*”.

:host The host on which the MPD server resides; if not given, it defaults first to the environment variable MPD_HOST, and then to “localhost”.

:port The port on which MPD is listening. If not given, it defaults to the environment variable MPD_PORT, then to the value 6600.

:local The path to the Unix socket on which MPD is listening. This argument is mutually exclusive with :host & :port. This option is preferred, so to force a TCP connection, pass this explicitly as nil.

:password If given, the “password” command shall be issued after the initial connection is made with this parameter; this should of course only be done over an encrypted connection, such as port forwarding over SSH.

:subsystems If given, this connection will perpetually idle; whenever a command is issued on this connection, a “noidle” command shall be given, followed by the command. When that command completes, the “idle” command will be re-issued specifying the same subsystems. This argument shall be a cons cell whose car is either the symbol 'all, the symbol representing the subsystem of interest, or a list of symbols naming the subsystems of interest (e.g. '(player mixer output)) and whose cdr is a callback to be invoked when any of those subsystems change; the callback shall take two parameters the first of which will be the elmpd-connection which saw the state change & the second of which will be either a single symbol or a list of symbols naming the changed subsystems.

A few examples follow. To attempt to connect to the “default” MPD instance, say:

(elmpd-connect)

This will attempt to connect first to the Unix socket at /var/run/mpd/socket, and failing that, to the TCP/IP socket at

(or (getenv ``MPD_HOST'') ``localhost'')

at port

(or (getenv ``MPD_PORT'') ``6600'')

To force a connection to the MPD instance on localhost at port 1234, and to create a connection that will, in between explicit commands, idle on subystems options, player and sticker, and invoke a function named my-callback when any one of those subsystems changes:

(elmpd-connect :local nil :port 1234 :subsystems '((options player sticker) . #'my-callback))

The available symbols naming subsystems:

  • database
  • update
  • stored
  • playlist
  • player
  • mixer
  • output
  • options
  • partition
  • sticker
  • subscription
  • message
  • neighbor
  • mount
API: elmpd-connection

elpmd-connect returns an elmpd-connection instance, an opaque type representing an MPD connection.

As already mentioned, the connection object will still be in the process of construction on return. The caller is free to send commands through the connection immediately; they will be queued up & sent as soon as possible.

If the MPD server is down, or the elmpd-connect arguments are incorrect, the connection will never be made; the connection will be stalled in the 'failed state, with any commands issued so far stuck in its queue. Since the failure will take place asynchronously, there is no convenient point at which to detect this; it is up to the caller to at some point, perhaps periodically, query the connection state, detect this & correct it.

API: elmpd-conn-status conn

Returns a symbol representing the process status for conn, as defined by process-status (see process-status in Emacs Lisp)

API: elmpd-conn-failed-p conn

Returns #t if conn’s connection status is 'failed, nil else.


3.2 Connection Idioms

Among MPD clients, there are two idioms for sending commands while receiving notifications of server-side changes:

  1. just maintain two connections (e.g. mpdfav); issue the “idle” command on one, send commands on the other
  2. use one connection, issue the “idle” command, and when asked to issue another command, send “noidle”, issue the requested command, collect the response, and then send “idle” again (e.g. libmpdel). Note that this is not a race condition per the MPD docs – any server-side changes that took place while processing the command will be saved & returned on “idle”

As a library, elmpd does not make that choice, but rather supports both styles. To create a connection in the second style, supply the :subsystems argument. To create a connection solely for dispatching commands, omit it.


3.3 Sending Commands


3.3.1 Simple Commands

Once you’ve established a connection, send a command via elmpd-send. For instance,

(let ((conn (elmpd-connect :host "localhost")))
  (elmpd-send conn "play"))

will send the “play” command to the MPD instance on localhost at port 6600 (assuming no Unix socket). Note that this code will likely return before anything actually happens. As mentioned above, elmpd-connect returns immediately after creating the network process; it only reads & parses the MPD greeting asynchronously. Likewise, elmpd-send only queues up the “play” command; it will actually be sent & its response read in the background.

If you’d like to do something with the response, you can provide a callback:

(let ((conn (elmpd-connect :host "localhost")))
  (elmpd-send 
   conn
   "getvol"
   (lambda (_conn ok rsp)
     (if ok (message "volume is %s" (substring rsp 7))
       (error "Failed to get volume: %s" rsp)))))

The callback is invoked with the elmpd-connection on which the command was sent, a boolean indicating success or failure of the command, and either the server response (on success) or the server error message (on failure).


3.3.2 Command Lists

MPD makes provision for packaging-up multiple commands in one shot: command lists. To issue a command list rather than an individual command, specify a list of string instead of just a string:

(let ((conn (elmpd-connect :host "localhost")))
  (elmpd-send 
   conn
   '("random 1" "consume 1" "crossfade 5" "play")))

will send the following to the local MPD daemon:

command_list_begin
random 1
consume 1
crossfade 5
play
command_list_end

This presents a few options as to how the results can be processed if a callback is provided. The keyword argument :response selects among them:

  1. 'default: The command list will be initiated with command_list_begin (which results in a single response for the entire list). The callback will be invoked once in the same manner as simple commands: i.e. with the connection, result and either response or error message.
  2. 'list: The callback will be invoked once with the connection, a boolean result code, and a list of responses to each individual command in the list (the command list will be started with command_list_ok_begin producing a result for each command in the list).
  3. 'stream: The callback will be invoked once for each completed command in the list with three parameters: the connection, a boolean indicating success or failure, and the individual command result or error message (the command list will be started with command_list_ok_begin).
API: elmpd-send conn cmd cb kwargs

Send MPD command cmd on conn. If the optional cb is given, it shall be a callback to be invoked with the command results. The :response keyword argument describes the way in which the callback will be invoked for command lists. :response may be one of 'default (the default), 'list or 'stream.


3.3.3 The elmpd-chain Macro

It sometimes happens that one would like to issue a command depending on the results of a prior command, for instance the following snippet will raise the volume to fifty if it’s not already at least that high already:

(let ((conn (elmpd-connect :host "localhost")))
  (elmpd-send 
   conn
   "getvol"
   (lambda (_conn ok rsp)
     (if ok
         (let ((vol (string-to-number (substring rsp 7 -1))))
           (if (< vol 50)
               (elmpd-send
                conn
                "setvol 50"
                (lambda (_conn ok rsp)
                  (if ok
                      (message "Increased volume from %d to 50." vol)
                    (message "Failed to increase volume: %s" rsp))))))
       (error "Failed to get volume: %s" rsp)))))

This quickly becomes inconvenient & difficult to read. In any such non-trivial case, the elmpd-chain macro can make this easier:

(let ((conn (elmpd-connect :host "localhost"))
      (vol 0))
  (elmpd-chain
   conn
   ("getvol"
    (lambda (_conn rsp)
      (setq vol (string-to-number (substring rsp 7 -1)))))
   :or-else
   (lambda (_conn rsp) (error "Failed to get volume: %s" rsp))
   :and-then
   ((format "setvol %d" (max 50 vol))
    (lambda (_ _) (message "Set volume to %d." vol)))
   :or-else
   (message "Failed to increase volume: %s" rsp)))

The general format is:

(elmpd-chain
 conn
 CMD
 [:or-else ELSE-HANDLER]
 [:and-then
   [CMD OR-ELSE? AND-THEN...])

where CMD may be any of:

  1. A single value cmd
  2. a list with two elements cmd, cb
  3. a list with three elements cmd, cb, style

In any case, cmd may be either a string (for a simple command), or a list of strings (for a command list). In case 3, if cmd is a string, then style must be 'default. Note that the callback will be invoked with just two arguments (the connection and the response), since it will only be invoked on success (you can place failure logic in an :or-else clause). Similarly, :or-else handlers are also invoked with just two arguments, since it will only be invoked on failure.

Macro: elmpd-chain conn args

Chain multiple commands on conn.


4 Roadmap & Contributing

elmpd was first released in mid-2020, and saw development through the rest of the year. Things have been quiet since the end of that year with the addition of elmpd-chain. In September of ’24 I decided to finally call this “1.0”, implying a certain level of stability (and notably will respect semver going forward).

Bugs & feature requests are welcome in the Issues section of the project. Also, you can just reach out directly at , or shoot a webmention to me at my web site.


Index


Function Index