Skip to content

feat(v3 library): batch→popup handoff + English-base romaji (metadata-curation capstone)#782

Merged
byrongamatos merged 2 commits into
mainfrom
feat/v3-batch-popup-handoff
Jul 5, 2026
Merged

feat(v3 library): batch→popup handoff + English-base romaji (metadata-curation capstone)#782
byrongamatos merged 2 commits into
mainfrom
feat/v3-batch-popup-handoff

Conversation

@ChrisBeWithYou

@ChrisBeWithYou ChrisBeWithYou commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

The capstone of the metadata-curation epic — it composes and sits on top of #777#778#779#780 (the Fix-metadata popup stack) + #781 (no-match badge/filter), then adds the two pieces that make the whole loop click. Best merged after those land; it then rebases to just the last two commits below.

New in this PR

1. Batch → popup handoff (1823e84)
The persistent "No match" badge is now clickable → opens the Fix-metadata popup for that song in one tap (data-meta-fix + wireCardswindow.__fbFixMatch, stopPropagation so it doesn't also play). The loop becomes: Unmatched filter → see the pile → click a miss → fix it → next.

2. English-base romaji (c56997a)
Native script (kanji/kana) is treated as identity only, never player-facing:

  • Filename romaji fallback — a blank-artist CDLC pack (Artist_Title_v1_p) has no readable name (artist blank; title = the raw filename), and a match would fill it with native script. query_page + pack_fields now surface the author's own romaji parsed from the filename (Junko Yagami — BAY CITY). Display-only, keyset-safe (raw title stashed for the cursor); a real pack artist or a user override still wins.
  • Smart adopt — "Use these values" now keeps the readable romaji name + title and takes only album / year / genre / cover-art from the match, so Identify-by-audio on a Japanese song gives "Junko Yagami — BAY CITY — FULL MOON" with the right cover, never 八神純子 / 黄昏のBAY CITY.

Verified live

The whole ~640-song city-pop pile now reads in clean romaji (was blank artist + filename-junk titles); the Unmatched badge opens the popup; smart-adopt keeps romaji + fills album/art. Tests green (romaji fallback fires for blank-artist packs and is left alone for real ones; handoff + unmatched suites pass).

Scoping note: the romaji fallback covers the grid + popup; the artist tree/albums still bucket blank-artist packs under "Unknown Artist" (follow-up). Genuinely-blank-filename gap → MB romaji alias (aliases already fetched in #772) is the deeper fallback, not wired here.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added per-song metadata overrides for title, artist, album, year, and genre, with the option to lock fields so they stay unchanged.
    • “No match” badges now open the metadata fix flow for quicker editing.
    • Songs with missing artist text now show a romaji-based display fallback.
  • Bug Fixes

    • Library browsing and paging now stay consistent when overridden titles are shown.
    • Genre filtering now reflects the saved overridden genre.
    • Locked fields are preserved during enrichment and gap-fill updates.

@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: f1113346-14a7-4725-89b7-b950840ef1a7

📥 Commits

Reviewing files that changed from the base of the PR and between c56997a and d025002.

📒 Files selected for processing (4)
  • server.py
  • static/v3/match-review.js
  • static/v3/songs.js
  • tests/test_field_overrides.py

📝 Walkthrough

Walkthrough

Adds per-song catalog field overrides with optional locks, backed by a new SQLite table and GET/PUT APIs. Query/list responses overlay overrides on pack values with romaji fallback; genre filtering respects overrides. Enrichment and gap-fill honor locks. Frontend updates fix-metadata value adoption and adds a clickable unmatched badge.

Changes

Backend overrides, storage, and enrichment/gap-fill locking

Layer / File(s) Summary
Override storage schema and CRUD methods
server.py
New song_field_override table/index, MetadataDB get/set/lock methods, purge cleanup on song deletion, and demo-mode write blocking for PUT /api/song/.+/overrides.
Override-aware display, genre, and paging
server.py
Query/list responses overlay overrides on pack values, romaji fallback for blank artist, override-aware genre facet/filter (effective-genre SQL), unmatched-set computation, keyset cursor _sort_title stash/restore, and internal-field stripping from responses.
Overrides HTTP API
server.py
Adds GET/PUT /api/song/{filename}/overrides endpoints with field validation, returning merged overrides plus pack baseline values.
Locked-field enforcement in enrichment and gap-fill
server.py
Lock-aware candidate field filtering applied during automatic enrichment matching; gap-fill preview/confirm excludes locked fields, including genre.
Backend override, filter, and enrichment tests
tests/test_field_overrides.py
Comprehensive test suite covering store semantics, lifecycle, API behavior, lock enforcement, grid/genre/romaji overlays, and paging correctness.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Fix-metadata editor and unmatched badge UI

Layer / File(s) Summary
Candidate value adoption in Fix-metadata popup
static/v3/match-review.js
useTheseValues now preserves current song artist/title (with candidate fallback) while sourcing album/year from the candidate, keeping existing genre selection.
Unmatched badge click-to-fix wiring
static/v3/songs.js
Resting no-match badge becomes clickable with data-meta-fix; wireCards adds a click handler invoking window.__fbFixMatch for that song instead of triggering playback.

Estimated code review effort: 2 (Simple) | ~15 minutes

Possibly related PRs

  • got-feedBack/feedBack#764: Both PRs modify _enrich_one's matching flow—this PR adds locked-field filtering while #764 changes artist/title seeding for blank manifests, touching the same enrichment code path.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately highlights the main v3 library changes: batch-to-popup handoff and English-base romaji fallback.
✨ 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-batch-popup-handoff

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
static/v3/songs.js (1)

3845-3856: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

toggleUnmatchedFilter doesn't refresh the Filters count badge.

render() calls updateFilterBadge() after building the toolbar (Line 3640), implying filter-count display is meant to track state.filters. This new toggle mutates state.filters.match directly but skips that call, so the "Filters" button's count chip (v3-songs-filter-count) goes stale after using this shortcut until the next full render.

♻️ Suggested fix
     function toggleUnmatchedFilter() {
         const m = state.filters.match || (state.filters.match = []);
         const i = m.indexOf('unmatched');
         const on = i < 0;
         if (on) m.push('unmatched'); else m.splice(i, 1);
         const btn = document.getElementById('v3-songs-unmatched');
         if (btn) { btn.classList.toggle('bg-fb-primary', on); btn.classList.toggle('text-white', on); }
+        updateFilterBadge();
         reload();
     }
🤖 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 `@static/v3/songs.js` around lines 3845 - 3856, The toggleUnmatchedFilter
shortcut updates state.filters.match but does not refresh the Filters count
badge, so the v3-songs-filter-count chip can become stale. After mutating the
match filter and before or after calling reload(), invoke the same badge refresh
path used by render(), specifically updateFilterBadge(), so the toolbar count
stays in sync with state.filters whenever this shortcut is used.
🤖 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.

Inline comments:
In `@static/v3/songs.js`:
- Around line 501-533: The click handler for the [data-meta-fix] badge is
getting lost when enrichBadge is re-rendered by _patchCardEnrich() via
outerHTML, so the live-updated badge no longer opens Fix metadata. Update the
click wiring in wireCards() to use delegated handling from a stable parent
element, or ensure the handler is reattached after _patchCardEnrich() replaces
the node, so clicks on the nomatch badge continue to trigger the metadata fix
flow instead of the card play handler.

---

Nitpick comments:
In `@static/v3/songs.js`:
- Around line 3845-3856: The toggleUnmatchedFilter shortcut updates
state.filters.match but does not refresh the Filters count badge, so the
v3-songs-filter-count chip can become stale. After mutating the match filter and
before or after calling reload(), invoke the same badge refresh path used by
render(), specifically updateFilterBadge(), so the toolbar count stays in sync
with state.filters whenever this shortcut is used.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6c0f0575-2840-46f5-ba07-e1f2af2a4c14

📥 Commits

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

📒 Files selected for processing (8)
  • 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_library_filters.py
  • tests/test_mb_enrichment.py

Comment thread static/v3/songs.js
Comment on lines +501 to +533
// Cards whose enrichment landed 'failed' (from the grid payload) — tracked so
// the PERSISTENT "no match" badge survives a batch tile clearing (a
// _patchCardEnrich with no flag falls back to this instead of wiping it).
// Populated as cards render (enrichBadge is called per card with the flag).
const _unmatched = new Set();
function enrichBadge(fn, unmatched) {
if (unmatched !== undefined) { if (unmatched) _unmatched.add(fn); else _unmatched.delete(fn); }
// A live batch tile wins over the resting no-match marker (they never
// coexist — the batch clears its tiles when it finishes).
const st = _metaTile[fn] || (_unmatched.has(fn) ? 'nomatch' : null);
if (!st) return '';
const M = {
queued: ['bg-black/60 text-fb-textDim', '• Queued'],
working: ['bg-fb-primary text-white', '⟳ Matching…'],
done: ['bg-fb-good/90 text-black', '✓ Updated'],
nochange: ['bg-black/60 text-fb-textDim', '— No match'],
queued: ['bg-black/60 text-fb-textDim', '• Queued', ''],
working: ['bg-fb-primary text-white', '⟳ Matching…', ''],
done: ['bg-fb-good/90 text-black', '✓ Updated', ''],
nochange: ['bg-black/60 text-fb-textDim', '— No match', ''],
// Resting indicator: subtle, so a mostly-unmatched library isn't a
// wall of loud badges. Clickable — a one-click handoff into the
// Fix-metadata popup for this song (see the [data-meta-fix] wiring).
nomatch: ['bg-black/60 text-fb-textDim', 'No match', 'Click to fix the metadata by hand'],
};
const conf = M[st] || M.queued;
const fixable = st === 'nomatch'; // resting badge → opens Fix-metadata
// top-10 clears the tuning chip (top-2) in both normal and select mode;
// z-20 sits it above the art. Non-interactive so it never eats a click.
return '<span class="v3-meta-tile absolute top-10 left-2 z-20 ' + conf[0] +
' text-[0.5625rem] font-bold px-1.5 py-0.5 rounded-sm leading-tight pointer-events-none">' +
conf[1] + '</span>';
// z-20 sits it above the art. Batch states are non-interactive; the
// resting "no match" badge is the handoff into the popup.
const cls = 'v3-meta-tile absolute top-10 left-2 z-20 ' + conf[0] +
' text-[0.5625rem] font-bold px-1.5 py-0.5 rounded-sm leading-tight ' +
(fixable ? 'pointer-events-auto cursor-pointer hover:bg-fb-primary hover:text-white transition-colors' : 'pointer-events-none');
return '<span class="' + cls + '"' +
(fixable ? ' data-meta-fix="1"' : '') +
(conf[2] ? ' title="' + conf[2] + '"' : '') +
'>' + conf[1] + '</span>';

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant structure first
ast-grep outline static/v3/songs.js --view expanded

# Read the badge wiring and patching regions with line numbers
sed -n '460,560p' static/v3/songs.js | cat -n
sed -n '3850,3895p' static/v3/songs.js | cat -n

# Find every caller of _patchCardEnrich and every data-meta-fix wiring
rg -n "_patchCardEnrich\\(|data-meta-fix|wireCards\\(" static/v3/songs.js

Repository: got-feedBack/feedBack

Length of output: 10641


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1468,1492p' static/v3/songs.js | cat -n
sed -n '3868,3880p' static/v3/songs.js | cat -n
rg -n "meta-fix|addEventListener\\('click'|closest\\('\\[data-meta-fix\\]'" static/v3/songs.js

Repository: got-feedBack/feedBack

Length of output: 12994


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1447,1565p' static/v3/songs.js | cat -n

Repository: got-feedBack/feedBack

Length of output: 8117


Badge clicks need delegated handling after live enrichment updates. _patchCardEnrich() replaces the badge node with outerHTML, so the [data-meta-fix] listener attached in wireCards() is lost on the live-update path. The recreated badge still looks clickable, but clicking it now falls through to the card play handler instead of opening Fix metadata. Delegate this click from a stable parent or rebind after patching.

🤖 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 `@static/v3/songs.js` around lines 501 - 533, The click handler for the
[data-meta-fix] badge is getting lost when enrichBadge is re-rendered by
_patchCardEnrich() via outerHTML, so the live-updated badge no longer opens Fix
metadata. Update the click wiring in wireCards() to use delegated handling from
a stable parent element, or ensure the handler is reattached after
_patchCardEnrich() replaces the node, so clicks on the nomatch badge continue to
trigger the metadata fix flow instead of the card play handler.

@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.

…handoff

Connects the two halves: the "No match" badge (the unmatched pile) now opens the
Fix-metadata popup for that song in one click, instead of right-click → menu.
The resting badge becomes interactive (pointer-events-auto + hover), carrying a
data-meta-fix hook; wireCards opens window.__fbFixMatch(playTarget) on click and
stops propagation so it doesn't also play the card. Batch tile states stay
non-interactive. Loop becomes: Unmatched filter → see the pile → click one →
fix it. tailwind.min.css regenerated for the badge's hover classes.
…English base)

Two changes so an English-speaking base never sees a blank name or native script:

- Filename romaji fallback: a blank-artist CDLC pack ("Artist_Title_v1_p") shows
  nothing useful (artist blank; title = the raw filename), and a match fills it
  with kanji/kana. query_page + pack_fields now surface the author's own romaji
  parsed from the filename ("Junko Yagami — BAY CITY") when the pack has no
  artist of its own — display-only, keyset-safe (raw title stashed for the
  cursor), a real pack artist or a user override still wins.
- Smart adopt: "Use these values" now KEEPS the readable romaji name + title the
  card already shows and takes only album/year/genre (+ art via the pin) from the
  match, so identifying a Japanese song gives "Junko Yagami — BAY CITY — FULL MOON"
  with the right cover, never native script.

Tests: romaji fallback fires for a blank-artist CDLC pack (grid + pack_fields
agree) and is left alone when the pack has a real artist.
@byrongamatos
byrongamatos force-pushed the feat/v3-batch-popup-handoff branch from c56997a to d025002 Compare July 5, 2026 21:35
@byrongamatos
byrongamatos merged commit 6aaa2dc into main Jul 5, 2026
3 checks passed
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