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.
This commit is contained in:
ChrisBeWithYou
2026-07-05 23:35:46 +02:00
committed by byrongamatos
parent 2810307fcc
commit d025002838
4 changed files with 58 additions and 6 deletions
+29 -2
View File
@@ -1266,6 +1266,18 @@ class MetadataDB:
out.setdefault(fn, {})[field] = {"value": value, "locked": bool(locked)} out.setdefault(fn, {})[field] = {"value": value, "locked": bool(locked)}
return out 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: def pack_fields(self, filename: str) -> dict:
"""The stored (pack) values for the overridable catalog fields — the """The stored (pack) values for the overridable catalog fields — the
Fix-metadata popup shows these behind each override as the 'revert to Fix-metadata popup shows these behind each override as the 'revert to
@@ -1275,7 +1287,11 @@ class MetadataDB:
row = self.conn.execute( row = self.conn.execute(
"SELECT title, artist, album, year, genre FROM songs WHERE filename = ?", "SELECT title, artist, album, year, genre FROM songs WHERE filename = ?",
(filename,)).fetchone() (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 # 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) # 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 s["unmatched"] = s["filename"] in um
if amap: if amap:
s["artist"] = amap.get((s.get("artist") or "").lower(), s.get("artist")) 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 # 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 # explicit per-song choice. Only a non-empty override VALUE replaces a
# cell; a lock-only row (value None) leaves the displayed value alone. # cell; a lock-only row (value None) leaves the displayed value alone.
@@ -4224,7 +4251,7 @@ class MetadataDB:
cell = ov.get(field) cell = ov.get(field)
val = cell.get("value") if cell else None val = cell.get("value") if cell else None
if val: 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["_sort_title"] = s["title"] # raw title, for the keyset cursor
s[field] = val s[field] = val
# Grouped rows carry the ⚑ N (chart_count) + the work_key from the # Grouped rows carry the ⚑ N (chart_count) + the work_key from the
+1 -1
View File
File diff suppressed because one or more lines are too long
+10 -3
View File
@@ -511,10 +511,17 @@
// then land on Details pre-filled for review. // then land on Details pre-filled for review.
async function useTheseValues(song, cand) { async function useTheseValues(song, cand) {
if (!cand) return; 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 = { song._pendingDetails = {
title: String(cand.title || ''), artist: String(cand.artist || ''), artist: String(song.artist || cand.artist || ''),
album: String(cand.album || ''), year: String(cand.year || ''), title: String(song.title || cand.title || ''),
genre: (Array.isArray(cand.genres) && cand.genres[0]) ? String(cand.genres[0]) : String(cand.genre || ''), album: String(cand.album || song.album || ''),
year: String(cand.year || song.year || ''),
genre: String((Array.isArray(cand.genres) && cand.genres[0]) || cand.genre || ''),
}; };
try { try {
await post('/api/enrichment/review/' + enc(song.filename) + '/pick', { candidate: cand }); await post('/api/enrichment/review/' + enc(song.filename) + '/pick', { candidate: cand });
+18
View File
@@ -233,6 +233,24 @@ def test_lock_only_genre_does_not_change_facet(server):
assert server.meta_db._effective_genre_expr() == "genre" 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): 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. # Raw titles A/B/C → title-sort order is A, B, C on the RAW column.
_put(server, "b.archive", title="B") _put(server, "b.archive", title="B")