My music listening algorithm
Sun Oct 11 2026
This is the grand sequel to "My road to Navidrome".
Smart Playlists
The other day I gave twenty dollars to Satan because a professional acquaintance swore on the greatness of vibe coding, and I thought that a Godot-based Navidrome client would be a fun thing to try it with. I have to say, "coding" like that is awfully boring, pressing Enter like I'm at a slot machine in a casino, but one good thing came out of it—I learned the Subsonic API, which is how you interact with a Navidrome server. Looking back, I suppose I was doing "vibe coding" wrong, because I was actually reviewing the code and learning something, but I digress.
My point being that this opened the doors to various experiments of how I could actually listen to my collection. Obviously, I had my regular playlists, but the real game changer came with my discovery of smart playlists. They allow you to, basically, write a database query in a declarative manner and store that in JSON format in a nsp file. In my brain, I just call them "dynamic playlists", because there's nothing really smart about the playlists themselves, but I have no doubt that the implementation is pretty smart.
That's about it; you can kinda stop reading now. That was the main thing I wanted to share.
The System
Anyway, the examples in the linked article paint a good picture of what kinda stuff you can do with the smart dynamic playlists. I tried some random ideas until I decided that I want to make sure I've listened to every single track in my library, so this playlist became my daily go-to:
{
"name": "100 unplayed",
"comment": "Tracks with a play count of 0",
"all": [
{"is": {"playCount": 0}}
],
"limit": 100
}
It was really fun listening to my music like this because I felt like I was reaching into the cobwebbed parts of my library and hearing tracks that I forgot I had.
Eventually all tracks got listened to, and this playlist came up empty. I felt some sort of achievement, like I had completed something and there was no more to do. Like getting Prestige in Call of Duty, back in the day.
There was more to do, though. Why not keep going with this? Just raise the floor from 0 to 1. I updated the playlist file with the following contents:
{
"name": "prestige 1",
"comment": "Tracks with a play of less than 2",
"all": [
{"lt": {"playCount": 2}}
],
"sort": "lastPlayed",
"order": "asc"
}
I dropped the limit as my library was modest enough to not cause performance issues when querying. I changed the play count test from equality to a less-than so that new music I add would also land in this playlist so it could be "caught up". Now that I had a play count for the entries, I could sort them so that the ones I hadn't heard the longest would come up first (I eventually realized that an unpredicable order was more important for me, so now I just shuffle it). And I titled it "Prestige 1", because that sounds cool.
Now I had an ever-shrinking playlist which felt like a goal I could work towards while making sure I listen to the deep cuts. I could always go and listen to my popular stuff whenever I wanted, but this felt like I was building a more intimate relationship with my music collection overall, and I'd end up finding more tracks to add to my "main" playlists.
Eventually I also discovered that you can filter on static playlists, so if I wanted to listen to Prestige 1 but was only in the mood for, let's say, metal, I could quickly "fork" Prestige 1 to only include tracks from a metal playlist:
{
"name": "prestige 1 metal",
"comment": "Tracks with a play of less than 2",
"all": [
{"lt": {"playCount": 2}},
{ "inPlaylist": { "id": "1yWtxhL986a4YuF0JzmWUp" } }
],
"sort": "lastPlayed",
"order": "asc"
}
This also kind of exposed the weakness of the standard shuffle. Many tracks in my most popular static playlist had a bump in the playcount only because of my system and not through getting played naturally, so that validated my approach even more.
Clients
For this system to work, you obviously need to use a client which "scrobbles" the music you play, which is LastFM-ese for letting the server know that you've listened to a track. In practice, it utilizes the /rest/scrobble endpoint in the Subsonic API to increase the playCount property of a track by 1.
Some clients, for the iPhone in particular, didn't do that, so here's a list of the software I personally use with Navidrome daily:
- Desktop - Supersonic. Themable, lightweight. Supports ReplayGain. Works for both, Linux and Windows.
- iPhone - play:Sub. My favorite Navidrome client for iPhones out of all I've tried. Great caching system. I cache and pre-cache as much music as I can to save on data, and "play:Sub" scrobbles cached tracks too, as it should. It's paid ($4.99), but it's well worth it.
- Steam Deck - My main gaming machine, currently. At the moment I stream music from my phone to the Steam Deck via Bluetooth. This article outlines the steps. I'm waiting for this Decky Launcher plugin to implement scrobbling so I don't have to use the phone anymore.
Conclusion
I've wanted to make a follow-up to the Navidrome article for a while now. It's been more than a year since I spun up my Navidrome instance, and I've learned quite a lot since then, and I want to share some of that stuff.
If you've got a Navidrome box as well, I recommend you try this system or smart dynamic playlists in general to make your music listening sessions a bit more interesting and varied.