mpdpopm 0.1.14

Maintain ratings & playcounts for your mpd server
Documentation

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Installing
    1. Use the pre-built binaries
    2. Crates.io
    3. Use the Debian package
    4. Autotools source distributions
    5. Building from source
  4. Getting Started
    1. Program Structure
    2. Getting Set-up
      1. MPD
      2. mppopmd
      3. mppopm
    3. Advanced Usage
      1. Keeping Other Repositories of Information Up-to-Date
      2. Generalized Commands
  5. Status & Roadmap

Introduction

mpdpopm provides a companion daemon to mpd for maintaining play counts, ratings and last-played timestamps, along with an associated CLI for talking to the daemon. Similar to mpdfav, but written in Rust (which I prefer to Go), it will maintain this information in your sticker database. Along the lines of mpdcron, it will also allow you to keep that information up-to-date in your tags by invoking external (user-provided & -configured) commands.

Prerequisites

Music Player Daemon: "Music Player Daemon (MPD) is a flexible, powerful, server-side application for playing music. Through plugins and libraries it can play a variety of sound files while being controlled by its network protocol." If you're reading this, I assume you're already running MPD, so this document won't have much to say on installing & configuring it.

If you choose to use the pre-built binaries or the Debian package (available under releases), that's all you'll need– you can jump ahead to the section entitled Installing, below.

If you would prefer to download mpdpopm from crates.io, you'll need need the Rust toolchain ("Rust is a memory- & thread-safe language with no runtime or garbage collector"). Installing the toolchain is easy:

curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

mpdpopm is also available as an Autotools source distribution (also under releases), and of course you can just clone the repo & build the project from source. In either of those two cases you'll need the Gnu Autotools installed in addition to Rust. Again, if you're considering that route, I assume you're familiar with the Autotools & won't say much about them here.

Installing

As mentioned above, you can install mpdpopm in a few different ways. In increasing order of complexity:

Use the pre-built binaries

Thanks to a suggestion by m040601, you can download pre-built binaries for each release. At the time of this writing, only Linux & MacOS are supported, and only on x8664 at that. If that works for you, you can do something like:

cd /tmp
curl -L --output mpdpopm-0.1.14.tar.gz https://github.com/sp1ff/mpdpopm/releases/download/0.1.14/mpdpopm-0.1.14-x86_64-unknown-linux.tar.gz
tar xf mpdpopm-0.1.14.tar.gz
tree mpdpopm-0.1.14-x86_64-unknown-linux/
mpdpopm-0.1.14-x86_64-unknown-linux/
├── bin
│   ├── mppopm
│   └── mppopmd
└── doc
    ├── AUTHORS
    ├── ChangeLog
    ├── COPYING
    ├── NEWS
    ├── README.org
    └── THANKS

2 directories, 8 files

Copy the binaries mppopmd (the daemon) and mppopm (the CLI) to a convenient place (e.g. /usr/local/bin or $HOME/.local/bin) and proceed to Getting Started, below.

Crates.io

If you've got the Rust toolchain installed, just say cargo install mpdpopm. The binaries will now be in $HOME/.cargo/bin, and you can proceed to Getting Started, below.

Use the Debian package

If you're running on a Debian-based Linux distribution, and you're on an x8664 processor, I've begun providing a Debian binary package, courtesy of the very cool cargo-deb Cargo helper command. Just do:

cd /tmp
curl -L -O https://github.com/sp1ff/mpdpopm/releases/download/0.1.14/mpdpopm_0.1.14_amd64.deb
sudo dpkg -i mpdpopm_0.1.14_amd64.deb

The binaries will be placed in /usr/local/bin, and you can proceed to Getting Started, below.

Autotools source distributions

If you've got the Rust toolchain as well as Autotools installed, you can build from source via Autotools:

cd /tmp
curl -L -O https://github.com/sp1ff/mpdpopm/releases/download/0.1.14/mpdpopm-0.1.14.tar.xz
tar xf mpdpopm-0.1.14.tar.xz
cd mpdpopm-0.1.14
./configure
make
make check
sudo make install

All the usual configure options apply (--prefix, e.g.)

Building from source

Finally, and again if you have the build toolchain (Rust & Autotools) installed, you can build from source:

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

Notice the call to ./bootstrap, in this case.

Getting Started

Program Structure

mpdpopm provides two programs:

  1. mppopmd is the companion daemon process
  2. mppopm is the associated command-line interface to the daemon

They both make use of the mpd protocol, a simple text-based protocol by which clients can communicate with the mpd daemon and with one another. From the perspective of mpd, mppopm & mppopmd are just new clients. Via this protocol mppopmd will monitor mpd for song playback & note when songs complete; this is how it knows to increment the playcount & update the last played timestamp for each song to which you listen.

Also via the protocol, mpd clients can communicate with one another by registering "channels" and subscribing to them; one client can send a message to a given channel, and all other clients subscribed to that channel will receive that message. In particular, if an mpd client sends the "rating" command to the mppopmd commands channel (the channel name is configurable, but defaults to "unwoundstack.com:commands"), mppopmd will set the rating for the given track.

The mechanism by which mppopmd records this information (i.e play counts, last played and ratings) is mpd stickers. A sticker is a little bit of textual information which clients can attach to songs in the form of a name-value pair. mpdpopm defines a new sticker name for each of these items & udpates the values for each song when & as requested.

Of course, other mpd clients can't be aware of mppopmd, its command channel, or the stickers it sets: you the user will have to bridge that gap. You could of course just fire-up netcat & start sending commands, but that's not particularly convenient. That's where mppopm comes in. It is a small command-line tool for controlling mpdpopm functionality; you could for instance say:

mppopm set-rating '*****'

to rate the current track at five stars. Under the hood, it connects to the mpd daemon, sends a "rating" message to the mppopmd commands channel, and mppopmd, in turn, tells mpd to set the rating sticker for the curren track to 255 (more on the rating system below).

Getting Set-up

MPD

If you're reading this, I assume you already have MPD up & running, so this section will be brief. One note, prompted by user m040601, however: as mentioned above, mpdpopm leverages the MPD sticker database. I was chagrined to find that if you do not configure MPD to maintain a sticker database, all sticker commands will simply be disabled. Therefore, before setting up mpdpopm, find your mpd configuration file and check to be sure you have a sticker_file entry; something like this:

sticker_file "/home/sp1ff/lib/mpd/sticker.sql"

Check also that you have write access to the named file & its parent directory.

mppopmd

I haven't yet provided a systemd service unit, so you'll have to run the daemon process manually. That begins with preparing a configuration file, the installation of which I haven't implemented yet, either (so you'll have to do it by hand).

  1. Configuration

    The configuration file is optional, so you can skip this part if you like. However, if you want to customize the behavior of the mpdpopm daemon, you'll need to set one up, so I've documented it here in the form of a worked example.

    The format is a LISP S-expression in which individual, top-level items are expressed as cons cells:

    ((log . "/home/sp1ff/var/log/mppopmd.log")
     (host . "192.168.1.6")
     (port . 6600)
     (local_music_dir . "/mnt/my-nfs-server/mp3")
     (playcount_sticker . "unwoundstack.com:playcount")
     (lastplayed_sticker . "unwoundstack.com:lastplayed")
     (played_thresh . 0.6)
     (poll_interval_ms . 5000)
     (playcount_command . "/home/sp1ff/bin/scribbu")
     (playcount_command_args . ("popm" "-a" "-A" "-b" "-o" "sp1ff@pobox.com" "-C" "%playcount" "%full-file"))
     (commands_chan . "unwoundstack.com:commands")
     (rating_sticker . "unwoundstack.com:rating")
     (ratings_command . "/home/sp1ff/bin/scribbu")
     (ratings_command_args . ("popm" "-a" "-A" "-b" "-o" "sp1ff@pobox.com" "-r" "%rating" "%full-file")))
    

    Entries:

    • log: file to which the daemon will log when running in the background (if you specify the -F flag, to run it in the foreground, it will log to stdout); no log rotation is provided, so either setup logrotate or keep an eye on it so it doesn't grow too large.
    • host: IP address on which the MPD daemon is listening (should match bind_to_address in your mpd configuration file); defaults to "localhost"
    • port: port on which the mpd daemon is listening (should match port in your mpd configuration file); defaults to 6600
    • localmusicdir: the root of your MPD music directory, as seen from this host
    • playcountsticker: the sticker name mpdpopm will use to store play counts (defaults to "unwoundstack.com:playcount")
    • lastplayedsticker: the sticker name mpdpopm will use to store the last played timestamp (defaults to "unwoundstack.com:lastplayed")
    • ratingsticker: the sticker name mpdpopm will use to store the rating (defaults to "unwoundstack.com:rating")
    • playedthresh: the percentage of a song's duration that must be played back in order for it to be considered as "played", expressed as number between 0 & 1 (defaults to 0.6)
    • pollintervalms: the amount of time, in milliseconds, between polls of mpd by mppopmd to check on playback progress
    • commandschan: name of the MPD channel on which mppopmd shall listen for commands ("unwoundstack.com:commands" by default)
    • playcountcommand: optional name of a program to run when the play count is incremented; this should be an absolute path. use playcountcommandargs to specify arguments to this command
    • playcountcommandargs: an array of arguments to specify to the playcount command; arguments may contain replacement parameters that will be filled in at the time of execution (on which more below)
    • ratings: optional name of a program to run when the rating is set; this should be an absolute path. use rating to specify arguments to this command
    • ratingscommandargs: an array of arguments to specify to the ratings command; arguments may contain replacement parameters that will be filled in at the time of execution (on which more below)

    There is an additional configuration parameter not documented here: gen_cmds. That is described below.

  2. Starting the Daemon

    Once you've got your configuration file prepared, you should probably start the daemon in the foreground for ease of trouble-shooting. I suggest starting it with the -v flag ("verbose") the first time, as well (there's also a -d flag which will produce more copious debug output). You should expect to see something like this:

    mppopmd -v -F -c <path to configuration file>
    [2020-12-12T15:26:19.620806454-08:00][mppopmd] mppopmd 0.1.14 logging at level Debug.
    [2020-12-12T15:26:19.621395828-08:00][mpdpopm] mpdpopm 0.1.14 beginning.
    [2020-12-12T15:26:19.621998677-08:00][mpdpopm::clients] Connected 0.22.0.
    [2020-12-12T15:26:19.623398521-08:00][mpdpopm::clients] Connected 0.22.0.
    [2020-12-12T15:26:19.623874861-08:00][mpdpopm::clients] Sent subscribe message for unwoundstack.com:commands; got `OK
    '.
    [2020-12-12T15:26:19.623888424-08:00][mpdpopm::clients] Subscribed to unwoundstack.com:commands.
    [2020-12-12T15:26:19.624985027-08:00][mpdpopm] selecting...
    [2020-12-12T15:26:19.628412738-08:00][mpdpopm] output status is Ok(
        Output {
    	status: ExitStatus(
    	    ExitStatus(
    		0,
    	    ),
    	),
    	stdout: "/home/sp1ff\n",
    	stderr: "",
        },
    )
    [2020-12-12T15:26:19.628778521-08:00][mpdpopm] No database update needed
    [2020-12-12T15:26:19.628817190-08:00][mpdpopm] No more commands to process.
    

    At this point the daemon is just sitting around, waiting for something to happen. Bring up your favorite mpd client & start playing a track. That should induce some activity:

    [2020-12-12T15:26:29.522581696-08:00][mpdpopm::clients] Sent idle message; got `changed: player
    OK
    '.
    [2020-12-12T15:26:29.522756287-08:00][mpdpopm] subsystem Player changed
    [2020-12-12T15:26:29.527064915-08:00][mpdpopm::playcounts] Updating status: 0.000% complete.
    ...
    [2020-12-12T15:28:19.653519123-08:00][mpdpopm::playcounts] Updating status: 60.698% complete.
    [2020-12-12T15:28:19.653569350-08:00][mpdpopm::playcounts] Increment play count for 'M/Miles Davis - Boplicity.mp3' (songid: 262) at 0.6069790770994554 played.
    [2020-12-12T15:28:19.661696678-08:00][mpdpopm::clients] Sent message `sticker get song "M/Miles Davis - Boplicity.mp3" "unwoundstack.com:playcount"'; got `sticker: unwoundstack.com:playcount=3
    OK
    '
    [2020-12-12T15:28:19.661743547-08:00][mpdpopm::playcounts] Current PC is 3.
    [2020-12-12T15:28:19.770956673-08:00][mpdpopm::clients] Sent `sticker set song "M/Miles Davis - Boplicity.mp3" "unwoundstack.com:lastplayed" "1607815699"'; got `OK
    '
    [2020-12-12T15:28:19.868244915-08:00][mpdpopm::clients] Sent `sticker set song "M/Miles Davis - Boplicity.mp3" "unwoundstack.com:playcount" "4"'; got `OK
    '
    ...
    

    In this example, mppopmd noticed that "Boplicity" by Miles Davis started playing; when it was played 60% of the way through, the daemon updated the play count from 3 to 4 & set the last played timesetamp.

    Once things seem to be working, you might consider removing the -F flag & running mppopmd as a proper daemon.

mppopm

At this point, mpdpopm will happily monitor your playback history & keep play counts & last played timestamps for you. If you would like to rate tracks, however, you will need to somehow induce your favorite mpd client to send a "rating" message to the mpdpopm commands channel ("unwoundstack.com:commands" by default). Since this is unlikely to be convenient, I wrote an mpd client for the purpose: a little CLI called mppopm. You can simply execute

mppopm set-rating '*****'

to set the current track's rating to five "stars" (say mppopm --help for an explanation of the rating system; in brief– it's Winamp's). NB. the set rating command by default produces no output; if you want confirmation that somethings happening, use the -v flag.

The CLI offers "get" & "set" commands for play counts, last played timestamps & the rating. Say mppopm --help for a full list of options, including how to tell it where the mpd server can be found on your network.

Advanced Usage

Keeping Other Repositories of Information Up-to-Date

One problem that arises is that you may have the information managed by mpdpopm replicated in other locations. For instance, I have play counts also recorded in the PCNT & POPM ID3 tags attached to my music files. I record ratings in the POPM tag, as well. For that reason, mppopmd has the ability to run arbitrary commands after updating stickers. I developed this for the purpose of keeping my ID3 tags up-to-date, but one could do anything (pop up a notification, update a database &c).

I will again illustrate through a worked example: keeping ID3 tags up-to-date. I use scribbu, a little ID3 tagging utility I wrote to keep my tags up-to-date. The following two lines, placed in your configuration file:

(playcount_command . "/home/sp1ff/bin/scribbu")
(playcount_command_args . ("popm" "-a" "-A" "-b" "-o" "sp1ff@pobox.com" "-C" "%playcount" "%full-file"))

will cause mppopmd to invoke the command /home/sp1ff/bin/scribbu popm -a -A -b -o sp1ff@pobox.com -C %playcount %full-file every time it updates the play count sticker. The tokens %playcount & %full-file will be replaced with the new play count and the absolute path to the file backing the current song, respectively.

There are two such options: playcount_command & ratings_command. Both support the replacement paramter %full-file, which, at the time of execution, will expand the the absolute path to the file backing the current song. The playcount command also provides the replacment parameter %playcount, which will expand to the new play count (expressed in base ten). The ratings command provides the %rating parameter, which will expand to the new rating, expressed as an integer betweern 0 & 255, inclusive (again in base ten).

Generalized Commands

The idea of executing arbitrary commands proved useful enough to me that I generalized it: I set up a mechanism whereby the user can configure arbitrary server-side commands in response to messages received on the mpdpopm commands channel. As an example, an entry in the configuration file like this:

(gen_cmds .
	  (((name . "set-genre")
	    (formal_parameters . (Literal Track))
	    (default_after . 1)
	    (cmd . "/home/sp1ff/bin/scribbu")
	    (args . ("genre" "-a" "-C" "-g" "%1" "%full-file"))
	    (update . TrackOnly)))

will define a new command "set-genre", with two parameters, the second of which can be omitted (it will default to the current track). When mppopmd receives this command (i.e. when a client says something like:

sendmessage unwoundstack.com:commands "set-genre Rock"

mppopmd will invoke /home/sp1ff/bin/scribbu like this:

/home/sp1ff/scribbu genre -a -C -g Rock "${music-dir}/${song URI}"

where the "psuedo-variables" music-dir and song URI above will be replaced with the configured music directory and the current song's URI.

The configuration is perforce more complex because we have to, at configuration-time, define a mapping between the actual parameters supplied by a client in the message to "unwoundstack.com:commands" and the replacement parameters used in the command arguments. The command's replacement parameters are defined by a simple list, given in formal_parameters, of parameter types. At this time, there are only two formal parameter types:

  1. Literal :: the actual parameter shall be copied verbatim into the replacement parameters, under the name "%i" where i is the one-based index of this formal parameter, expressed in base ten
  2. Track :: the actual parameter will be interpreted as a song URI; it again may be referred in the replacement parameters as %i. Only one Track argument may appear in the list of formal parameters.

Actual parameters after index default_after (counting from one) are optional; if not specified the replacement parameter will take the default value for its type ("" for literals, the currently playing song's URI for tracks).

Two additional parameters are available:

  1. current-file :: the absolute path to the currently playing track (if any)
  2. full-file :: if the list of formal parameters contains a Track argument in slot i, the actual parameter will be interpreted as a song URI, %i will return the absolute path to that file (i.e. with music_dir prepended to the actual argument), as will %full-file

Finally, some commands (such as "set-genre=, above) may change your music collection in such a way as to necessitate an mpd database update after they complete. The update configuration item governs that: it may be set to "NoUpdate", "TrackOnly", or "FullDatabase" to indicate that this command will require no update, an update to the song named by the Track parameter only, or the full database, respectively.

Status & Roadmap

I am currently using mpdpopm day in & day out with my music collection, but it's early days; I have chosen the version number (0.1) in the hopes of indicating that. Right now, mpdpopm is the bare-bones of an app: it's plumbing, not the sink. You can use the mppopm CLI to, say, rate the current song, but in order to actually do anything with that rating in the future, you're going to have to write some kind of mpd client for yourself. For example, I have a shell script that queries the sticker database for all songs with no rating & queues 'em up for listening. I have some Emacs LISP that queues up all songs with a rating > 128. But these are quick-and-dirty ways to leverage mpdpopm while I figure out where it should go.

To that end, I plan to add a filter feature that would extend the MPD "find" capability to allow queries that include the stickers that mpdpopm manages– so you could, for instance, say:

find (artist =~ foo) and (rating > 175)

MPD would handle the "artist =~" clause & mpdpopm the "rating >" clause, as well as combining the results.

Windows support may be some time coming; the daemon depends on Unix signal handling, the MPD Unix socket, and the Unix daemon logic, especially fork & exec… if you'd like to run it on Windows, let me know– if there's enough interest, and I can get some kind of Windows VM setup, I'll look at a port.

Suggestions, bug reports & PRs welcome!