From 5cad2c02b036abdf32a46704cadd3850b4a3b2df Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Wed, 24 Jun 2026 16:20:07 -0700 Subject: [PATCH] Sync wishlist re-add: carry the cover image through (real parity) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- core/sync/wishlist_readd.py | 56 ++++++++++++++++++++----------- tests/test_sync_wishlist_readd.py | 31 +++++++++++++++-- 2 files changed, 65 insertions(+), 22 deletions(-) diff --git a/core/sync/wishlist_readd.py b/core/sync/wishlist_readd.py index 08fdf90f..80cf0b81 100644 --- a/core/sync/wishlist_readd.py +++ b/core/sync/wishlist_readd.py @@ -35,33 +35,49 @@ def reconstruct_sync_track_data( sid = str(tr.get('source_track_id') or '') tracks = tracks or [] - # Prefer the full original track: same index if its id matches, else search by id. - full = None + # Base: the FULL original track (best fidelity — full album object, ids, source, + # 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): cand = tracks[track_index] if isinstance(cand, dict) and (not sid or str(cand.get('id') or '') == sid): - full = cand - if full is None and sid: - full = next( + base = dict(cand) + if base is None and sid: + match = next( (t for t in tracks if isinstance(t, dict) and str(t.get('id') or '') == sid), None, ) - if isinstance(full, dict) and full.get('id'): - return full + if match is not None: + base = dict(match) - # Fallback: rebuild from the track_result fields (id required). - if not sid: - return None - album: Dict[str, Any] = {'name': tr.get('album') or ''} - if tr.get('image_url'): - album['images'] = [{'url': tr['image_url']}] - return { - 'id': sid, - 'name': tr.get('name') or '', - 'artists': [{'name': tr.get('artist') or ''}], - 'album': album, - 'duration_ms': tr.get('duration_ms') or 0, - } + # No full track in the cache — rebuild a minimal dict from the track_result. + if not (isinstance(base, dict) and base.get('id')): + if not sid: + return None + base = { + 'id': sid, + 'name': tr.get('name') or '', + 'artists': [{'name': tr.get('artist') or ''}], + 'album': {'name': tr.get('album') or ''}, + '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"] diff --git a/tests/test_sync_wishlist_readd.py b/tests/test_sync_wishlist_readd.py index 6e048234..130a866a 100644 --- a/tests/test_sync_wishlist_readd.py +++ b/tests/test_sync_wishlist_readd.py @@ -29,8 +29,35 @@ def test_prefers_full_original_track_by_index(): trs = [_tr(0, "sp_a"), _tr(1, "sp_b")] tracks = [_full("sp_a"), _full("sp_b", name="Other")] out = reconstruct_sync_track_data(trs, tracks, 1) - assert out is tracks[1] # exact original (full album+images) - assert out["album"]["images"][0]["url"] == "http://cdn/cover.jpg" + assert out["id"] == "sp_b" # the full original track + 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():