Consolidates in-progress work across autotag, explore, and library: - autotag: beets/Picard-informed scoring engine — ID-first matching, VA handling, recommendation tiers, and a merged distance/rank cascade, with an eval harness for regression tracking. - explore: offline MusicBrainz dump import/incremental refresh replaces the legacy tier crawl; index-first local search with fuzzy matching and a dedicated ranker; disk-free guards for dump downloads. - library: artist-credit extraction and matching. - lyrics: owned-library lyric search (FTS) with LRCLIB backfill. Also: rewrite README to be user-focused, and migrate upstream to git.ljones.me/yonlu/yellowjacket. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
14 KiB
Autotag (v1.3) — MusicBrainz Autotagger
The MusicBrainz autotagger, collectively v1.3. Builds on the explore-browser API client + cache foundation. Five sequential phases (008–012), each depending on the prior one.
| Phase | Title | Status |
|---|---|---|
| 008 | Schema & Grouping Foundation | shipped 2026-04-20 |
| 009 | Scoring Engine & MB Orchestration | shipped 2026-04-21 |
| 010 | Review UI & Apply Pipeline | active |
| 011 | Auto-Accept & Entry Points | pending |
| 012 | Settings & Polish | pending |
008 — Schema & Grouping Foundation · shipped 2026-04-20
First v1.3 phase. Lays down the schema + bookkeeping — no scoring, no UI, no MB calls yet.
What landed
- 008.1 — Migration 31:
audio_files.tag_status. Text column with inlineCHECK(tag_status IN (...))constraint (SQLiteALTER TABLE ADD COLUMNsupports column constraints, so both fresh and upgraded DBs enforce it). Partial indexidx_audio_files_tag_status_untagged ON audio_files(library_id) WHERE tag_status = 'untagged'powers the pending badge. Backfill setsuser_confirmedwhere the joined recording already has an MBID; everything else defaults tountagged. - 008.2 — Migration 32:
tagging_items+group_key. Newtagging_itemstable (PKgroup_key,status CHECK IN ('pending','matched','confirmed','skipped'), two indexes including the partialWHERE status = 'pending'for the badge).audio_files.group_key TEXT NOT NULL DEFAULT ''with partial indexWHERE group_key != ''. Column-referencing indexes live in the migration, not the schema file, becauseCREATE TABLE IF NOT EXISTSis a no-op on existing tables and the partial-index predicate would reference a column that hasn't been added yet. - 008.3 —
backend/autotag.GroupKey+ scan-path integration. Lower-case hex SHA-1 overlibraryID || 0 || parentDirLower || 0 || albumTrimmed || 0 || discNumber— intentionally shallow, deeper normalization is scoring territory (009).saveAudioFileswitched toCreateAudioFileWithGroupKey+UpsertTaggingItemOnTrackAdd.updateAudioFileMetadatarebinds on key change: decrement old group → delete if empty → upsert new group → write new key onto the audio_files row. Migration 32 streams existing rows in batches of 500 and aggregatestagging_itemsat the end, defaulting status toconfirmedwhen every track in the group isuser_confirmed. - 008.4 — Pending-queue sqlc queries.
CountPendingTaggingItems, threeListPendingTaggingItems...variants (alphabetical, by-score nulls-last, by-recent) — sqlc has no dynamic ORDER BY so each sort has its own query.CAST(@param AS INTEGER|TEXT)hints give typed params (otherwise sqlc emitsinterface{}).GetTaggingItemandListAudioFilesInTaggingGroupround out the queue API.EXPLAIN QUERY PLANtest asserts the badge query usesidx_tagging_items_status_pendingso a future schema change that breaks the partial index fails loudly.
Key decisions retained
- Hash algorithm in Go, not SQL.
autotag.GroupKeyis the single source of truth for the key format so we can evolve it without coupling to SQLite functions. - SHA-1 over alternatives. Matches the codebase's existing non-crypto deterministic-key convention. Collision risk at album-group cardinality (millions) is irrelevant.
- Null-byte separators between hash inputs. Prevents
a|bvsab|ambiguity. - Per-track integration inside
commitBatch's transaction, not a post-scan callback — keepstagging_itemscoherent after partial scans. best_match_release_mbid,score,last_checked_atshapes fixed now even though they stay NULL until 009-010. Avoids schema churn later.
009 — Scoring Engine & MB Orchestration · shipped 2026-04-21
Second v1.3 phase. Given an album-group, produce a ranked list of candidate releases with per-track alignment data, using as few MusicBrainz calls as the API-minimization playbook allows. No UI yet — 010 surfaces this to the user.
What landed
backend/autotag/domain types —Candidate,TrackAlignment,GroupScore,LocalTrack,CandidateSource(local/musicbrainz),AlignmentStatus(matched/missing/extra/mismatched).Normalize(s)— Unicode NFC → qualifier-suffix strip ((Remastered 2009),[Bonus Track],(feat. X), etc.) → case fold → punctuation drop → whitespace collapse. Comparison-only, not human-readable.- Per-track distance — weighted 60% title similarity (1.0 − Levenshtein/max-len), 30% length delta (linear: ≤1 s = 1.0, ≥30 s = 0.0, neutral 0.5 when either side unknown), 10% track-number match.
- Greedy alignment —
AlignTrackspicks the highest-scoring (local, cand) pair iteratively; not Hungarian-optimal but fine at album cardinalities. Emitsmatched/missing/extra/mismatchedrows so the review UI can render the diff. - Local-first resolver —
ListLocalReleaseGroupCandidatessqlc query pre-filters onrg.mbid != '' AND r.mbid != '' AND rg.name = ? COLLATE NOCASE, then Go applies full normalization. Candidate track lists only include recordings that themselves have MBIDs (avoids untagged dupes polluting the "canonical" view when the same(name, artist)release group is shared across libraries). - MB orchestration —
MBClientinterface (SearchReleaseGroups,BrowseReleases,LookupArtist) hidesexplore.MusicBrainzClient;backend/explore/autotagclient.goadapts one to the other.buildMBQueryassemblesrelease:"X" AND arid:<mbid> AND tracks:N—arid:makes the search cache key deterministic,tracks:Nfilters out box-set-style releases. One search per album + oneBrowseReleasesper candidate RG. - Release-level ranker — aggregate track score (70%) + track-count match (15%, zero at ≥50% delta) + meta (15%, avg of year/Official/country bonuses).
Scorer.ScoreGrouphits local first, runs MB only when no local candidate scores ≥ 0.90. - Persistence —
SetTaggingItemBestMatchwritesbest_match_release_mbid,score,last_checked_at = CURRENT_TIMESTAMP,status = 'matched'.
Key decisions retained
- SHA-1-grade normalization vs. full MB-equivalent. Qualifier regex handles the common cases (remaster, deluxe, explicit, feat., bonus, etc.) without dragging in a full MB title-parsing library. Edges that bite in real libraries will show up in 010 review UX and can be patched then.
*sqlcgen.Queriesas the DB boundary for autotag, not*database.DB. Thedatabasepackage already depends onautotag.GroupKey(from 008.3), so reversing the dependency viadatabasewould create a cycle. Usingsqlcgendirectly is acyclic and keepsautotagswappable.- Scorer constructor takes
MBClientas interface, not*explore.MusicBrainzClient. Lets tests inject a stub without spinning up the HTTP + cache layer. The concrete adapter lives inexplore/autotagclient.go. localSufficient = 0.90threshold for skipping MB. Empirical guess — will get retuned in 011 auto-accept phase when we observe real corpus scores.- Weights
60/30/10for title/length/track-number. Cribbed from beets' broad intuition; tuned to emphasize title-matching since length data can be unreliable from Vorbis Comments. Tests document the expected floors (e.g. "exact match should score ≥ 0.99") so nudging weights won't silently regress. release_groupsandrecordingsmust both carry MBIDs for a local candidate. Otherwise untagged dupes of the same album (across libraries) falsely expand the "canonical" track list.- UTF-8 em-dashes in SQL comments broke sqlc's string-literal emitter, truncating generated query strings mid-word. All autotag SQL comments use ASCII punctuation.
Known follow-ups into 010+
guessArtistMBIDis a stub returning""becauseLocalTrackdoesn't currently carry artist MBIDs. 010 should thread artist MBIDs throughListAudioFilesInTaggingGroupso the MB resolver can usearid:filters.yearBonususestime.Now().Year()as a placeholder target. Should become the earliest release-date hint from the group's tracks once 010 provides it.- VA compilation detection threshold is still open. The scorer doesn't special-case per-track artist credits differing from album-artist.
010 — Review UI & Apply Pipeline · ACTIVE
Third v1.3 phase. User reviews one album at a time, sees the diff clearly, and applies or skips — file tags get written, DB gets synced, cover art follows the never-replace-existing rule.
Requirements: REVIEW-01..07 · Depends on: 009 (needs candidates + scores)
Success criteria
/autotagshows the next pending album with its top candidate as a field-by-field diff. Missing-from-local and extra-in-local tracks are shown explicitly.- Keyboard shortcuts work without the mouse:
Aapply,Mmore candidates,Sskip,Lleave-as-is,Upaste URL,→/←navigate. - Apply writes tags to every track in the group via the existing format-specific writers + atomic write + DB sync + FTS5 sync. Whitelisted fields only: title, artist, album, album-artist, year, track#, track-total, disc#, disc-total, all MBIDs.
- Cover art rule: embed only when the file has no existing art and CAA returns ≥500 px on the shortest side. Never replace existing embedded art (auto or manual).
- First-ever apply per library shows an irreversibility warning. "Don't show again" sets a flag on the
librariesrow; never shows again for that library. - While the user reviews album N, candidates for album N+1 are prefetched into
http_cacheso advancing feels instant. - "Paste MB URL" dialog accepts a release URL, extracts the MBID, runs one
LookupRelease, renders the diff against the current album.
Sub-plans
- Wails bindings —
StartAutotagQueue,GetCurrentCandidate,GetCandidates(groupKey),Apply(groupKey, releaseMBID),Skip,LeaveAsIs(groupKey),RetagGroup(groupKey). /autotagpage layout — focused album header, diff table, candidate sidebar, missing/extra panel.- Keyboard shortcut wiring through the existing scope-aware dispatch.
- Apply pipeline integration with existing tag writers + DB sync.
- Cover art apply rule + CAA fetch + 500 px minimum check.
- File-write warning dialog with per-library persistence.
- Prefetch-next-album goroutine, rate-limiter aware.
- Paste-MB-URL escape hatch.
Risk callout
Every apply rewrites a file. The AtomicWrite pipeline mitigates corruption risk; the per-library warning mitigates surprise. Dry-run mode (from 009) lets developers validate scoring changes without file writes.
011 — Auto-Accept & Entry Points · pending
Fourth v1.3 phase. The strict all-match auto-accept path runs as a background job; the tool is reachable from every place a user expects.
Requirements: AUTO-01..06 · Depends on: 010 (needs the apply pipeline)
Success criteria
- An album-group qualifies for auto-accept iff: exact track-count match, every track's normalized title matches, every track's length within ±2s, no cover-art replacement required, no existing-MBID conflicts. Decision uses already-cached candidate data — no additional MB calls.
- Auto-accept job processes all qualifying groups in the queue, emits progress events, is cancellable at any point, honors the shared rate limiter.
- Right-click on a track / album / artist exposes "Autotag this album" (queues + jumps to review) and "Retag" (flips status to
untaggedand requeues). - After a library scan finishes with N new untagged albums, a non-blocking toast appears linking to
/autotag. - Sidebar has an "Autotag" nav entry with a pending-count badge, updates reactively.
- Pasting a MB release URL into the Paste-URL dialog renders a full diff against the current album with one
LookupReleasecall.
Sub-plans
- Strict all-match rule + unit tests.
- Auto-accept background job — progress events, cancellation, queue traversal.
- Context menu integrations on track/album/artist views.
- Post-scan toast wiring via the existing scan-complete event.
- Sidebar nav entry + pending-count badge store integration.
Risk callout
Release selection can pick the wrong edition. The exact-track-count gate prevents most silent misbehavior, but bonus-track editions and remaster reissues with matching track counts are genuine ambiguity. Manual review handles the edge cases — that's why auto-accept is strict by design, and the slider for fuzzy auto-accept is explicitly out of scope.
012 — Settings & Polish · pending
Fifth and final v1.3 phase. Configuration surfaces in the existing settings system; the known sharp edges (rate-limit contention, VA compilations, singleton files) get sanded; the fingerprinting seam is in place for the future.
Requirements: CFG-01..06 · Depends on: 011
Success criteria
- Autotag settings panel accessible via the existing templ/HTMX settings UI. Exposes: enable/disable auto-accept, per-library file-write warning reset, default review filter, default sort order.
- Shared rate limiter distinguishes interactive from background requests. User-initiated MB calls (paste-URL, opening a review, explore browsing) are never blocked behind a running auto-accept job.
- VA compilation albums (per-track artist credits differ from album-artist) are detected. The auto-accept artist-match rule relaxes for them; the ranker prefers MB releases credited to "Various Artists".
- Singleton files (
track_count = 1, no sibling context) use a recording-level match path (SearchRecordingswith title + artist + length filters). Lower confidence ceiling — never eligible for auto-accept regardless of confidence. type Identifier interface { Identify(path) ([]Candidate, error) }exists withMetadataIdentifieras the v1 implementation. No fpcalc integration, but the seam is in place for a futureAcoustIDIdentifier.- User-facing quickstart docs exist; CLAUDE.md gets a
backend/autotag/package description; scoring-function dev notes are committed.
Sub-plans
- Autotag settings panel (templ + HTMX).
- Rate-limiter priority support.
- VA compilation detection and scoring adjustments.
- Singleton-file match path.
Identifierinterface seam withMetadataIdentifier.- Docs — user quickstart + dev notes + CLAUDE.md update.
Ship criteria for v1.3 overall
- All 29 SCHEMA/MATCH/REVIEW/AUTO/CFG requirements complete.
- All five phases' success criteria verified end-to-end on a real library (10k+ tracks, mixed match quality).
- Auto-accept run against a well-tagged subset produces zero incorrect matches.
- Manual review workflow can process 100 albums in under 30 minutes without mouse use.