Tree / Self Hosting / Music
Library Restoration
Rebuilding a private music collection of 175,905 audio files into one consistently tagged, named and sorted library, backed by a local MusicBrainz mirror.
- Status
- Yellow
- Type
- workflow
- Hours
- about 238
- Tokens
- about 170 M
The library is rebuilt and most of it is clean, but with this many files the right workflow is hard to find, and we are still working on it.
In this article
A collection without a system
My music had grown for years without any rule. At the start it was 1.14 TB, 192,587 files, 175,905 of them audio, in about 12,000 albums. It was ordered by where the music came from, not by artist, in 29 top folders. 68 % of album folders carried only the album name and the rest used five other patterns. Depth ran from one to six levels. Required tags were complete on 61 % of the files, 10 % had no tags at all, MusicBrainz IDs were close to zero, and only 23 % had an embedded cover. File names came in at least six shapes, from track and title to scene release names. 2,204 titles lay in several collections at once.
The goal I set was simple to say: order, once and for good. Every title fully tagged, none falling through. One cover per album. A folder icon made from that cover in every album folder, so that the file manager shows covers instead of yellow folders. One rule for every file, folder and tag, with no exceptions and no leftover folders. And no review lists for me: the process had to run automatically, with unclear cases decided by a fixed rule rather than handed back to me.
The new library is built in a separate tree. The originals are only read, and the new tree replaces them at the very last stage, which only I can declare.
The target: conventions before code
Before any file moved, we settled what the finished library should look like. Agents read the source code of Kodi and Jellyfin, the documentation of beets, Picard and other tools, and forum practice, and wrote studies on naming, tagging, covers, NFO files, artwork, playlists and the handoff to media servers. Where sources disagreed, the beets standard won. I confirmed each rule.
The result is a long convention document. Some of its rules exist only because the source code showed a trap:
- A folder of samplers must not be called
Compilations, because Jellyfin takes every folder holding album folders for an artist. Samplers sit underVarious Artists. - Single tracks go into a Non-Album folder inside the artist folder, not the other way round, otherwise a fake artist called Non-Album appears.
- An artist whose name equals one of Jellyfin's special subfolder words needs a suffix.
- The front cover is
cover.jpg. A field study found it read by 14 of 14 documented readers, against 12 of 14 forfolder.jpg. - Everything ends up as MP3. Non-MP3 audio is converted to 320 kbit/s, existing MP3 stays byte for byte. That was my decision for uniformity.
Non-music needed its own answers. Sound effects, ringtones, jingles, radio plays and comedy sat between the songs. A study proposed the Universal Category System for effects and album logic for spoken works. Nothing there is thrown away. It moves to an area outside the media servers. Soundtracks count as music, and film and game soundtracks were included in the scope, which raised it to 376 collections.
How the pipeline works
Harvest. One ffmpeg pass over all 194,005 audio files, soundtracks included, gave audio checksum, SHA-256, decode errors, Chromaprint fingerprint, loudness, duration, bitrate, original tags and embedded images. It took 12.7 hours at low priority and measured 13,531 hours of playing time. It found 16,860 groups of bit-identical sound with 23,777 surplus files, and at least one decode error in 22,057 files.
Identification ladder. Each file climbs until something knows it: a MusicBrainz match through beets, then an AcoustID match by fingerprint, then loose single files, and finally a clean "as found" stage that tags from what exists. A real test on 52 folders set the match limit. Right hits lay between 0.15 and 0.22, wrong ones at 0.35 or above, so the limit moved from 0.15 to 0.25.
Local MusicBrainz. A mirror in Docker with 5,858,206 releases takes the public rate limit out of the equation.
Duplicates by sound. Bit-identical audio is grouped. The winner is lossless first, then bitrate, then the complete album, then better tags.
Naming. One function computes the target path from the tags, so path and tags cannot disagree.
Checks. Rule checks over the whole tree, planted bait errors, blind random samples by two checkers.
Safety. beets only ever touches staged copies, checked by SHA-256 against the harvest, behind a write guard in every script. That guard exists because a test showed that a plugin could reach originals. Every write run keeps a journal with before and after values and a way back.
Tools and models
- beets 2.14.1 with 16 plugins, among them musicbrainz, chroma, fetchart, embedart, lastgenre, ftintitle, replaygain, mbsync and my project's own plugin.
- ffmpeg with Chromaprint, fpcalc, mutagen, mp3val and flac for reading and checking. Picard is in the toolbox as reference.
- MusicBrainz, AcoustID, the Cover Art Archive, Discogs, fanart.tv and TheAudioDB as data and image sources.
- Docker, WSL2 and Solr for the mirror.
- Python for everything else: 304 scripts in the end, mostly one per class of error.
- Claude Code agents as planner, builder, checkers and readers. Sonnet ran the sample checks in the design, Opus the daily improvement run.
Some settings were switched off on purpose after an audit of the configuration. beets' scrub would have removed ratings, cover_format would have deleted source images, and a removal suggestion in importsource can delete source folders. No machine learning model does the identification. The intelligence here is fingerprints, a reference database and agents that read names.
Planner, builder, four checkers
Claude Code agents planned, coded and ran all of this. The idea behind the checking was simple: nobody who changed something could also accept it. Every change went through a planner, a builder and four parallel checkers, with up to two repair rounds, and each collection went through its own acceptance. Decisions, errors and special cases lived in binding registers that every role had to read.
I set the goal, confirmed the conventions, gave the go for large runs and looked at the result. Deleting originals stays with me.
Why this is so hard
This is the real story of the project. With so many files it is hard to find the right workflow, and we are still working on it. Looking back through the project, these are my own conclusions about why.
You cannot look at it. At about 154,000 titles, thirty seconds each would be more than 1,200 hours of pure checking. So everything rests on rules and samples, and those only see what someone thought of.
There is no complete truth to compare with. Where MusicBrainz knows a title, the result is excellent: 119,555 of about 120,900 anchored titles match it, 98.9 %. Of the 971 that really differ, most do so because AcoustID supports our tag or because of a deliberate rule. But a large part of a private collection has no anchor. 3,563 of 10,139 artist names do not stand exactly in MusicBrainz, mostly collaborations and unknown names. DJ mixes, samplers from old CDs, downloaded singles and scene releases carry no reference at all. For those titles "correct" means only "most plausible".
The sources contradict each other. A fingerprint is strong evidence and still wrong sometimes. One DJ's tracks were matched to a fantasy author. In 2,322 titles AcoustID named a different main artist than the tag, and that could not be settled by rule, because aliases, cover versions, composers and performers all look the same to a script. Own tags are worse: web addresses as artist, artist and album in one field, encoding garbage, album names cut off at 30 characters.
The rules only appear while you work. A convention can be written in advance, but the exceptions live in the data. Every collection brought new cases: MP3 hidden in WAV containers, video files read as audio, stacked ID3v1 blocks, one "mixed by" tag that would turn a whole mixed folder into a DJ mix. Each new rule meant re-running what was already finished.
Long runs make slow feedback. One large collection of 59,516 files took about 40 hours through all steps. beets imports one album after another, about 9 seconds each, on one core and one SQLite database. The mirror answered in 0.16 seconds and was never the brake. A wrong assumption can cost days before it shows.
Checkers are blind in the same way. Green results proved less than they seemed to. When a reader went through 800 names and marked 55 oddities, the tree check had found 7 of them and the artist scan 1. A hand sample of 40 titles without any finding showed visible faults in about 12. One rule in the tree check had been dead without anyone noticing. Agent readers are one model with similar prompts, so a second opinion is not truly independent, and two checkers on the same sample agreed on only about 60 % of their findings. And about 30 % of what readers reported in one count was a matter of taste, not an error.
Repair creates new errors. Every library-wide fix touches thousands of files on the strength of a few examples. One rule drawn from one finding left 36 albums wrong. Splitting albums with mixed editions was right for most, but 14 of 132 new albums turned out to be duplicates. Meanwhile every repair changes the state that the last check measured.
"Perfect" cannot be proven. Zero findings in 1,000 titles only prove an error rate under about 0.3 %. So the honest aim is a stated error rate plus zero violations of formal rules, not a promise.
What we tried
- Did not work: importing collection by collection. Each new rule forced re-runs of finished collections, and artwork was fetched before albums had their final place.
- Worked: "everything once". Rules frozen, each file once through identification, special cases collected instead of fixed on the spot, artwork last. It got everything into the tree.
- Did not work: gates as proof. The library was sorted and all gates stood at zero, yet many names were wrong, because the gates only compared structure.
- Worked: my own walk through the folders. I saw in the file manager that almost all artists in two letter folders were wrong. About 2,800 titles had unusable artist fields that no scan had caught. That is where the tree check came from.
- Worked: a tree check with bait. 18 rules, each with a planted fault, took hard findings from 5,259 to 11 explained exceptions.
- Partly: agent readers over all artist and album folders. They found typos no script finds, and about 2,000 reports came back, but 18 such sniffers alone proved nothing. Fixing, not finding, was the bottleneck: about 540 of 780 high findings stood unchanged.
- Worked: using the fingerprints properly. A full AcoustID run gave 143,600 hits, 89 %, and re-rating old hits against the original file name recovered titles that had been rejected wrongly.
- Partly: text rules for names, such as title case and apostrophes. They fixed thousands of titles and work well with a MusicBrainz anchor, but English capitalisation without a source stays a matter of judgement.
- Partly: blind acceptance rounds of 600 titles with two checkers. Findings went 18.5, 11.0, 8.2 and 10.8 %. Each round revealed a class no detector knew and it was fixed library-wide, but the rate then stopped falling. What remains is a long tail of single cases without a mechanical rule.
- Did not work: more of the same rounds as a way to finish. A plateau does not end by itself.
- Worked: a slimmer method for Silver. A plain inventory read of all 225,421 files in 58 minutes, 14 small rule checks on that file, a bait test in which all 14 baits were found, then each class of error fixed library-wide, for example the disc field from 16,203 findings to 0. Part of the drop comes from corrected rules, part from real repair.
- Did not work: a full decode as a separate audit step at the very beginning. It was stopped early and later returned in a better form as the harvest.
- Rejected: writing finished databases straight into Kodi and Jellyfin. A study showed it is feasible, because their check values can be rebuilt from the source code, but it stays a last step at most. Tags, NFO and images do the handoff.
Close to Silver, and not there yet
The stages are Silver, artwork and playlists, Gold. Silver means the file system is right: everything in place, named and tagged, with a frozen inventory as reference. We are close, not there.
- About 154,000 titles are in the new tree.
- 8,401 files still lack a required tag, at the last split about 7,500 a year and 1,400 a track number. MusicBrainz and Discogs do not know them reliably. Years were filled only where at least two releases agreed, and I would rather leave a field open than guess.
- 205 files still sit under a placeholder, about 190 of them real titles without a known artist.
- A pre-check of 500 titles by two checkers put high-certainty errors at 1.2 to 1.6 %.
The biggest blind spot is still ahead. Nobody has yet seen what Jellyfin and Kodi actually show from this library. A test on a safe copy is planned, because that view is the one that counts.
What comes next
Decide what to do with the open placeholders and the fields without a source. Then a fresh sample of 1,500 titles by two kinds of checker with no new class of error, and the inventory is frozen. After that artwork for the whole library at once, new playlists, the media server test and one repair round. The theme music of 42 games in Playnite, copied from the originals through Playnite Bridge for now, is then swapped for the finished library tracks. Gold comes on my word, and only then can the originals go. Until then the workflow itself remains the open work: each round taught us something about how a library of this size has to be handled.