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)):
|
if not artist_name and isinstance(artist, (str, bytes)):
|
||||||
artist_name = artist
|
artist_name = artist
|
||||||
if artist_name:
|
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:
|
if not normalized:
|
||||||
normalized.append({'name': 'Unknown Artist'})
|
normalized.append({'name': 'Unknown Artist'})
|
||||||
|
|
@ -95,6 +103,43 @@ def _normalize_track_artists(track_item: Any) -> list[dict]:
|
||||||
return normalized
|
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:
|
def _normalize_track_album(track_item: Any) -> dict:
|
||||||
album = _extract_lookup_value(track_item, 'album', default={})
|
album = _extract_lookup_value(track_item, 'album', default={})
|
||||||
if isinstance(album, dict):
|
if isinstance(album, dict):
|
||||||
|
|
@ -102,7 +147,6 @@ def _normalize_track_album(track_item: Any) -> dict:
|
||||||
else:
|
else:
|
||||||
album_data = {
|
album_data = {
|
||||||
'name': _extract_lookup_value(album, 'name', 'title', default=str(album) if album else '') or '',
|
'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',
|
'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,
|
'total_tracks': _extract_lookup_value(album, 'total_tracks', 'track_count', default=0) or 0,
|
||||||
'release_date': _extract_lookup_value(album, 'release_date', default='') or '',
|
'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('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('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 '')
|
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:
|
else:
|
||||||
album_data.setdefault('images', [])
|
album_data['images'] = []
|
||||||
|
|
||||||
album_data.setdefault('artists', _normalize_track_artists(track_item))
|
album_data.setdefault('artists', _normalize_track_artists(track_item))
|
||||||
return album_data
|
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',
|
'name': _extract_lookup_value(track_item, 'name', 'title', default='Unknown Track') or 'Unknown Track',
|
||||||
'artists': _normalize_track_artists(track_item),
|
'artists': _normalize_track_artists(track_item),
|
||||||
'album': _normalize_track_album(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,
|
'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,
|
'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,
|
'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,
|
'provider': provider,
|
||||||
'source': 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)
|
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'
|
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):
|
def test_no_match_no_wishlist_add(mock_db_and_wishlist):
|
||||||
"""No match found → no wishlist add, matched stays 0."""
|
"""No match found → no wishlist add, matched stays 0."""
|
||||||
db, ws = mock_db_and_wishlist
|
db, ws = mock_db_and_wishlist
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue