Using beets to Organize a 15k+ Music Collection
A full-album MP3 collection started in the late ’90s, 15k+ tracks I could no longer browse or trust, turned back into a library I can search, check, and listen to, with beets and a set of Claude skills I built on top of it.
· 8 min read
I started collecting MP3s in the late ’90s, and from the start it was about albums: full records, in order, the way they were released. Over the years that habit turned into more than 15,000 tracks across more artists than I can keep in my head, stored in folders named by whoever ripped them, in whatever style made sense that day. The music was all there, but I’d lost track of it. I couldn’t browse it, I couldn’t tell what was broken, and I had no idea what I actually owned.
I didn’t want another streaming playlist. I wanted my own collection back, organized well enough that I could find anything in it and spot what was wrong. beets turned out to be the right tool. It’s a small Python command-line library manager that keeps a database of your collection, tags files from MusicBrainz, and moves them into folders according to rules you write once.
What follows is the setup I ended up with and the handful of decisions that mattered. For a proper installation guide, the beets docs are better than anything I’d write.
Claude drives, beets does the work #
I never learned the beets CLI. Instead I wrote a handful of Claude Code skills that sit on top of beets, one per job: bringing in new albums, putting files where they belong, checking the library’s health, answering questions about the collection. I ask for what I want, and Claude works out the beet commands from the docs, runs the preview, and shows me the plan. My part is deciding what “organized” means and approving that plan before anything moves. beets has a lot of commands and a template language with its own quirks, and I care much more about how my albums are filed than about remembering flags.
Why A to Z folders #
The old layout was a single Music/ folder holding a mix of everything: some proper artist folders, some artist folders that were just a pile of loose tracks, and album folders at the root that never made it into any artist folder. A lot of tracks were missing tags, and many had no cover art. Music players cope with this by reading whatever tags exist, but I still browse the files by hand, and scrolling through hundreds of half-organized folders to find one record got old fast.
So the new layout puts each artist under a letter, the way a record shop splits its bins. A slice of the old top level:
Music/
├── Radiohead/
│ └── OK Computer/
│ └── 01 Airbag.mp3
├── radiohead_kid_a_FLAC/
│ └── 01-everything-in-its-right-place.flac
├── The Beatles/
│ └── Come Together.flac
├── VA - Guardians of the Galaxy Soundtrack/
│ └── 01 Hooked on a Feeling.mp3
└── unsorted/
└── 03-track.mp3After import and move:
B/
└── The Beatles/
└── Abbey Road/
└── 01 Come Together.flac
R/
└── Radiohead/
├── OK Computer/
│ └── 01 Airbag.mp3
└── Kid A/
└── 01 Everything In Its Right Place.flac
Soundtracks/
└── Guardians of the Galaxy/
└── 01 Blue Swede - Hooked on a Feeling.mp3Nobody renamed a folder by hand, not me and not Claude. beets generated every path on the right. The unsorted/03-track.mp3 is missing on purpose: beets couldn’t match it with confidence, so it skipped it instead of guessing (more on that below).
Tags first, folders second #
beets never tries to parse radiohead_kid_a_FLAC/. It matches the whole release against MusicBrainz, which suits a collection built on albums: one match tags every track on the record at once. It writes a canonical set of tags (artist, album, track number, title) into the files, and only then feeds those tags into the path template. The old folder names are disposable, which is why two very differently named Radiohead folders both end up under one clean Radiohead/.
The same tags also land in library.db, beets’ SQLite database, which is what makes the collection searchable: beet list artist:Radiohead albumtype:album instead of grepping folder names and hoping the casing matches.
Where every album goes #
Albums are the unit of this collection, so they’re the unit of the layout: letter, artist, album, tracks in order. The letter comes from a small inline field, and everything that isn’t a regular album gets its own rule in paths::
1item_fields:
2 initial: |
3 name = albumartist or ''
4 for article in ('the ', 'a ', 'an ', 'el ', 'la ', 'los ', 'las '):
5 if name.lower().startswith(article):
6 name = name[len(article):]
7 break
8 return name[0].upper() if name and name[0].isalpha() else '#'
9
10# simplified: the real config also adds a disc prefix for box sets
11paths:
12 albumtype:soundtrack: Soundtracks/$album/$track $artist - $title
13 comp: Various Artists/$album/$track $artist - $title
14 singleton: $initial/Non-Album/$artist/$title
15 default: $initial/$albumartist/$album/$track $title$initial drops a leading article (including the Spanish El, La, Los, Las, since plenty of this collection is in Spanish), takes the first letter, and sends anything that doesn’t start with a letter to #. The first version just took the first character, and it worked fine until 2Pac got a folder literally called 2. The stripping only happens in the path: the tags still say “The Beatles”, but the folder sits under B/. Most guides use $albumartist_sort for this, but it’s only filled in on confident MusicBrainz matches, and anything without it lands in a folder with no name.
beets tries the query rules top to bottom and falls through to default. Without the soundtrack and compilation rules, Guardians of the Galaxy gets filed under whichever contributing artist beets picked first. Those two rules also put the artist in the filename, because on a record with twenty artists, 01 Hooked on a Feeling.mp3 only tells half the story. Loose tracks that aren’t part of an album go to Non-Album/, and box sets get a disc prefix (1-01, 2-01) so the discs don’t interleave.
One more setting keeps the paths portable:
1asciify_paths: yesFolder names come out as plain ASCII, so the library copies cleanly to any drive, while the tags keep their accents. That split caught me once: Mala Rodríguez lives in M/Mala Rodriguez/ on disk, but beet list artist:"Mala Rodriguez" typed without the accent matches nothing, because the query runs against the tags, not the folder name.
Cleanup that comes along for free #
The MusicBrainz match already replaces years of hand-typed, inconsistently cased tags with one canonical set. Three plugins handle the rest of my long-standing annoyances:
1plugins: fetchart embedart ftintitleftintitle moves (feat. Someone) out of the song title into its own field, fetchart pulls cover art per release, and embedart writes that art into the file itself, so there’s no sidecar folder.jpg to lose on the next copy.
Nothing moves without a preview #
Consolidation moves files I care about, and an AI was the one typing the commands. So the safety defaults live in config.yaml, not in flags someone has to remember:
1import:
2 quiet: yes # never prompts, so unattended runs can't hang
3 quiet_fallback: skip # no strong match: skip it, never guess
4 duplicate_action: skip # the default is "ask", which would hang the runquiet_fallback: skip is the line that matters most. An album beets isn’t sure about gets left alone instead of tagged with a best guess, and waits for me to deal with it by hand.
On top of that, every step gets previewed before it runs:
1beet import --pretend /path/to/messy/library # preview matches, touches nothing
2beet import /path/to/messy/library # import and tag
3beet duplicates -a -c # check for duplicate albums before moving anything
4beet move -p # preview every file the paths: scheme would relocate
5beet move # execute the real reorganizationimport --pretend matters most for an album collection, because a wrong release match mistags a whole record at once. beet move -p is where a broken template gets caught: drop $track from a path by accident and every album sorts alphabetically instead of in order. The template runs literally and has no idea what anyone meant, human or AI. For duplicates, I set beets to compare artist, album and year, since albums imported as-is have no MusicBrainz IDs to compare.
There’s nothing inherently safe about beet move, or about letting an AI run it. I trust the setup because the config refuses to guess, every step shows the full plan before anything changes, and the call on whether a match is actually right stays with me.
What I got back #
It wasn’t one clean run. It took a few days of going back and forth: importing, checking what got skipped, fixing a template, running it again. But in the end almost the whole collection was organized.
| What | Count |
|---|---|
| Tracks | 15,002 |
| Albums | 1,157 |
| Artists | 457 |
| Size | 95.2 GiB |
| Play time | ~1,049 hours |
What I got back works two ways. The first is a library I can walk through with any file explorer, no music app needed: open a letter, open an artist, and the albums are there, named properly with the tracks in order. The second is a library any music player can read, because every track now carries the right tags and album art. I used to have folders that made sense to nobody and tags that were half missing. Now I get the best of both worlds.