Skip to content

sp1ff/elmpd

Repository files navigation

elmpd

https://melpa.org/packages/elmpd-badge.svg https://stable.melpa.org/packages/elmpd-badge.svg https://github.com/sp1ff/elmpd/workflows/melpazoid/badge.svg

Introduction

elmpd is a tight, asynchronous, ergonomic MPD client library in Emacs Lisp.

Prerequisites

Emacs 25.1.

Installing

The simplest way to install elmpd is available from MELPA.

I’m now making GitHub releases that include Autotools source distributions:

curl -L --output elmpd-0.2.4.tar.gz  https://github.com/sp1ff/scribbu/archive/v0.5.tar.gz
tar xf elmpd-0.2.4.tar.gz && cd elmpd-0.2.4
./configure && make all check
sudo make install

And of course, you can just build from source:

git clone git@github.com:sp1ff/elmpd.git
cd elmpd
./bootstrap
./configure && make all check
sudo make install

Getting Started

Creating Connections

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.

There are two idioms I’ve seen in MPD client libraries 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”

Since elmpd is a library, I do not make that choice here, but rather support both styles. See the docstring for elmpd-connect on how to configure your new connection in either way.

The implementation is callback-based; each connection comes at the cost of a single socket plus whatever memory is needed to do the text processing in handling responses. In particular, I declined to use tq despite the natural fit because I didn’t want to use a buffer for each connection, as well.

Invoking Commands

Send commands via elmpd-send. For example, to start MPD playing:

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

Sends the “play” command to your MPD server listening on port 6600 on localhost. 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)))))

You can send command lists by specifying a list rather than a string as the second parameter:

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

Will send the following to your local MPD daemon:

command_list_begin
random 1
consume 1
crossfade 5
play
command_list_end

Now that we’re sending multiple commands, we may be interested in processing the responses in different ways. For instance:

(let ((conn (elmpd-connect :host "localhost"))
      (groups '("Pogues" "Rolling Stones" "Flogging Molly")))
    (elmpd-send 
     conn
     (cl-mapc (lambda (x) (format "count \"(Artist =~ '%s')\"" x)) groups)
     (lambda (_conn ok rsp)
       (if ok
           ;; `rsp' is a list; one response per command
           (cl-mapc
            (lambda (x)
              (let* ((lines (split x "\n" t))
                     (line (car lines)))
                (message (substring line 7)))))
         (error "Error counting: %s" rsp)))
     'list))

will issue the “count” command in a command list (once for each of “Pogues”, “Rolling Stones” & “Flogging Molly”), receive the responses as a list, and process the list. If you just can’t wait, you can specify =’stream= instead of =’list=; in this case as soon as a response from a sub-command is available, your callback will be invoked with it:

(let ((conn (elmpd-connect :host "localhost"))
      (groups '("Pogues" "Rolling Stones" "Flogging Molly")))
    (elmpd-send 
     conn
     (cl-mapc (lambda (x) (format "count \"(Artist =~ '%s')\"" x)) groups)
     (lambda (_conn ok rsp)
       (if ok
           ;; `rsp' is a string; one invocation per command
           (let* ((lines (split x "\n" t))
                  (line (car lines)))
             (message (substring line 7)))
         (error "Error counting: %s" rsp)))
     'stream))

Prior to 0.2.2, sending a subsequent response meant you had to invoke elmpd-send from your callback, like so:

(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)))))

As with Javascript futures, this quickly became inconvenient & difficult to read, so I introduced the elmpd-chain macro in the hopes of achieving a syntax more like async Rust:

(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)))

Motivation & Design Philosphy

Damien Cassou, the author of mpdel and libmpdel, reached out to ask “Why elmpd?” His question prompted me to clarify my thoughts around this project & I’ve adapted my response here.

I’ve looked at a few MPD clients, including mpdel. As I experimented with my workflow, however, 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, for example. I customize my mode line heavily, and I wanted a little bit of logic to add the current track to the mode line & keep it up-to-date. 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 reading mail (in Emacs, of course!)

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 echo-ing commands to /dev/tcp/$host/$port). I spent time earlier this year learning to write asynchronous Rust, and that inspired me to see how simple I could make this using just callbacks. At the time of this writing, elmpd exports just two functions: elmpd-connect & elmpd-send. Each 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).

Status & Roadmap

I’ve been using the library for some time with good results. The bulk of the work has been in getting the asynchronous logic right; as such it is not very featureful. It is ripe for being used to build up a more caller-friendly API: (something-play) instead of:

(let ((conn (elmpd-connect)))
  (elmpd-send conn "play"))

I’ve written a separate package, mpdmacs, that hopefully does so in a generic way. I’ve recently, finally, gotten my head around LISP macros (short story: the code really is just data!) and so I’ve just implemented elmpd-chain; my next project for this package is to re-implement my other packages built on top of this in terms of elmpd-chain & refine it.

About

A tight, ergonomic, async MPD client library in Emacs Lisp

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages