Sync wishlist re-add: carry the cover image through (real parity)

The re-add showed no album/single art. Cause: reconstruct returned the full track
from tracks_json AS-IS — and some syncs store tracks_json lean (no album.images),
so the re-added wishlist entry had an empty album.images even though the track's
cover was sitting right there in the track_result's image_url.

Fix: always backfill album.images from the track_result's image_url when the album
has none (and copy the dict so tracks_json isn't mutated). Real album art is kept
when present; the 250px thumb only fills a gap. Verified against a real sync row in
all three cases (full / lean tracks_json / no tracks_json) — album.images now
populated in every one. The wishlist card reads album.images, so the cover shows.
This commit is contained in:
BoulderBadgeDad 2026-06-24 16:20:07 -07:00
parent e148f859e7
commit 5cad2c02b0
2 changed files with 65 additions and 22 deletions

View file

@ -35,33 +35,49 @@ def reconstruct_sync_track_data(
sid = str(tr.get('source_track_id') or '') sid = str(tr.get('source_track_id') or '')
tracks = tracks or [] tracks = tracks or []
# Prefer the full original track: same index if its id matches, else search by id. # Base: the FULL original track (best fidelity — full album object, ids, source,
full = None # artists) by index if its id matches, else by id search. Copied so we never
# mutate the caller's tracks list.
base: Optional[Dict[str, Any]] = None
if 0 <= track_index < len(tracks): if 0 <= track_index < len(tracks):
cand = tracks[track_index] cand = tracks[track_index]
if isinstance(cand, dict) and (not sid or str(cand.get('id') or '') == sid): if isinstance(cand, dict) and (not sid or str(cand.get('id') or '') == sid):
full = cand base = dict(cand)
if full is None and sid: if base is None and sid:
full = next( match = next(
(t for t in tracks if isinstance(t, dict) and str(t.get('id') or '') == sid), (t for t in tracks if isinstance(t, dict) and str(t.get('id') or '') == sid),
None, None,
) )
if isinstance(full, dict) and full.get('id'): if match is not None:
return full base = dict(match)
# Fallback: rebuild from the track_result fields (id required). # No full track in the cache — rebuild a minimal dict from the track_result.
if not sid: if not (isinstance(base, dict) and base.get('id')):
return None if not sid:
album: Dict[str, Any] = {'name': tr.get('album') or ''} return None
if tr.get('image_url'): base = {
album['images'] = [{'url': tr['image_url']}] 'id': sid,
return { 'name': tr.get('name') or '',
'id': sid, 'artists': [{'name': tr.get('artist') or ''}],
'name': tr.get('name') or '', 'album': {'name': tr.get('album') or ''},
'artists': [{'name': tr.get('artist') or ''}], 'duration_ms': tr.get('duration_ms') or 0,
'album': album, }
'duration_ms': tr.get('duration_ms') or 0,
} # Ensure the cover carries through. The wishlist display reads
# spotify_data.album.images; tracks_json is sometimes stored WITHOUT images, so
# backfill from the track_result's image_url (the album art the sync extracted)
# whenever the album has none. This is what makes the re-add reach full parity
# with the original auto-add's appearance.
album = base.get('album')
if not isinstance(album, dict):
album = {'name': base.get('name', '')}
else:
album = dict(album)
img = tr.get('image_url')
if img and not album.get('images'):
album['images'] = [{'url': img}]
base['album'] = album
return base
__all__ = ["reconstruct_sync_track_data"] __all__ = ["reconstruct_sync_track_data"]

View file

@ -29,8 +29,35 @@ def test_prefers_full_original_track_by_index():
trs = [_tr(0, "sp_a"), _tr(1, "sp_b")] trs = [_tr(0, "sp_a"), _tr(1, "sp_b")]
tracks = [_full("sp_a"), _full("sp_b", name="Other")] tracks = [_full("sp_a"), _full("sp_b", name="Other")]
out = reconstruct_sync_track_data(trs, tracks, 1) out = reconstruct_sync_track_data(trs, tracks, 1)
assert out is tracks[1] # exact original (full album+images) assert out["id"] == "sp_b" # the full original track
assert out["album"]["images"][0]["url"] == "http://cdn/cover.jpg" assert out["album"]["images"][0]["url"] == "http://cdn/cover.jpg" # its own art kept
assert out is not tracks[1] # a copy, not the source entry
def test_backfills_album_image_when_full_track_has_none():
# THE BUG: tracks_json stored without album.images, but the track_result has the
# cover. The re-add must carry the image through (parity with the auto-add).
trs = [_tr(0, "sp_a", image_url="http://img/cover250.jpg")]
tracks = [_full("sp_a", with_images=False)] # full data but NO images
out = reconstruct_sync_track_data(trs, tracks, 0)
assert out["id"] == "sp_a"
assert out["album"]["images"] == [{"url": "http://img/cover250.jpg"}]
def test_full_track_with_images_is_not_overwritten():
# When the cached track already has real album art, keep it (don't downgrade to
# the 250px track_result thumb).
trs = [_tr(0, "sp_a", image_url="http://img/thumb.jpg")]
tracks = [_full("sp_a", with_images=True)]
out = reconstruct_sync_track_data(trs, tracks, 0)
assert out["album"]["images"][0]["url"] == "http://cdn/cover.jpg" # original kept
def test_does_not_mutate_source_tracks_list():
trs = [_tr(0, "sp_a", image_url="http://img/x.jpg")]
tracks = [_full("sp_a", with_images=False)]
reconstruct_sync_track_data(trs, tracks, 0)
assert "images" not in tracks[0]["album"] # source untouched
def test_matches_by_id_when_index_misaligns(): def test_matches_by_id_when_index_misaligns():