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
This commit is contained in:
parent
761fc29523
commit
2bc8e8a27b
2 changed files with 112 additions and 5 deletions
|
|
@ -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…
Reference in a new issue