How I’m Streaming Music
2026-05-30
In an earlier post I wrote that I was planning to use Jellyfin for music streaming. I was, but that plan changed. The client landscape for Jellyfin wasn’t really strong enough for my liking, and overall I got the impression that the music experience on Jellyfin is a little less polished. Fortunately for me, this market is well served by a few other self-hostable services.
The one that I chose is Navidrome. It is dead easy to install, and after setting it up a couple of months ago I have not looked back. I have client applications installed on everything I use, and it integrates nicely with last.fm. I’m happy as a clam.
Navidrome is great and I recommend it, but I didn’t write this blog post to wax romantic about Navidrome. What’s more interesting is what Navidrome doesn’t do, and hence what we need to use something else for. Playing music with Navidrome works great. However, it opts out entirely of library management, and that workflow is complex enough that I’m writing this post for my own benefit, more than anyone else’s.
Library Management
Navidrome maintains an extremely narrow focus on playing music only. Its library is built entirely from metadata tags of the audio files in its library. You point it at a directory, and it scans it for files, ignoring all directories and file names on its way. This means that if you want to rename a song or update an album artist, you need to do that in the file’s metadata before adding it to Navidrome.
This is a good thing, and honestly a courageous stand against feature creep by the maintainers. It also forces users to maintain a good library, which I like a lot. It’s like forcing me to clean my room before going out to play with my friends.
And helpfully enough, Navidrome has a really good documentation page explaining how tags are used, and how you can tag your own library, going as far to recommend several tools.
Introducing Beets
I chose beets, which is a command line app that bills itself as “the media library management system for obsessive music geeks”. I don’t want to get ahead of myself, but beets might be my favourite open source project of all time.
While beets is awesome, the learning curve is steep. I really recommend reading the documentation thoroughly before getting into it. That’s not the chore it sounds like because this project’s documentation is immaculate. It’s written by a real person who is funny and knows what they are talking about. It’s not generated, it includes personal apologies for rough edges, it explains the thinking that led to things working the way they do, and it was truly a pleasure to read. I couldn’t shut up about how good these docs were while I was doing this. This page on the autotagger is I think the best time I’ve ever had reading documentation.
To give a high level overview, beets maintains a library for you, which is a directory that contains audio files. Alongside the file tree, it also maintains a SQLite database on disk. I have a directory in my home called Music Import, and I dump new audio files into that. From there, I run beet import ./Music\ Import and it does its magic. That magic is essentially divining metadata from the files based on their file names, directory grouping, and existing tags, and then searching for things against Musicbrainz’ database, and updating the tags so everything is complete and correct.
Where I need to fix things up manually, it pops me into $EDITOR, and I can update the tags that are relevant. This was particularly useful with a lot of my dad’s music, which is not available on Musicbrainz and which he had not tagged up to my standards.
The configuration I had for beet to do this is this:
---
directory: /srv/media/music
library: /srv/media/music/beets.db
import:
move: true
plugins:
- musicbrainz
- fromfilename
- convert
- edit
edit:
itemfields: track title artist artists album albumartist albumartists composers tracktotal albumdisambig genre grouping subtitle lyricists
convert:
auto: yes
dest: /srv/media/music
threads: 10
format: flac
never_convert_lossy_files: yes
delete_originals: yesThis tells beets that my music library is stored at /srv/media/music (/srv/media is a replicated ZFS dataset that I have configured to store large media like images, audio, and video), and that when I import something, it should move it there.
Beets has a bunch of plugins, some of which are bundled, some of which need to be installed separately. That’s a little too much complexity for my tastes so I have only enabled plugins that are packaged alongside the programme itself. The ones I use are:
musicbrainz: MusicBrainz is an online freely available database of music. This plugin adds it to the autotagger, so that when I import music, it lines up the metadata with albums in here, if they exist.fromfilename: By default, themusicbrainzplugin searches for albums based on the existing metadata attached to a given audio file. I had several albums that did not have good tagging, but did have good filenames. This plugin adds the ability to infer metadata from filenames, and then use that to find a match in MusicBrainz.convert: As long as you have FFmpeg installed, this plugin allows audio files to be converted with thebeet convertcommand.edit: I have a lot of music that does not exist on any online database, and I’m more pedantic than the people who gave it to me. This plugin allows me to open up$EDITORto manually tweak metadata where necessary. It works at import time, or it can be manually invoked withbeet edit
After the plugins I have my own configuration for some of those plugins. The long list of itemfields in the edit section are fields that I added while trying to get some data to tag correctly. (I was unsuccessful, see “Workflows” just below.) My configuration for the convert plugin will automatically convert any losslessly compressed (or uncompressed) audio into FLAC.
Workflows
Importing New Music
When I get a new album (I’ve found Bandcamp and Qobuz to be excellent stores for DRM free music), I rsync it to a specific location (~/Music Import) on my server. Then on the server, I run beet import '~/Music Import' and let it do its magic. If the format of the audio files is something uncompressed like WAV, this command will transcode it prior to importing it.
Navidrome scans its media directory periodically for new things, and I typically wait for that to happen. However, early on in the process, I was anxious to see whether it worked, so I would feverishly rescan on demand. The system is solid enough these days that I don’t need to be quite so close to the machine.
Querying
The beet list command reads from the beets database, which is a SQLite database describing your music library. Ideally, it will agree with the filesystem, but computers are not always operating under ideal conditions. Many other commands (move, remove, update, edit) use the same query language as beet list, so I’ve found it to be a waypoint on my journey towards some ambitious goal like getting my albums to look right.
A query comprises a series of space-separated field:term clauses, and the library will be filtered to those items where $field contains $term. The most useful ones I have found are format (e.g. format:mp3) and artist (e.g. artist:Mumford), although more are available (run beet fields to see all available fields), and I am only scratching the surface of the real power of queries.
Updating Things In Place
I recently purchased a special edition of an album. It was so special in fact, that one of the tracks did not appear in any known database. Musicbrainz did its best to match the other 15 tracks on the album and this one I had to add. I matched up all of the metadata fields using the edit mode during the import, but once it was in Navidrome, I had two different albums.
The problem is that Navidrome relies on some (unknown to me) combination of fields to disambiguate albums, and those fields were not exposed in the UI of the edit plugin by default. I tried for a little while to add fields to my configuration, but I couldn’t get it to work.
Fortunately, beet edit has a flag --all which puts every available metadata field into the editor. This was a little overstimulating, but after painstakingly updating every common field to match (and then rescanning), my problem went away, and I now have my one cohesive Golden Wattle edition of Florescence.
A Note on SELinux
I have written about how my humble server has SELinux turned on on my server. Because Navidrome (and others) run in containers, I need to make sure that all files and directories have the context type container_file_t. The rules for that are all set up (here and here), but SELinux does not continuously monitor filesystem activity to keep contexts up to date when new files are added. That happens with the restorecon command, so every time I add something to my library, I need to run:
sudo restorecon -R /srv/media/music/
So that Navidrome can actually see it in its container.
Last.fm
I have been a fond last.fm user for many years at this point, and it is very important to me to maintain this corpus of data, especially as I look to lose the (admittedly limited and inaccessible) analysis that music streaming services offer. With Apple Music, I relied on third party applications like Marvis, Albums, and Neptunes to handle this for me.
Navidrome goes in a different direction and supports it on the server side. However, because it’s self-hosted, that meant that I had to create my own “Application” in Last.fm’s console and add an API key to my instance of Navidrome to get this to work. This wasn’t particularly difficult, and was documented well. It’s nice to have one less thing to look for in a client, too.
Clients
I wrote about this at the top, but the client ecosystem for Navidrome is very good. I’ve tried (and purchased) several options, but have gravitated towards a few. On my Mac, I’ve been using Amperfy, and on my phone I have been using Nautiline. But I would recommend that you put absolutely no stock in what I say here, and look at the available options yourself. This is one of these times where software can be joyful and exciting, so I encourage you to take the plunge yourself.
Music is extremely important to me, and maintaining access to the music I love without some rent seeking overlord is a huge part of that. While it’s not the entire reason that I have 150 vinyl records in my apartment (there’s a massive cool factor there for me), it is certainly a driving factor, and it is the primary reason I was motivated to set this up.
When I got this working for the first time, when I had my own music files playing on my phone while out for a walk, going over my own private connection back to my house, I had a grin that could be seen from lower Earth orbit. Self-hosting is fun, if you’re the right kind of person.