#766 follow-on: source rows borrow their matched server track's cover

A source row with no art of its own (e.g. a YouTube source, which provides
none at mirror time) now borrows the cover from its MATCHED server track, so
both sides of the sync editor show an image.

The endpoint already had a borrow fallback (_server_art_map), but it matched by
an exact normalized "{artist}|{title}" key — so a YouTube-shaped row like
"Arctic Monkeys - Do I Wanna Know?" never matched the library's "Do I Wanna
Know?" and stayed blank even though the server had the cover. This borrow is
keyed off the ACTUAL source<->server pairing the reconcile already computed, so
it works for those rows once #768's canonical matching pairs them.

Done in the pure reconcile_playlist (final pass), so no frontend change is
needed — the editor already renders source_track.image_url. Guarded so it only
fills an EMPTY source image (Spotify/CDN art is never overwritten) and only when
the matched server track actually has a thumb.

Composes with the rest: #766 made the server cover URL work, #768 made the
YouTube row match, this makes the matched source row borrow that cover — so an
artless YouTube row matched to a Navidrome track with art shows on both sides.

Tests: tests/test_playlist_reconcile.py (+4) — artless source borrows the
matched cover; source with its own art keeps it; unmatched source has nothing to
borrow; borrow skipped when the server track has no thumb. 15 reconcile + 59
sync/navidrome tests pass.
pull/776/head
BoulderBadgeDad 1 week ago
parent 89b438974f
commit cd9e4abc7c

@ -180,6 +180,17 @@ def reconcile_playlist(
'confidence': 0.0,
})
# #766: a source row with no art of its own (e.g. a YouTube source, which
# provides none) borrows its MATCHED server track's cover so both sides of
# the editor show an image. Keyed off the actual pairing — works for
# "Artist - Title" rows that a fuzzy title lookup would miss. Source rows
# that already have their own art (Spotify CDN, etc.) keep it.
for entry in combined:
st = entry.get('source_track')
sv = entry.get('server_track')
if st and sv and not st.get('image_url') and sv.get('thumb'):
st['image_url'] = sv['thumb']
return combined

@ -116,6 +116,46 @@ def test_duplicate_server_tracks_one_matched_one_extra():
assert sorted(c["match_status"] for c in combined) == ["extra", "matched"]
# ── #766: source borrows the matched server cover ─────────────────────────
def _svr_art(title, artist, tid, thumb):
return {"title": title, "artist": artist, "id": tid, "ratingKey": tid, "thumb": thumb}
def test_artless_source_borrows_matched_server_cover():
# YouTube-style source row, no art of its own, matched to a server track
# that has a cover -> source side borrows it.
source = [_src("Arctic Monkeys - Do I Wanna Know?", "Official Arctic Monkeys", "s1")]
server = [_svr_art("Do I Wanna Know?", "Arctic Monkeys", "nv1", "/api/navidrome/cover/al42")]
combined = reconcile_playlist(source, server)
assert combined[0]["match_status"] == "matched"
assert combined[0]["source_track"]["image_url"] == "/api/navidrome/cover/al42"
def test_source_keeps_its_own_art_when_present():
# A Spotify-style source row with its own CDN art must NOT be overwritten.
source = [_src("Do I Wanna Know?", "Arctic Monkeys", "s1", image_url="https://cdn/spotify.jpg")]
server = [_svr_art("Do I Wanna Know?", "Arctic Monkeys", "nv1", "/api/navidrome/cover/al42")]
combined = reconcile_playlist(source, server)
assert combined[0]["source_track"]["image_url"] == "https://cdn/spotify.jpg"
def test_unmatched_source_has_no_cover_to_borrow():
source = [_src("Totally Absent Song", "Ghost", "s1")]
server = [_svr_art("Something Else", "Nobody", "nv1", "/api/navidrome/cover/al99")]
combined = reconcile_playlist(source, server)
missing = [c for c in combined if c["match_status"] == "missing"]
assert missing and not missing[0]["source_track"]["image_url"]
def test_borrow_skipped_when_server_track_has_no_thumb():
source = [_src("Do I Wanna Know?", "Arctic Monkeys", "s1")]
server = [_svr("Do I Wanna Know?", "Arctic Monkeys", "nv1")] # no thumb
combined = reconcile_playlist(source, server)
assert combined[0]["match_status"] == "matched"
assert not combined[0]["source_track"]["image_url"]
def test_norm_title_helper_parity():
assert norm_title("Stay (feat. X)") == "stay"
assert norm_title("Song (2019 Remaster)") == "song"

Loading…
Cancel
Save