Devlog: Listenbrainz Widget Part 1 – Scrobbling

Unless I've broken it since writing this, there should be a little widget near the bottom of each page on this site indicating the music I am listening to, or I most recently listened to. Developing this has been one of those situations where I could see a series rakes stretching out before me, and I intentionally traced a route that meant I stepped on as many as possible.

Like all respectable personal websites, there is a relatively ill-defined yet strictly adhered-to Philosophy underpinning most of the design and architectural decisions going into why this site looks/works the way it does. Maybe I will write them up somewhere but for now they are:

  1. Purple is the best colour. Things should be purple as often as possible.
  2. This is a static website. When a visitor's browser requests something/index.html that should be a real file in a corresponding directory on my server's filesystem.
  3. I built this website. The markup/css/js that the user receives was written by me, and I know exactly why each line needs to be there.
  4. Cache everything. If an external API is queried, or some data-processing is done, cache the result and reuse it.
  5. Nothing matters and I can do anything I like, all the time.

Which brings us to step 1:

Scrobbling

If you're not familiar, a Scrobbling service tracks what music you listen to and when. By connecting it to every streaming service and music player you use, you can achieve a kind of Spotify Wrapped, but across all the music you ever listened to, regardless of source. It also lets you broadcast the music you're listening to right now.

My Scrobbler of choice is ListenBrainz↗. All my digitised CDs have been tagged with the appropriate MusicBrainz IDs, and the music players I used (Kodi + a self-hosted Navidrome instance) support scrobbling to ListenBrainz-style services.

Rather than query the ListenBrainz now-playing endpoint for every visit to a page on my website, I needed a way to keep track of my now playing track myself. To achieve this, I pretend my Raspberry Pi is a ListenBrainz service. All queries go to it, and are forwarded on to the actual ListenBrainz website. To avoid doing this routing in some kind of bespoke Node.js application, it is entirely done in NGINX, using a combination of a reverse-proxy, and mirror directive:

location /apis/listenbrainz {

  location /apis/listenbrainz/1/submit-listens {
    mirror /apis/jukebox/submit-now-listening;
    rewrite /apis/listenbrainz(.*) /$1 break;
    proxy_pass https://api.listenbrainz.org;
  }

  rewrite /apis/listenbrainz(.*) /$1 break;
  proxy_pass https://api.listenbrainz.org;
}

location /apis/jukebox/submit-now-listening {
   internal;
   proxy_pass http://localhost:31415;
}

In a nutshell, calls to the /apis/listenbrainz/ endpoint are redirected to the actual ListenBrainz website using the proxy_pass directive, after stripping my base-url with the rewrite. The mirror directive in the nested location block specifically intercepts calls to submit-listens and (as well as passing them on to ListenBrainz) also mirrors them to the Node.js app which handles all the dynamic content of my site. The benefit of this approach is that all my music players can pretend they are communicating with the real ListenBrainz service directly, and I can route calls to the internal API endpoint I want without having written a single line of code yet. I'm not convince the duplication in the nested location block is necessary, but this was the only way I could get it to work.

For now it suffices to say that calls to the submit-listens endpoint have enough data (track title, album name, artist(s), track length and much more besides) to get the very basic functionality working. With a couple of lines of client-side JavaScript I can show visitors the current I am currently listening to. But there is so much more to do! In the next installment, we'll tackle how the album artwork is dealt with, and at last enter the magical world of Node.js.

Epilogue

I said at the beginning there were rakes aplenty just waiting to be stepped on. I've glossed over a dozen or so in the explanation above because each time I tried to describe them my heart started palpitating and/or I lost the will to live. One illustrative example I wanted to highlight though is Kodi↗'s ListenBrainz plugin. It shows the kind of scope-creep that can be hidden inside a use case like 'I want to show visitors what I'm listening to'.

Somewhat surprisingly, the ListenBrainz plugin is broken. Then again, I guess it's up to me to write a fix and get a pull request approved in the official repository. That's an adventure for another day though.

Essentially, the plugin allows you to set a URL other than https://api.listenbrainz.org as your preferred API endpoint. In my case, I wanted it to be https://roleplayingjames.net/apis/listenbrainz. The trouble is the plugin keeps wiping out everything past the '.net'. This is a fairly easy mistake to make, one I've made before, but it's also relatively simple to fix. Which is what I did↗.

Unfortunately this lead me discover a further issue. My music is tagged by a program call beets↗, which does some clever things to apply IDs to your music files which different media players can understand. The ListenBrainz plugin has some clever mapping↗ for these tags, but they're case sensitive! So my scrobbles from Kodi are, as of time of writing, not working.