A tight, ergonomic, async client library for mpd.
This manual corresponds to elmpd version 1.0.0.
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.
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).
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.
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:
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.
Returns a symbol representing the process status for conn, as
defined by process-status
(see process-status in Emacs Lisp)
Returns #t
if conn’s connection status is 'failed
,
nil
else.
Among MPD clients, there are two idioms for sending commands while receiving notifications of server-side changes:
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.
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).
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:
'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.
'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).
'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
).
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
.
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:
cmd
cmd, cb
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.
Chain multiple commands on conn.
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 sp1ff@pobox.com, or shoot a webmention to me at my web site.
Jump to: | C E L M S Z |
---|
Jump to: | C E L M S Z |
---|