Preserve artwork in quality scanner wishlist handoff

- carry track-level album art through the quality scanner normalization path
- preserve artist artwork when provider results expose it
- keep album.image_url and album.images populated so the wishlist UI can render the cover consistently
- add a regression test covering provider payloads with image_url on both the track and artist
pull/445/head
Antti Kettunen 4 weeks ago
parent 761fc29523
commit 2bc8e8a27b
No known key found for this signature in database
GPG Key ID: C6B2A3D250359BD7

@ -87,7 +87,15 @@ def _normalize_track_artists(track_item: Any) -> list[dict]:
if not artist_name and isinstance(artist, (str, bytes)):
artist_name = artist
if artist_name:
normalized.append({'name': str(artist_name)})
artist_data = {'name': str(artist_name)}
artist_images = _normalize_image_entries(_extract_lookup_value(artist, 'images', default=[]))
artist_image_url = _extract_lookup_value(artist, 'image_url', 'artist_image_url', default=None)
if artist_image_url and not artist_images:
artist_images = [{'url': str(artist_image_url)}]
if artist_images:
artist_data['images'] = artist_images
artist_data['image_url'] = artist_images[0].get('url')
normalized.append(artist_data)
if not normalized:
normalized.append({'name': 'Unknown Artist'})
@ -95,6 +103,43 @@ def _normalize_track_artists(track_item: Any) -> list[dict]:
return normalized
def _normalize_image_entries(image_value: Any) -> list[dict]:
if not image_value:
return []
if isinstance(image_value, dict):
image_value = [image_value]
elif isinstance(image_value, (str, bytes)):
image_value = [image_value]
else:
try:
image_value = list(image_value)
except TypeError:
return []
normalized = []
seen_urls = set()
for image in image_value:
if isinstance(image, dict):
image_url = image.get('url') or image.get('image_url')
if not image_url:
continue
image_dict = dict(image)
image_dict['url'] = str(image_url)
elif isinstance(image, (str, bytes)):
image_dict = {'url': str(image)}
else:
continue
if image_dict['url'] in seen_urls:
continue
seen_urls.add(image_dict['url'])
normalized.append(image_dict)
return normalized
def _normalize_track_album(track_item: Any) -> dict:
album = _extract_lookup_value(track_item, 'album', default={})
if isinstance(album, dict):
@ -102,7 +147,6 @@ def _normalize_track_album(track_item: Any) -> dict:
else:
album_data = {
'name': _extract_lookup_value(album, 'name', 'title', default=str(album) if album else '') or '',
'images': _extract_lookup_value(album, 'images', default=[]) or [],
'album_type': _extract_lookup_value(album, 'album_type', default='album') or 'album',
'total_tracks': _extract_lookup_value(album, 'total_tracks', 'track_count', default=0) or 0,
'release_date': _extract_lookup_value(album, 'release_date', default='') or '',
@ -112,10 +156,30 @@ def _normalize_track_album(track_item: Any) -> dict:
album_data.setdefault('album_type', _extract_lookup_value(track_item, 'album_type', default='album') or 'album')
album_data.setdefault('total_tracks', _extract_lookup_value(track_item, 'total_tracks', 'track_count', default=0) or 0)
album_data.setdefault('release_date', _extract_lookup_value(track_item, 'release_date', default='') or '')
if isinstance(album, dict):
album_data.setdefault('images', album.get('images', []) or [])
album_images = _normalize_image_entries(album_data.get('images'))
if not album_images and isinstance(album, dict):
album_images = _normalize_image_entries(
album.get('images')
or album.get('image_url')
or album.get('album_cover_url')
or album.get('cover_url')
)
if not album_images:
album_images = _normalize_image_entries(
_extract_lookup_value(track_item, 'images', default=None)
or _extract_lookup_value(track_item, 'image_url', default=None)
or _extract_lookup_value(track_item, 'album_cover_url', default=None)
or _extract_lookup_value(track_item, 'cover_url', default=None)
)
if album_images:
album_data['images'] = album_images
album_data.setdefault('image_url', album_images[0].get('url'))
else:
album_data.setdefault('images', [])
album_data['images'] = []
album_data.setdefault('artists', _normalize_track_artists(track_item))
return album_data
@ -126,6 +190,7 @@ def _normalize_track_match(track_item: Any, provider: str) -> dict:
'name': _extract_lookup_value(track_item, 'name', 'title', default='Unknown Track') or 'Unknown Track',
'artists': _normalize_track_artists(track_item),
'album': _normalize_track_album(track_item),
'image_url': _extract_lookup_value(track_item, 'image_url', 'album_cover_url', default=None),
'duration_ms': _extract_lookup_value(track_item, 'duration_ms', default=0) or 0,
'track_number': _extract_lookup_value(track_item, 'track_number', default=1) or 1,
'disc_number': _extract_lookup_value(track_item, 'disc_number', default=1) or 1,
@ -135,6 +200,12 @@ def _normalize_track_match(track_item: Any, provider: str) -> dict:
'provider': provider,
'source': provider,
}
if not track_data['image_url']:
album_images = track_data['album'].get('images') if isinstance(track_data['album'], dict) else []
if isinstance(album_images, list) and album_images:
first_image = album_images[0]
if isinstance(first_image, dict):
track_data['image_url'] = first_image.get('url')
return ensure_wishlist_track_format(track_data)

@ -357,6 +357,42 @@ def test_match_adds_to_wishlist(mock_db_and_wishlist):
assert add_args['source_context']['original_file_path'] == '/x.mp3'
def test_match_preserves_album_and_artist_images(mock_db_and_wishlist):
"""Image metadata from the provider payload should survive the wishlist handoff."""
db, ws = mock_db_and_wishlist
db._watchlist_artists = [_WatchlistArtist('Artist')]
db._tracks = [_track_row(artist_name='Artist', title='Track', file_path='/x.mp3', bitrate=128)]
state = {}
match = {
'id': 'sp-1',
'name': 'Track',
'artists': [{'name': 'Artist', 'image_url': 'https://example.test/artist.jpg'}],
'album': 'Album',
'image_url': 'https://example.test/cover.jpg',
'duration_ms': 200000,
'popularity': 50,
'external_urls': {},
'album_type': 'album',
'release_date': '2024-01-01',
}
deps = _build_deps(
state=state,
quality_tier_result=('low_lossy', 4),
source_clients={'spotify': _FakeMetadataClient(results=[match])},
primary_source='spotify',
)
qs.run_quality_scanner('watchlist', 1, deps)
assert state['matched'] == 1
assert len(ws.added) == 1
add_args = ws.added[0]
assert add_args['track_data']['image_url'] == 'https://example.test/cover.jpg'
assert add_args['track_data']['album']['image_url'] == 'https://example.test/cover.jpg'
assert add_args['track_data']['album']['images'] == [{'url': 'https://example.test/cover.jpg'}]
assert add_args['track_data']['artists'][0]['image_url'] == 'https://example.test/artist.jpg'
def test_no_match_no_wishlist_add(mock_db_and_wishlist):
"""No match found → no wishlist add, matched stays 0."""
db, ws = mock_db_and_wishlist

Loading…
Cancel
Save