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 '')
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"]

View file

@ -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():