feat(library): show per-song overrides in the grid (popup slice 3)

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>
This commit is contained in:
ChrisBeWithYou
2026-07-05 21:06:23 +02:00
committed by byrongamatos
co-authored by Claude Opus 4.8
parent d1c8899487
commit c61aa008ba
2 changed files with 103 additions and 1 deletions
+32 -1
View File
@@ -556,7 +556,13 @@ def next_library_cursor(sort: str, last_song: dict | None) -> str | None:
key = "mtime" if col == "mtime" else col key = "mtime" if col == "mtime" else col
if key not in last_song or "filename" not in last_song: if key not in last_song or "filename" not in last_song:
return None return None
return _encode_cursor([last_song[key], last_song["filename"]]) # A title display-override (Fix-metadata popup) replaces last_song["title"]
# for the card, but the keyset seek runs on the RAW title column — resume
# from the raw value query_page stashed (present only when the last row's
# title was overridden), so paging never skips/dupes.
val = (last_song["_sort_title"] if (key == "title" and "_sort_title" in last_song)
else last_song[key])
return _encode_cursor([val, last_song["filename"]])
# Song-level "mastered" threshold — best accuracy across a song's arrangements # Song-level "mastered" threshold — best accuracy across a song's arrangements
@@ -4156,6 +4162,15 @@ class MetadataDB:
# per-tile state only paints while a pass runs. Cheap set membership like # per-tile state only paints while a pass runs. Cheap set membership like
# favs/estd, so the misses stay visible at rest. # favs/estd, so the misses stay visible at rest.
um = self._unmatched_set(fns) um = self._unmatched_set(fns)
# Per-song display OVERRIDES (Fix-metadata popup, slice 3). "Grid shows
# only overrides": the effective cell is the user's override else the
# pack value — a matched MusicBrainz canon NEVER silently re-titles a
# card (canon lives in the Details drawer + art). Overlaid in Python
# over the visible window, keyset-safe exactly like the P4 alias re-label
# below: the seek still runs on the raw column (the one overridable
# keyset column, title, stashes its raw value for the cursor — see
# _sort_title / next_library_cursor).
omap = self.overrides_map(fns)
# Canonical artist at display (P4): re-label the card's artist through the # Canonical artist at display (P4): re-label the card's artist through the
# alias override so "ACDC" reads as "AC/DC". Display-only — the row's sort # alias override so "ACDC" reads as "AC/DC". Display-only — the row's sort
# position (raw artist) is untouched, so a card can show a canonical name # position (raw artist) is untouched, so a card can show a canonical name
@@ -4168,6 +4183,18 @@ 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"))
# 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.
ov = omap.get(s["filename"])
if ov:
for field in ("title", "artist", "album", "year"):
cell = ov.get(field)
val = cell.get("value") if cell else None
if val:
if field == "title":
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 # Grouped rows carry the ⚑ N (chart_count) + the work_key from the
# materialized read-model, so the card can render the "N charts" chip and # materialized read-model, so the card can render the "N charts" chip and
# address the Charts drawer (GET /api/work/{work_key}/charts) without a # address the Charts drawer (GET /api/work/{work_key}/charts) without a
@@ -8825,6 +8852,10 @@ async def list_library(q: str = "", page: int = 0, size: int = 24, sort: str = "
# The cursor to resume after this page (effective sort folds in dir=desc). # The cursor to resume after this page (effective sort folds in dir=desc).
next_cursor = (next_library_cursor(_effective_keyset_sort(sort, dir), songs[-1]) next_cursor = (next_library_cursor(_effective_keyset_sort(sort, dir), songs[-1])
if (is_local and songs) else None) if (is_local and songs) else None)
# Drop the private raw-title stash query_page attached for the cursor — it's
# an internal keyset detail, not part of the card payload.
for s in songs:
s.pop("_sort_title", None)
return {"songs": songs, "total": total, "page": page, "size": size, return {"songs": songs, "total": total, "page": page, "size": size,
"next_cursor": next_cursor} "next_cursor": next_cursor}
+71
View File
@@ -146,3 +146,74 @@ def test_compose_lock_filter_strips_locked_cand_keys(server):
assert out["recording_id"] == "r" and out["title"] == "T" and out["album"] == "A" assert out["recording_id"] == "r" and out["title"] == "T" and out["album"] == "A"
# no locks → base filter returned unchanged (zero-copy common path) # no locks → base filter returned unchanged (zero-copy common path)
assert server._compose_lock_filter(None, set()) is None assert server._compose_lock_filter(None, set()) is None
# ── display overlay in the grid (slice 3) ─────────────────────────────────────
# "Grid shows only overrides": the effective cell is the user's override else the
# pack value. Display-only + keyset-safe — the seek stays on the raw column.
def _grid(server, **kw):
songs, _ = server.meta_db.query_page(**kw)
return {s["filename"]: s for s in songs}
def test_grid_shows_override_value_over_pack(server):
_put(server, "a.archive", title="Wrong Title", artist="Wrong",
album="Pack Album", year="1999")
server.meta_db.set_song_override("a.archive", "title", value="Right Title")
server.meta_db.set_song_override("a.archive", "artist", value="Right Artist")
server.meta_db.set_song_override("a.archive", "year", value="1979")
s = _grid(server)["a.archive"]
assert s["title"] == "Right Title"
assert s["artist"] == "Right Artist"
assert s["year"] == "1979"
assert s["album"] == "Pack Album" # no override → pack value shows
assert s["_sort_title"] == "Wrong Title" # raw title stashed for the cursor
def test_grid_ignores_lock_only_override(server):
_put(server, "a.archive", title="Pack Title")
server.meta_db.set_song_override("a.archive", "title", locked=True) # lock, no value
s = _grid(server)["a.archive"]
assert s["title"] == "Pack Title" # a lock without a value never retitles
assert "_sort_title" not in s # …and stashes nothing
def test_override_beats_alias_relabel_for_artist(server):
_put(server, "a.archive", artist="ACDC")
server.meta_db.set_artist_alias("ACDC", "AC/DC") # P4 alias
assert _grid(server)["a.archive"]["artist"] == "AC/DC" # alias applies alone
server.meta_db.set_song_override("a.archive", "artist", value="AC-DC (mine)")
assert _grid(server)["a.archive"]["artist"] == "AC-DC (mine)" # override wins over alias
def test_route_strips_private_sort_title(client, server):
_put(server, "a.archive", title="Pack")
server.meta_db.set_song_override("a.archive", "title", value="Shown")
row = next(s for s in client.get("/api/library?sort=title").json()["songs"]
if s["filename"] == "a.archive")
assert row["title"] == "Shown"
assert "_sort_title" not in row # private keyset stash never leaks to the client
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")
_put(server, "a.archive", title="A")
_put(server, "c.archive", title="C")
# Overrides that would reshuffle the order IF the cursor wrongly used the
# displayed value — the seek must stay on the raw title, so paging still
# covers every row exactly once (no skip/dupe).
server.meta_db.set_song_override("a.archive", "title", value="ZZZ")
server.meta_db.set_song_override("c.archive", "title", value="AAA")
seen, cursor = [], None
for _ in range(10):
url = "/api/library?sort=title&size=1" + (f"&after={cursor}" if cursor else "")
data = client.get(url).json()
if not data["songs"]:
break
seen.append(data["songs"][0]["filename"])
cursor = data["next_cursor"]
if not cursor:
break
assert sorted(seen) == ["a.archive", "b.archive", "c.archive"] # each exactly once