Enrich source-only artist-detail response and skip discography dedup for source artists
Source artists landing on /artist-detail were rendering an almost-blank
hero — image + name + a tiny Download button — because the backend
response only had {id, name, image_url, server_source: null, genres: []}.
The library.js renderers do their best with what they have, and that
wasn't much.
Backend changes (_build_source_only_artist_detail):
- Set the source-specific ID field (deezer_id / spotify_artist_id /
itunes_artist_id / discogs_id / soul_id / musicbrainz_id) on
artist_info so the corresponding service badge renders on the hero.
- Try the source's own get_artist_info / get_artist for genres +
followers (Spotify always; Deezer/iTunes/Discogs when available).
Spotify also fills image_url if metadata_service.get_artist_image_url
came up empty.
- Last.fm enrichment by artist name — bio + listeners + playcount +
lastfm_url. Mirrors what library artists get from the cached
enrichment workers but on demand for source artists.
- All enrichment lookups are wrapped in try/except so a 500 from any
one source doesn't break the whole response.
Frontend (library.js populateArtistDetailPage):
- Watchlist button now initialises for source artists too. Falls back
to artist.id + artist.name when there's no canonical Spotify
identity (which is the common case for non-library artists).
Discography dedup opt-out:
- Added dedup_variants flag to MetadataLookupOptions (default True so
library artists are unchanged). Source-only path now passes
dedup_variants=False so every "Deluxe Edition" / "Remastered" /
"Anniversary" variant the source returns is shown — matches the
inline /artists page behaviour the user was comparing against.
Result: source artists' hero now shows badges + bio + listeners +
playcount + watchlist button + genres in addition to image and name.
Discography lists every release the source returns, not the deduped
canonical view.
This commit is contained in:
parent
93f1941829
commit
f936b8cb12
3 changed files with 130 additions and 14 deletions
|
|
@ -36,6 +36,10 @@ class MetadataLookupOptions:
|
|||
max_pages: int = 0
|
||||
limit: int = 50
|
||||
artist_source_ids: Optional[Dict[str, str]] = None
|
||||
dedup_variants: bool = True # Collapse "Deluxe Edition" / "Remastered" etc.
|
||||
# into a single canonical release card. Off
|
||||
# gives the inline-Artists-page behaviour of
|
||||
# showing every variant the source returns.
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
|
@ -633,9 +637,10 @@ def get_artist_detail_discography(
|
|||
else:
|
||||
albums.append(card)
|
||||
|
||||
albums = _dedup_variant_releases(albums)
|
||||
eps = _dedup_variant_releases(eps)
|
||||
singles = _dedup_variant_releases(singles)
|
||||
if options is None or options.dedup_variants:
|
||||
albums = _dedup_variant_releases(albums)
|
||||
eps = _dedup_variant_releases(eps)
|
||||
singles = _dedup_variant_releases(singles)
|
||||
|
||||
albums = _sort_discography_releases(albums)
|
||||
eps = _sort_discography_releases(eps)
|
||||
|
|
|
|||
119
web_server.py
119
web_server.py
|
|
@ -11321,28 +11321,112 @@ _SOURCE_ONLY_ARTIST_SOURCES = frozenset({
|
|||
})
|
||||
|
||||
|
||||
_SOURCE_ID_FIELD = {
|
||||
'spotify': 'spotify_artist_id',
|
||||
'itunes': 'itunes_artist_id',
|
||||
'deezer': 'deezer_id',
|
||||
'discogs': 'discogs_id',
|
||||
'hydrabase': 'soul_id',
|
||||
'musicbrainz': 'musicbrainz_id',
|
||||
}
|
||||
|
||||
|
||||
def _build_source_only_artist_detail(artist_id, artist_name, source):
|
||||
"""Synthesize an artist-detail response from a single metadata source for an
|
||||
artist that isn't in the local library. Used when `/api/artist-detail/<id>`
|
||||
is called with a `source` param and the library DB lookup misses."""
|
||||
is called with a `source` param and the library DB lookup misses.
|
||||
|
||||
Enriches the response with whatever metadata we can pull on demand:
|
||||
- Image URL via metadata_service helper
|
||||
- Source-specific ID field (so the source's own service badge renders)
|
||||
- Genres from the source's get_artist_info if available
|
||||
- Last.fm bio + listeners + playcount + URL by artist name
|
||||
"""
|
||||
from core.metadata_service import (
|
||||
MetadataLookupOptions,
|
||||
get_artist_detail_discography as _get_artist_detail_discography,
|
||||
get_artist_image_url as _get_artist_image_url,
|
||||
)
|
||||
|
||||
# Resolve artist image via the same helper that powers /api/artist/<id>/image
|
||||
resolved_name = (artist_name or artist_id or '').strip()
|
||||
|
||||
# 1. Image URL via the same helper /api/artist/<id>/image uses.
|
||||
image_url = None
|
||||
try:
|
||||
image_url = _get_artist_image_url(artist_id, source_override=source)
|
||||
except Exception as e:
|
||||
logger.debug(f"Artist image lookup failed for {source}:{artist_id}: {e}")
|
||||
|
||||
# Fetch discography from the specified source, with source_override pinned so
|
||||
# the fallback chain starts with the caller-requested provider.
|
||||
# 2. Source-side artist info (image, genres, followers depending on source).
|
||||
source_genres = []
|
||||
source_followers = None
|
||||
try:
|
||||
if source == 'spotify' and spotify_client and spotify_client.is_spotify_authenticated():
|
||||
sp_artist = spotify_client.get_artist(artist_id, allow_fallback=False)
|
||||
if sp_artist:
|
||||
source_genres = sp_artist.get('genres') or []
|
||||
source_followers = (sp_artist.get('followers') or {}).get('total')
|
||||
if not image_url and sp_artist.get('images'):
|
||||
image_url = sp_artist['images'][0].get('url')
|
||||
elif source == 'deezer':
|
||||
dz = _get_deezer_client()
|
||||
if dz:
|
||||
dz_artist = dz.get_artist_info(artist_id)
|
||||
if dz_artist:
|
||||
source_genres = dz_artist.get('genres') or []
|
||||
source_followers = (dz_artist.get('followers') or {}).get('total')
|
||||
elif source == 'itunes':
|
||||
it = _get_itunes_client()
|
||||
if it:
|
||||
it_artist = it.get_artist(artist_id)
|
||||
if it_artist:
|
||||
source_genres = it_artist.get('genres') or []
|
||||
elif source == 'discogs':
|
||||
token = config_manager.get('discogs.token', '')
|
||||
if token:
|
||||
dc = _get_discogs_client(token)
|
||||
if dc:
|
||||
dc_artist = dc.get_artist(artist_id)
|
||||
if dc_artist:
|
||||
source_genres = dc_artist.get('genres') or []
|
||||
except Exception as e:
|
||||
logger.debug(f"Source-side artist info lookup failed for {source}:{artist_id}: {e}")
|
||||
|
||||
# 3. Last.fm enrichment by artist name — bio + listeners + playcount + url.
|
||||
lastfm_bio = None
|
||||
lastfm_listeners = None
|
||||
lastfm_playcount = None
|
||||
lastfm_url = None
|
||||
if resolved_name:
|
||||
try:
|
||||
from core.lastfm_client import LastFMClient
|
||||
lastfm = LastFMClient()
|
||||
if lastfm and getattr(lastfm, 'enabled', True):
|
||||
lf_info = lastfm.get_artist_info(resolved_name)
|
||||
if lf_info:
|
||||
bio_obj = lf_info.get('bio') or {}
|
||||
lastfm_bio = bio_obj.get('content') or bio_obj.get('summary')
|
||||
stats_obj = lf_info.get('stats') or {}
|
||||
if stats_obj.get('listeners'):
|
||||
try:
|
||||
lastfm_listeners = int(stats_obj['listeners'])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if stats_obj.get('playcount'):
|
||||
try:
|
||||
lastfm_playcount = int(stats_obj['playcount'])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
lastfm_url = lf_info.get('url')
|
||||
except Exception as e:
|
||||
logger.debug(f"Last.fm enrichment failed for {resolved_name}: {e}")
|
||||
|
||||
# 4. Discography from the specified source. Skip the variant-dedup so the
|
||||
# page shows every release the source returns — matches the inline
|
||||
# Artists-page behaviour the user is comparing against.
|
||||
discography_result = _get_artist_detail_discography(
|
||||
artist_id,
|
||||
artist_name=artist_name or artist_id,
|
||||
artist_name=resolved_name or artist_id,
|
||||
options=MetadataLookupOptions(
|
||||
source_override=source,
|
||||
allow_fallback=True,
|
||||
|
|
@ -11350,6 +11434,7 @@ def _build_source_only_artist_detail(artist_id, artist_name, source):
|
|||
max_pages=0,
|
||||
limit=50,
|
||||
artist_source_ids={source: artist_id},
|
||||
dedup_variants=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -11362,17 +11447,35 @@ def _build_source_only_artist_detail(artist_id, artist_name, source):
|
|||
|
||||
artist_info = {
|
||||
"id": artist_id,
|
||||
"name": artist_name or artist_id,
|
||||
"name": resolved_name or artist_id,
|
||||
"image_url": image_url,
|
||||
"server_source": None, # not in library
|
||||
"genres": [],
|
||||
"genres": source_genres,
|
||||
}
|
||||
|
||||
# Set the source-specific ID so the corresponding service badge renders on
|
||||
# the hero (e.g. source=deezer -> deezer_id; source=spotify -> spotify_artist_id).
|
||||
source_id_field = _SOURCE_ID_FIELD.get(source)
|
||||
if source_id_field:
|
||||
artist_info[source_id_field] = artist_id
|
||||
|
||||
if source_followers is not None:
|
||||
artist_info['followers'] = source_followers
|
||||
if lastfm_bio:
|
||||
artist_info['lastfm_bio'] = lastfm_bio
|
||||
if lastfm_listeners is not None:
|
||||
artist_info['lastfm_listeners'] = lastfm_listeners
|
||||
if lastfm_playcount is not None:
|
||||
artist_info['lastfm_playcount'] = lastfm_playcount
|
||||
if lastfm_url:
|
||||
artist_info['lastfm_url'] = lastfm_url
|
||||
|
||||
logger.info(
|
||||
f"Source-only artist-detail: {artist_info['name']} from {source} — "
|
||||
f"albums={len(discography_result.get('albums', []))}, "
|
||||
f"eps={len(discography_result.get('eps', []))}, "
|
||||
f"singles={len(discography_result.get('singles', []))}"
|
||||
f"singles={len(discography_result.get('singles', []))}, "
|
||||
f"genres={len(source_genres)}, lastfm_bio={'yes' if lastfm_bio else 'no'}"
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
|
|
|
|||
|
|
@ -966,10 +966,18 @@ function populateArtistDetailPage(data) {
|
|||
// Populate discography sections
|
||||
populateDiscographySections(discography);
|
||||
|
||||
// Initialize library watchlist button if it exists (for library page)
|
||||
// Initialize the watchlist button. Library artists that have been enriched
|
||||
// get the canonical Spotify identity; source artists fall back to the id
|
||||
// they came in with (Deezer/iTunes/Discogs/etc.).
|
||||
const libraryWatchlistBtn = document.getElementById('library-artist-watchlist-btn');
|
||||
if (libraryWatchlistBtn && data.spotify_artist && data.spotify_artist.spotify_artist_id) {
|
||||
initializeLibraryWatchlistButton(data.spotify_artist.spotify_artist_id, data.spotify_artist.spotify_artist_name);
|
||||
if (libraryWatchlistBtn) {
|
||||
const watchlistId = (data.spotify_artist && data.spotify_artist.spotify_artist_id)
|
||||
|| artist.id;
|
||||
const watchlistName = (data.spotify_artist && data.spotify_artist.spotify_artist_name)
|
||||
|| artist.name;
|
||||
if (watchlistId && watchlistName) {
|
||||
initializeLibraryWatchlistButton(watchlistId, watchlistName);
|
||||
}
|
||||
}
|
||||
|
||||
// Load Similar Artists section (works for both library + source artists via
|
||||
|
|
|
|||
Loading…
Reference in a new issue