Preserve cover art in discovery fix-modal cache matched_data

Companion fix to the provider-hardcode bug (6ceedc8). The cache
matched_data built by the 5 update_match / fix endpoints was dropping
image_url and album.images when album came back as a bare string —
common for Deezer and iTunes search results. Cache hits on re-discovery
then produced downloads with no artwork.

Each save site now carries image info through:
- album_obj gets image_url + images:[{url}] populated from spotify_track.image_url
- matched_data adds top-level image_url for pipeline consumers that check there
- Works for both dict-shaped album (Spotify) and string-shaped album (Deezer/iTunes)

Mirrors the handling already present in _build_fix_modal_spotify_data for
the in-memory result['spotify_data'] — explains why the UI showed art fine
during the fix modal but the cached entry lost it after restart.

save_discovery_cache_match uses INSERT OR REPLACE, so existing bad cache
entries refresh when the user re-fixes the track. No manual cache clearing
needed.

Added to 2.38 changelog (same round of discovery-fix work).
This commit is contained in:
Broque Thomas 2026-04-21 21:26:36 -07:00
parent 6ceedc8fd4
commit 03b7230ac2
2 changed files with 82 additions and 5 deletions

View file

@ -34562,8 +34562,23 @@ def update_tidal_discovery_match():
artists_list = spotify_track['artists']
if isinstance(artists_list, list):
artists_list = [a if isinstance(a, str) else a.get('name', '') for a in artists_list]
# Preserve cover image info so the download pipeline can find
# artwork when this cached match is used later. The fix modal
# sends image_url at the top level; search results often return
# album as a bare string, which previously dropped the artwork.
image_url = spotify_track.get('image_url') or ''
album_raw = spotify_track.get('album', '')
album_obj = album_raw if isinstance(album_raw, dict) else {'name': album_raw or ''}
if isinstance(album_raw, dict):
album_obj = dict(album_raw)
if image_url and not album_obj.get('image_url'):
album_obj['image_url'] = image_url
if image_url and not album_obj.get('images'):
album_obj['images'] = [{'url': image_url}]
else:
album_obj = {'name': album_raw or ''}
if image_url:
album_obj['image_url'] = image_url
album_obj['images'] = [{'url': image_url}]
matched_data = {
'id': spotify_track['id'],
@ -34571,6 +34586,7 @@ def update_tidal_discovery_match():
'artists': artists_list,
'album': album_obj,
'duration_ms': spotify_track.get('duration_ms', 0),
'image_url': image_url,
'source': 'spotify',
}
cache_db = get_database()
@ -36228,8 +36244,23 @@ def update_deezer_discovery_match():
artists_list = spotify_track['artists']
if isinstance(artists_list, list):
artists_list = [a if isinstance(a, str) else a.get('name', '') for a in artists_list]
# Preserve cover image info so the download pipeline can find
# artwork when this cached match is used later. The fix modal
# sends image_url at the top level; search results often return
# album as a bare string, which previously dropped the artwork.
image_url = spotify_track.get('image_url') or ''
album_raw = spotify_track.get('album', '')
album_obj = album_raw if isinstance(album_raw, dict) else {'name': album_raw or ''}
if isinstance(album_raw, dict):
album_obj = dict(album_raw)
if image_url and not album_obj.get('image_url'):
album_obj['image_url'] = image_url
if image_url and not album_obj.get('images'):
album_obj['images'] = [{'url': image_url}]
else:
album_obj = {'name': album_raw or ''}
if image_url:
album_obj['image_url'] = image_url
album_obj['images'] = [{'url': image_url}]
matched_data = {
'id': spotify_track['id'],
@ -36237,6 +36268,7 @@ def update_deezer_discovery_match():
'artists': artists_list,
'album': album_obj,
'duration_ms': spotify_track.get('duration_ms', 0),
'image_url': image_url,
'source': 'spotify',
}
cache_db = get_database()
@ -37077,8 +37109,23 @@ def update_spotify_public_discovery_match():
artists_list = spotify_track['artists']
if isinstance(artists_list, list):
artists_list = [a if isinstance(a, str) else a.get('name', '') for a in artists_list]
# Preserve cover image info so the download pipeline can find
# artwork when this cached match is used later. The fix modal
# sends image_url at the top level; search results often return
# album as a bare string, which previously dropped the artwork.
image_url = spotify_track.get('image_url') or ''
album_raw = spotify_track.get('album', '')
album_obj = album_raw if isinstance(album_raw, dict) else {'name': album_raw or ''}
if isinstance(album_raw, dict):
album_obj = dict(album_raw)
if image_url and not album_obj.get('image_url'):
album_obj['image_url'] = image_url
if image_url and not album_obj.get('images'):
album_obj['images'] = [{'url': image_url}]
else:
album_obj = {'name': album_raw or ''}
if image_url:
album_obj['image_url'] = image_url
album_obj['images'] = [{'url': image_url}]
matched_data = {
'id': spotify_track['id'],
@ -37086,6 +37133,7 @@ def update_spotify_public_discovery_match():
'artists': artists_list,
'album': album_obj,
'duration_ms': spotify_track.get('duration_ms', 0),
'image_url': image_url,
'source': 'spotify',
}
cache_db = get_database()
@ -38030,8 +38078,23 @@ def update_youtube_discovery_match():
artists_list = spotify_track['artists']
if isinstance(artists_list, list):
artists_list = [a if isinstance(a, str) else a.get('name', '') for a in artists_list]
# Preserve cover image info so the download pipeline can find
# artwork when this cached match is used later. The fix modal
# sends image_url at the top level; search results often return
# album as a bare string, which previously dropped the artwork.
image_url = spotify_track.get('image_url') or ''
album_raw = spotify_track.get('album', '')
album_obj = album_raw if isinstance(album_raw, dict) else {'name': album_raw or ''}
if isinstance(album_raw, dict):
album_obj = dict(album_raw)
if image_url and not album_obj.get('image_url'):
album_obj['image_url'] = image_url
if image_url and not album_obj.get('images'):
album_obj['images'] = [{'url': image_url}]
else:
album_obj = {'name': album_raw or ''}
if image_url:
album_obj['image_url'] = image_url
album_obj['images'] = [{'url': image_url}]
matched_data = {
'id': spotify_track['id'],
@ -38039,6 +38102,7 @@ def update_youtube_discovery_match():
'artists': artists_list,
'album': album_obj,
'duration_ms': spotify_track.get('duration_ms', 0),
'image_url': image_url,
'source': 'spotify',
}
cache_db = get_database()
@ -49757,11 +49821,23 @@ def fix_discovery_pool_track():
# Build matched_data in the same format as the discovery flow
artists = spotify_track.get('artists', [])
album_raw = spotify_track.get('album', '')
album_obj = album_raw if isinstance(album_raw, dict) else {'name': album_raw or ''}
image_url = spotify_track.get('image_url', '')
if not image_url and isinstance(album_raw, dict):
images = album_raw.get('images', [])
image_url = images[0].get('url', '') if images else ''
# Ensure album carries the artwork too — download pipeline checks
# album.images / album.image_url when extracting cover art.
if isinstance(album_raw, dict):
album_obj = dict(album_raw)
if image_url and not album_obj.get('image_url'):
album_obj['image_url'] = image_url
if image_url and not album_obj.get('images'):
album_obj['images'] = [{'url': image_url}]
else:
album_obj = {'name': album_raw or ''}
if image_url:
album_obj['image_url'] = image_url
album_obj['images'] = [{'url': image_url}]
matched_data = {
'id': spotify_track.get('id', ''),

View file

@ -3602,6 +3602,7 @@ const WHATS_NEW = {
'2.38': [
// --- April 21, 2026 (late) ---
{ date: 'April 21, 2026 (late)' },
{ title: 'Fix Missing Cover Art on Manually Fixed Discovery Tracks', desc: 'The cache matched_data built by the fix modal dropped the image_url and album.images fields when album came back as a bare string (common for Deezer/iTunes search results). Result: re-discovery used the cached match but downloads showed no artwork. Cache writes now carry image_url through to album.images + top-level matched_data, matching what the in-memory state already did. Re-fix the track to refresh its cache entry (INSERT OR REPLACE)', page: 'sync' },
{ title: 'Fix Manual Discovery Fixes Lost After Restart (Non-Spotify Users)', desc: 'When you clicked Fix on a discovery track and picked a manual match, the cache save hardcoded the provider as "spotify" regardless of your configured primary metadata source. On re-scan, the worker queried the cache with your actual primary (Deezer, iTunes, Discogs, Hydrabase) and missed the fix entirely. All 5 save sites (Tidal / Deezer / Spotify Public / YouTube / Discovery Pool) now use the active primary source, matching what the automatic workers already do', page: 'sync' },
],
'2.37': [