From 6aaa2dcf474d38263e5e50c4362594db3b672cba Mon Sep 17 00:00:00 2001 From: ChrisBeWithYou Date: Sun, 5 Jul 2026 16:35:58 -0500 Subject: [PATCH] =?UTF-8?q?feat(v3=20library):=20batch=E2=86=92popup=20han?= =?UTF-8?q?doff=20+=20English-base=20romaji=20(metadata-curation=20capston?= =?UTF-8?q?e)=20(#782)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(v3 library): click a "No match" badge to fix it — batch → popup 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. * feat(v3 library): show the author's romaji, not blank/native script (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. --- server.py | 31 +++++++++++++++++++++++++++++-- static/v3/match-review.js | 13 ++++++++++--- static/v3/songs.js | 26 ++++++++++++++++++++------ tests/test_field_overrides.py | 18 ++++++++++++++++++ 4 files changed, 77 insertions(+), 11 deletions(-) diff --git a/server.py b/server.py index 0c99ccc..0165823 100644 --- a/server.py +++ b/server.py @@ -1266,6 +1266,18 @@ class MetadataDB: out.setdefault(fn, {})[field] = {"value": value, "locked": bool(locked)} return out + def _romaji_display(self, filename: str, artist: str, title: str): + """English-base display fallback. A blank-artist CDLC pack named + 'Artist_Title_v1_p' has no readable name (artist blank; title = the raw + filename), and a match would fill it with the artist's NATIVE script + (kanji/kana). Surface the author's own romaji parsed from the filename + instead, so an English base reads 'Junko Yagami - BAY CITY'. Only kicks in + when the pack has no artist of its own — a real pack artist is untouched.""" + if (artist or "").strip(): + return artist, title + d = _artist_title_from_filename(filename) + return (d["artist"], d["title"]) if d else (artist, title) + def pack_fields(self, filename: str) -> dict: """The stored (pack) values for the overridable catalog fields — the Fix-metadata popup shows these behind each override as the 'revert to @@ -1275,7 +1287,11 @@ class MetadataDB: row = self.conn.execute( "SELECT title, artist, album, year, genre FROM songs WHERE filename = ?", (filename,)).fetchone() - return {k: ((row[i] or "") if row else "") for i, k in enumerate(keys)} + vals = {k: ((row[i] or "") if row else "") for i, k in enumerate(keys)} + # Baseline the author's romaji (from the filename) for a blank-artist pack, + # so the Details tab's Pack reference matches what the grid shows. + vals["artist"], vals["title"] = self._romaji_display(filename, vals["artist"], vals["title"]) + return vals # Effective genre = a per-song genre OVERRIDE (Fix-metadata popup) else the # scanned pack genre. Applied at FILTER/FACET time (like the P4 artist alias) @@ -4215,6 +4231,17 @@ class MetadataDB: s["unmatched"] = s["filename"] in um if amap: s["artist"] = amap.get((s.get("artist") or "").lower(), s.get("artist")) + # English-base romaji fallback: a blank-artist CDLC pack shows nothing + # useful (artist blank; title = the raw filename). Surface the author's + # romaji from the "Artist_Title_v1_p" filename so the card reads + # "Junko Yagami — BAY CITY", never blank or native script. Display-only; + # a user override (below) still wins. Keyset-safe: stash the raw title + # for the cursor before replacing it. + if not (s.get("artist") or "").strip(): + r_artist, r_title = self._romaji_display(s["filename"], s.get("artist"), s.get("title")) + if r_title != s.get("title") and "_sort_title" not in s: + s["_sort_title"] = s["title"] + s["artist"], s["title"] = r_artist, r_title # Override wins over the pack AND the alias re-label — it's the user's # explicit per-song choice. Only a non-empty override VALUE replaces a # cell; a lock-only row (value None) leaves the displayed value alone. @@ -4224,7 +4251,7 @@ class MetadataDB: cell = ov.get(field) val = cell.get("value") if cell else None if val: - if field == "title": + if field == "title" and "_sort_title" not in s: s["_sort_title"] = s["title"] # raw title, for the keyset cursor s[field] = val # Grouped rows carry the ⚑ N (chart_count) + the work_key from the diff --git a/static/v3/match-review.js b/static/v3/match-review.js index cc216d7..2337778 100644 --- a/static/v3/match-review.js +++ b/static/v3/match-review.js @@ -511,10 +511,17 @@ // then land on Details pre-filled for review. async function useTheseValues(song, cand) { if (!cand) return; + // Smart adopt for an English base: KEEP the readable name + title the card + // already shows (the author's romaji, e.g. "Junko Yagami / BAY CITY") — the + // match is often native script (kanji/kana). Take only what the pack lacks + // — album / year / genre — from the match; the pin below still brings the + // correct art + identity. The user can still edit any field. song._pendingDetails = { - title: String(cand.title || ''), artist: String(cand.artist || ''), - album: String(cand.album || ''), year: String(cand.year || ''), - genre: (Array.isArray(cand.genres) && cand.genres[0]) ? String(cand.genres[0]) : String(cand.genre || ''), + artist: String(song.artist || cand.artist || ''), + title: String(song.title || cand.title || ''), + album: String(cand.album || song.album || ''), + year: String(cand.year || song.year || ''), + genre: String((Array.isArray(cand.genres) && cand.genres[0]) || cand.genre || ''), }; try { await post('/api/enrichment/review/' + enc(song.filename) + '/pick', { candidate: cand }); diff --git a/static/v3/songs.js b/static/v3/songs.js index 32674eb..1c5490f 100644 --- a/static/v3/songs.js +++ b/static/v3/songs.js @@ -515,15 +515,22 @@ 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; points at the manual fix. - nomatch: ['bg-black/60 text-fb-textDim', 'No match', 'No metadata match found — right-click to fix it by hand'], + // 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 '' + conf[1] + ''; + // 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 '' + conf[1] + ''; } // After a song is scored, the badge for that card is stale until the next @@ -1467,6 +1474,13 @@ e.stopPropagation(); openChartsDrawer(e.currentTarget.getAttribute('data-charts'), song); }); + // "No match" badge → straight into the Fix-metadata popup for this + // song (the batch → fix handoff). stopPropagation so it doesn't also + // trigger the card's play. Follows the displayed chart, like the menu. + el.querySelector('[data-meta-fix]')?.addEventListener('click', (e) => { + e.stopPropagation(); + if (window.__fbFixMatch) window.__fbFixMatch(playTarget); + }); // Artist line → the artist page (PR-B). In select mode the grid's // capture-phase toggle intercepts first, so selection still wins. el.querySelector('[data-v3-artist]')?.addEventListener('click', (e) => { diff --git a/tests/test_field_overrides.py b/tests/test_field_overrides.py index d4a3f78..8761aab 100644 --- a/tests/test_field_overrides.py +++ b/tests/test_field_overrides.py @@ -233,6 +233,24 @@ def test_lock_only_genre_does_not_change_facet(server): assert server.meta_db._effective_genre_expr() == "genre" +def test_romaji_fallback_for_blank_artist_pack(server): + fn = "CDLC/0 - City Pop/Junko-Yagami_BAY-CITY_v1_p.feedpak" + _put(server, fn, title="Junko-Yagami_BAY-CITY_v1_p", artist="") # scanner fell back to the filename + s = {x["filename"]: x for x in server.meta_db.query_page()[0]}[fn] + # the grid shows the author's romaji, not blank / the raw filename / kanji + assert s["artist"] == "Junko Yagami" + assert s["title"] == "BAY CITY" + # the Details baseline (pack_fields) matches, so the popup agrees with the grid + pack = server.meta_db.pack_fields(fn) + assert pack["artist"] == "Junko Yagami" and pack["title"] == "BAY CITY" + + +def test_romaji_fallback_left_alone_when_pack_has_artist(server): + _put(server, "a.archive", title="Real Title", artist="Real Artist") + s = {x["filename"]: x for x in server.meta_db.query_page()[0]}["a.archive"] + assert s["artist"] == "Real Artist" and s["title"] == "Real Title" + + def test_title_keyset_paging_is_complete_with_overrides(client, server): # Raw titles A/B/C → title-sort order is A, B, C on the RAW column. _put(server, "b.archive", title="B")