Fixing tags on 17k mp3s
Beets is a python CLI tool for fixing up metadata in music collections. I wrote up a guide from my own use on my ~17k track collection because writing instructions is a good way to learn. But also: it’s a power tool built by hobbyists over 15+ years, so the UI is clunky and often misleading. It doesn’t really respect the user’s time and attention, but it’s the least bad tool I could find.
This post has been hanging out in my drafts folder a long time. I started writing these work notes in second person back when I thought this would be a straightforward project, maybe I’d finish and have a tutorial that highlighted the one or two rough spots. That didn’t work out, but I’m not putting in even more time to rewrite it.
Install
Despite spending a few years of my career as a Python developer, I’ve never been able to keep tools written in Python working for more than a few months without some update blowing it up (let alone a dev setup).
I’ve tried many of the various packaging tools over the decades, no real change in MTBF.
I’ve been using a new installer called uv that does keep them working.
To install with aome very useful optional dependencies: uv tool install beets --with bs4 --with librosa --with pylast --with pyacoustid --with resampy --with soundfile
Run: beet --help.
It will load your plugins and they’ll complain about most missing dependencies.
If you see an exception, puzzle out which python module it wants (eg acoustid is in pyacoustid), run uv tool uninstall beets, and rerun the uv tool install adding the missing library to a --with.
Install your distro’s packages for ffmpeg and oggz (may be named liboggz) to check for corrupt files with the badfiles plugin.
First run: auto-tagging
Using beets is broken up into 3+ runs of the software to shake out issues with the config or your library before making any edits to music files.
This first run is about collecting info on your library into the beets database (~/.config/beets/library.db).
Here’s my .config/beets/config.yaml and the config docs.
The important parts to highlight are:
- The
pathsuses filenames I’ve used for ages for compilations and single tracks, you probably didn’t organize your music like this. - Plugins are configured to write into the music files and will do so on this first run. Surprise! So you get to review each plugin’s config individually.
- The
importsection hascopy,move, andwriteall set tofalseto avoid rewriting tags on the first pass. This is to work around a UX problem (described below), but also to give time to build confidence in howbeetsworks.
Optional: review Metadata Source Plugins to configure searching more music info databases.
Run: beet import --autotag --quiet --incremental-skip-later ~/music
This will take hours and print a lot of debugging info. My ~17k tracks took a little over 5 hours, roughly 1 second per track.
--quiet doesn’t quiet output, it skips the process of making manual decisions.
That’ll be the second run, explained next.
On the first run, it will incorrectly print Resuming interrupted import of ~/music.
This is harmless.
If there are errors, rm ~/.config/beets/import.log and ~/.config/beets/library.db to start over from scratch.
But if it crashes or you have to ctrl-c it to finish later, just re-run the same command to resume where you left off.
(Speaking of which, I ran into a crash bug with fromfilename; I re-ran with -v and an added print to the plugin to try to get the filename, but it ran clean. Weird.)
Go looking for problems
Run beet bad | grep "WARNING" | grep -v "No supported tags in the file" at some point to find corrupt and truncated tracks.
The first grep strips out redundant “checker found N errors or warning” messages; the second strips out a warning for untagged files because that’s fine, we haven’t gotten to the tagging run yet.
Don’t enable the badfiles check_on_import config because it ignores --quiet.
Good luck, hopefully you don’t have too many damaged files. Around 4% of my mp3s had minor issues, none unplayable.
There’s a tool called mp3val that can repair mp3s.
Putting it together: beet bad | grep "WARNING" | grep -v "No supported tags in the file" | cut -d'"' -f 2 | grep -i .mp3$ | xargs -d '\n' mp3val -t -nb -f
I had a handful of tracks in other formats (flac, m4a, mp4, ogg) with minor errors, but I was able to quickly convert them: find . -name "*.m4a" -exec bash -c 'for file; do output="${file%.mp4}.mp3"; ffmpeg -i "$file" -vn -c:a libmp3lame -q:a 2 "$output"; done' _ {} \;
Second run: manual tagging
The primary value of beets is “auto-tagger”, which mostly automatically identifies and tags files. When it can’t make a confident decision it asks you, so you use the “auto-tagger” to make manual tagging decisions. The Using the Auto-Tagger doc describes the process.
I split manual tagging out into a separate run because of its UI problems. When the auto-tagger is waiting on a decision from you, it continues scanning files in the background. On your first run there will be a lot of noisy messages (often from plugins), so your decision prompts will be randomly scrolling up and off the screen while you’re trying to read them. There’s no way to pause the background work or repeat the prompt.
Extract a clean list of what was skipped:
Run: grep "^skip " ~/.config/beets/import.log | sed 's/^skip //' | sort -u > /tmp/skipped.txt
Then work through them individually with beet import -I ~/music/whoever or all with cat /tmp/skipped.txt | xargs -d '\n' beet import -I.
The -I ignores if you skipped a directory previously.
Inconveniently, beets says “skipped” to mean both “this is already imported, nothing to do” and “you skipped this and didn’t pass -I”.
Tagging is a slow process with very frustrating UI. I’m not going to make myself mad writing out all the details because nobody would read it. (This situation is why I’ve given away a dozen copies of Badass by Kathy Sierra and occasionally been driven to impart its wisdom via percussive osmosis.)
Between albums beets will stall for seconds or minutes as it waits on API rate limits.
Usually it will continue to print out-of-context and non-actionable debugging messages to the terminal.
While it’s a little nice to know beets hasn’t frozen, when you come back to the terminal you’ll may have no idea what prompt scrolled off the screen.
The only safe thing to type is S to Skip and then hope to find the album on a later run.
If you’re curious how far behind your editing beet has fallen, you can keep sudo strace -p $(pgrep -f beet) -e trace=openat -f 2>&1 | grep -oP '"\K[^"]+\.mp3' | awk '!seen[$0]++' running in another terminal.
Honestly, I couldn’t finish this run. It’s painfully slow, it often misses metadata clues in the filename so I have to manually search and suffer the delay again. And there’s no utility, no incremental progress unless I wanted to take off the safeties and start writing to my collection. So I backed up the database and threw Claude at it, told it to leave existing tags on a couple albums I had tagged deliberately, to apply fixes that seemed real likely, and to make a list for human review of anything seemed genuinely weird. It churned, I spent maybe an hour reviewing the esoterica, done. The point of the project was to take the error rate down from 60%; if an LLM took it down to 1% that’s a lot better than an Sisyphean slog where a human fails to take it down to 0%.
Third run: reviewing and applying tags
Now that the music metadata is correct in the beets database, you can use beet list and beet stats to query the your beets database.
For example beet list -f '"$path","$album","$year","$track","$artist","$title","$genre"' > compare.csv to generate spreadsheet of your key metadata.
The big value is writing this metadata into the mp3s themselves so that every music player can read them.
To see all potential changes, run: beet write --pretend, probably piping its infinite output into a csv or some grep pipeline to cut it down to meaningful decisions.
If everything looks good, run it again without --pretend.
N runs: expansion and maintenance
Run beet import ~/music when you add new music.
Run beet remove -f to tell beet to forget about files that no longer exist.
Run beet duplicates to find duplicated tracks.
Run uv tool upgrade beets to get new versions without losing hours to python packaging stack traces.
Use mbsync to pull in updated metadata.
Run beet mbsync -p to preview changes; run again without -p when happy.