Skip to content

feat(v3 library): "Fix metadata" popup — per-song override + lock, cover picker, MusicBrainz + AcoustID - #777

Merged
byrongamatos merged 5 commits into
mainfrom
feat/v3-fix-metadata-popup
Jul 5, 2026
Merged

feat(v3 library): "Fix metadata" popup — per-song override + lock, cover picker, MusicBrainz + AcoustID#777
byrongamatos merged 5 commits into
mainfrom
feat/v3-fix-metadata-popup

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

A Plex-style "Fix metadata" editor for the v3 library, reached from a card's right-click / ⋮ → Fix metadata…. It's the manual curation tool for packs whose metadata is wrong or blank (a large slice of community/CDLC charts) — the ones automatic matching can't crack.

Everything here is a reversible display layer — pack/.sloppak files are never modified.

Details tab — type + lock the shown fields

  • Edit title / artist / album / year; each cell shows Yours/Pack provenance, a lock (pins the field so an auto-match can't re-canonicalize it), and revert-to-pack.
  • Backed by a new per-field override + lock store (song_field_override): filename-keyed so it survives a rescan, dropped only with the song. GET/PUT /api/song/{fn}/overrides (GET also returns the pack baseline for provenance/revert).
  • The lock is enforced server-side: the matcher composes a per-song lock filter (_compose_lock_filter) so an auto-match still fixes identity/art but won't overwrite a locked display field, and gap-fill skips locked fields. An explicit user pick always wins.

Grid shows your overrides

  • query_page overlays the override (over the pack value, and over the P4 artist-alias re-label) for the visible window — matched MusicBrainz canon never silently re-titles a card (canon stays in the drawer + art). Keyset-safe: paging still seeks on the raw column (the one overridable keyset column, title, stashes its raw value for the cursor; stripped from the payload).

Cover art tab

  • Hands off to the shared cover picker (Cover Art Archive / paste a link / upload); the pick refreshes the art everywhere.

Match tab — MusicBrainz + Identify by audio

  • The existing MB search/candidate/pick flow, refactored into shared body/footer helpers (the queue-review flow is untouched).
  • Identify by audio (AcoustID, builds on feat(enrichment): AcoustID audio-fingerprint identification (opt-in) #759): fingerprints the pack's own master audio when text search can't tell the studio take from live/comp versions. Outcomes are explicit — ✓ fingerprinted-with-matches / ✓ fingerprinted-but-no-match / no-audio / off — so an empty result reads as "ran, found nothing" (with a pointer to the manual tabs), never as a broken button.

Notes

Testing

  • Override store + display overlay + lock enforcement: tests/test_field_overrides.py (+ test_mb_enrichment / test_gap_fill for lock/gap-fill). Full library/enrichment suites green; both JS modules node --check.
  • Verified live on the testbed against the real library: typed overrides repaint the grid; AcoustID identify returns real hits on packs AcoustID knows (e.g. Anri – Windy Summer → 杏里 / WINDY SUMMER / TIMELY!! 1983) and an honest "no match" on obscure tracks.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added per-song metadata overrides with per-field locking (title/artist/album/year) plus GET/PUT API endpoints and automatic cleanup on delete.
    • Updated single-song “Fix match” into a tabbed “Fix metadata” editor (Details/Cover/Match) that loads and saves overrides.
  • Bug Fixes
    • Kept library keyset pagination stable when display values are overridden.
    • Prevented locked fields from being overwritten by auto-matching or gap-fill, including excluding them from gap-fill previews.
  • Tests
    • Added/expanded tests covering override merge/clear semantics, paging stability, and locked-field matching/gap-fill behavior.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 138bdf38-6b33-4508-8814-5adc83907fa9

📥 Commits

Reviewing files that changed from the base of the PR and between d96627e and fcb3b41.

📒 Files selected for processing (7)
  • server.py
  • static/tailwind.min.css
  • static/v3/match-review.js
  • static/v3/songs.js
  • tests/test_field_overrides.py
  • tests/test_gap_fill.py
  • tests/test_mb_enrichment.py

📝 Walkthrough

Walkthrough

Adds per-song metadata overrides and field locking, wires the new state into library display, matching, and gap-fill flows, and updates the single-song review UI to edit and save those overrides.

Changes

Field overrides and locking feature

Layer / File(s) Summary
Override storage schema and CRUD surface
server.py
Adds song_field_override storage and MetadataDB methods for reading, writing, batching, and clearing per-field overrides and locks, plus deletion cleanup.
Overrides API endpoints and demo-mode guard
server.py
Adds GET/PUT /api/song/{filename}/overrides with allowlisting, normalization, and validation, and blocks the PUT route in demo mode.
Library grid override overlay and pagination fix
server.py
Overlays override values onto visible song fields, keeps title keyset paging stable with _sort_title, updates cursor generation, and strips _sort_title from API responses.
Enrichment matcher and gap-fill lock enforcement
server.py
Uses locked fields to filter automatic match candidates and to suppress locked fields from gap-fill proposal output.
Tabbed Fix-metadata editor UI
static/v3/match-review.js, static/v3/songs.js
Reworks the single-song modal into tabbed Details/Cover art/Match views, adds override editing and save handling, updates identify-by-audio messaging, and renames the menu action label.
Tests for overrides, gap-fill, and enrichment locking
tests/test_field_overrides.py, tests/test_gap_fill.py, tests/test_mb_enrichment.py
Adds coverage for override CRUD, API validation, lock filtering, library overlay and paging, gap-fill exclusion, and matcher preservation of locked fields.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • got-feedBack/feedBack#764: Both PRs modify the server-side background enrichment/matching pipeline in server.py, including _enrich_one-adjacent logic.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main feature: the new Fix metadata popup with per-song overrides, locking, cover art, and matching integrations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/v3-fix-metadata-popup

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/test_field_overrides.py (1)

15-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Factor the shared server fixture into tests/conftest.py. The reload/env/teardown pattern is repeated across multiple test modules; centralizing it would reduce drift if the cleanup logic changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_field_overrides.py` around lines 15 - 33, The shared server
setup/teardown logic in the `server` fixture is duplicated across test modules
and should be centralized. Move the reload/env/cleanup fixture into
`tests/conftest.py` so all tests reuse the same `server` setup, including the
`importlib.import_module("server")` reload,
`CONFIG_DIR`/`FEEDBACK_SKIP_STARTUP_TASKS` monkeypatching, and the `meta_db`
connection cleanup with `_join_background_db_threads`. Keep the `client` fixture
local if needed, but have it depend on the shared `server` fixture.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/test_field_overrides.py`:
- Around line 15-33: The shared server setup/teardown logic in the `server`
fixture is duplicated across test modules and should be centralized. Move the
reload/env/cleanup fixture into `tests/conftest.py` so all tests reuse the same
`server` setup, including the `importlib.import_module("server")` reload,
`CONFIG_DIR`/`FEEDBACK_SKIP_STARTUP_TASKS` monkeypatching, and the `meta_db`
connection cleanup with `_join_background_db_threads`. Keep the `client` fixture
local if needed, but have it depend on the shared `server` fixture.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 87c8a3a3-99e7-49b9-912f-03474f7924cb

📥 Commits

Reviewing files that changed from the base of the PR and between 74cff4e and d96627e.

📒 Files selected for processing (7)
  • server.py
  • static/tailwind.min.css
  • static/v3/match-review.js
  • static/v3/songs.js
  • tests/test_field_overrides.py
  • tests/test_gap_fill.py
  • tests/test_mb_enrichment.py

@ChrisBeWithYou

Copy link
Copy Markdown
Contributor Author

📋 Merge order — metadata-curation epic (7 PRs)

  1. feat(v3 library): "Fix metadata" popup — per-song override + lock, cover picker, MusicBrainz + AcoustID #777 — Fix-metadata popup
  2. feat(v3 library): "Write to file" in the Fix-metadata popup #778 — Write to file
  3. feat(v3 library): Match→Details "use these values" bridge #779 — Match→Details bridge
  4. feat(v3 library): genre field in the Fix-metadata popup #780 — Genre in Details
  5. feat(v3 library): persistent "no match" badge + Unmatched quick filter #781 — No-match badge + Unmatched filter (independent of 1–4 — fine to merge any time)
  6. feat(v3 library): batch→popup handoff + English-base romaji (metadata-curation capstone) #782 — Batch→popup handoff + romaji (capstone)
  7. feat(v3 library): searchable Cover Art Archive picker in Change-cover #783 — Searchable cover picker

#778#780 are stacked (each based on the branch above), so GitHub re-points each to main automatically as the previous one merges — just merge #777#778#779#780 top-down. #781 is independent (base: main).

⚠️ Hold #782 and #783 until #777#781 are all in main. #782's branch is a combined branch that already contains #777#781, so merging it early would double-merge the stack. Once #777#781 land, the author will rebase #782 (then #783) onto main — they shrink to just their own commits — and they're then ready to merge.

ChrisBeWithYou and others added 5 commits July 5, 2026 21:05
… (popup slices 1–2)

Backend foundation for the Fix-metadata popup. Not yet surfaced in the UI (the
display + 3-tab popup are the next slices); no PR until it's user-visible.

Slice 1 — the store:
- `song_field_override(filename, field, value, locked)` table: a reversible
  DISPLAY overlay (never written to the pack), filename-keyed so it survives a
  rescan (never purged by delete_missing) and is dropped only with the song.
- DB methods (partial upsert that drops empty+unlocked rows; batch map) +
  `GET`/`PUT /api/song/{fn}/overrides` (field allowlist title/artist/album/
  year/genre; clearing rides PUT since DELETE /api/song/{path} shadows sub-
  routes; PUT demo-blocked).

Slice 2 — locks respected by enrichment:
- The auto-matcher composes a per-song `_compose_lock_filter` onto the global
  apply-filter, so a match still applies IDENTITY (mbid/release → art) but never
  re-canonicalizes a LOCKED display field.
- Gap-fill (write-to-file) skips locked album/year/genre — writing the matched
  value would be exactly the clobber the lock exists to prevent.
- Review/manual picks bypass the filter (an explicit confirm overrides a lock).

Tests: store semantics + rescan-survival + API; the lock filter + reader; an
auto-match leaving a locked field un-canonicalized; gap-fill excluding locked
keys.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The grid now displays the user's per-song title/artist/album/year override
in place of the pack value ("grid shows only overrides") — a matched
MusicBrainz canon never silently re-titles a card; canon stays in the
Details drawer + art. Overlaid in Python over the visible window, keyset-safe
like the P4 artist-alias re-label: the seek still runs on the raw column, and
the one overridable keyset column (title) stashes its raw value for the cursor
so paging never skips/dupes. The private stash is dropped from the payload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… (slice 4)

Turns the thin single-song fix-match modal into the Plex-style metadata
editor reached from a card's "Fix metadata…" menu:

- Details tab: type + lock the displayed title/artist/album/year. Values
  ride the reversible override store (GET/PUT /api/song/{fn}/overrides); each
  field sits on its pack value (Yours/Pack provenance + revert-to-pack), a lock
  pins it against auto-match, and Save repaints the grid via library:changed
  (slice-3 overlay). This is the real tool for the blank-artist city-pop pile
  MusicBrainz can't surface — you just type the right title.
- Cover art tab: hands off to the shared image picker (its own modal); the
  pick refreshes the thumbnail everywhere.
- Match tab: the existing MusicBrainz search + candidate/pick flow, refactored
  into shared body/footer helpers (the queue-review flow is untouched).

Backend: GET /overrides now also returns the pack baseline so the Details tab
can pre-fill + show provenance. tailwind.min.css regenerated (build-tailwind.sh)
for the popup's new utility classes.

Identify-by-audio (AcoustID) is deferred: it lives in unmerged PR #759, off
main — the Match tab gains the button once #759 lands and this branch rebases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The AcoustID Identify button (#759) merged in referencing an out-of-scope
`panel` in the wiring — a leftover from the pre-popup fix-match modal that my
tab refactor renamed to `root`. Under strict mode that threw, so the handler
never attached and the button did nothing. Scope it to `root` (the tab body),
which is where the search-results area it renders into lives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An empty AcoustID result read the same as a broken button. Each state now says
plainly which outcome it is — ✓ fingerprinted-but-no-match vs no-audio vs off vs
unavailable — and, in the popup, points at the manual fallback (Search, or set
the album in Details + cover in Cover art by hand). A ✓ marks the states that
actually ran, so "worked, found nothing" no longer looks like a failure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants