Source-aware /api/artist-detail: fall back to metadata source when not in library
Part A of the deferred unification cleanup. The standalone artist-
detail endpoint used to 404 whenever `artist_id` wasn't a local library
primary key, which is exactly what source artists (Deezer/Spotify/
iTunes/etc.) have. That forced the Phase 4a revert: source artists had
to use the inline Artists page because this endpoint couldn't handle
them.
New behaviour:
- Library PK path — unchanged. Existing callers see the same response.
- `/api/artist-detail/<id>?source=<src>&name=<name>` with source in
(spotify, itunes, deezer, discogs, hydrabase, musicbrainz) — when
the library DB lookup misses, synthesize a response by:
• fetching artist image via metadata_service.get_artist_image_url
with source_override (the helper already backing /api/artist/
<id>/image)
• fetching discography via metadata_service.get_artist_detail_
discography with MetadataLookupOptions(source_override=source,
artist_source_ids={source: artist_id})
• returning { success, artist: {id, name, image_url, server_source:
null, genres: []}, discography, enrichment_coverage: {} }
- Library PK missing AND no source — preserves the 404 (caller didn't
give enough info to fall back).
Frontend plumbing: library.js loadArtistDetailData now appends
?source=<src>&name=<name> to the fetch URL when
artistDetailPageState.currentArtistSource is set. The field is already
seeded by navigateToArtistDetail's third arg (added during the earlier
unification work), so no new state plumbing is needed.
populateArtistDetailPage gracefully handles the missing-library-data
case per earlier exploration — owned_releases empty is fine,
enrichment_coverage optional, spotify_artist_id optional.
Part B will re-route the source-artist callsites (Search / Discover /
Watchlist / etc.) back through navigateToArtistDetail so they actually
exercise this new fallback path.
This commit is contained in:
parent
a5d97261e4
commit
89754480be
2 changed files with 106 additions and 4 deletions
|
|
@ -11316,11 +11316,93 @@ def test_artist_endpoint(artist_id):
|
|||
"message": f"Test endpoint working for artist ID: {artist_id}"
|
||||
})
|
||||
|
||||
_SOURCE_ONLY_ARTIST_SOURCES = frozenset({
|
||||
'spotify', 'itunes', 'deezer', 'discogs', 'hydrabase', 'musicbrainz',
|
||||
})
|
||||
|
||||
|
||||
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."""
|
||||
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
|
||||
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.
|
||||
discography_result = _get_artist_detail_discography(
|
||||
artist_id,
|
||||
artist_name=artist_name or artist_id,
|
||||
options=MetadataLookupOptions(
|
||||
source_override=source,
|
||||
allow_fallback=True,
|
||||
skip_cache=False,
|
||||
max_pages=0,
|
||||
limit=50,
|
||||
artist_source_ids={source: artist_id},
|
||||
),
|
||||
)
|
||||
|
||||
if not discography_result.get('success'):
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": discography_result.get('error', 'Could not load discography'),
|
||||
"source": source,
|
||||
}), 404
|
||||
|
||||
artist_info = {
|
||||
"id": artist_id,
|
||||
"name": artist_name or artist_id,
|
||||
"image_url": image_url,
|
||||
"server_source": None, # not in library
|
||||
"genres": [],
|
||||
}
|
||||
|
||||
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', []))}"
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"artist": artist_info,
|
||||
"discography": discography_result,
|
||||
"enrichment_coverage": {},
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/artist-detail/<artist_id>')
|
||||
def get_artist_detail(artist_id):
|
||||
"""Get artist detail data"""
|
||||
"""Get artist detail data.
|
||||
|
||||
For library artists, `artist_id` is the local DB primary key and the full
|
||||
library-aware path runs (owned releases + merged source discography + per-
|
||||
service enrichment coverage).
|
||||
|
||||
For source artists (Spotify/Deezer/iTunes/etc. that aren't in the library
|
||||
yet), pass `?source=<source>&name=<artist_name>` and the endpoint synthesizes
|
||||
a response directly from the metadata source — no owned releases, just name +
|
||||
image + discography so the artist-detail page can still render.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Getting artist detail for ID: {artist_id}")
|
||||
source_param = (request.args.get('source', '') or '').strip().lower()
|
||||
artist_name_arg = (request.args.get('name', '') or '').strip()
|
||||
logger.info(
|
||||
f"Getting artist detail for ID: {artist_id} "
|
||||
f"(source={source_param or 'library'})"
|
||||
)
|
||||
|
||||
# Get database instance
|
||||
database = get_database()
|
||||
|
|
@ -11329,6 +11411,13 @@ def get_artist_detail(artist_id):
|
|||
db_result = database.get_artist_discography(artist_id)
|
||||
|
||||
if not db_result.get('success'):
|
||||
# Library lookup failed. If a metadata source was specified, fall back
|
||||
# to a source-only response so the page can render a non-library artist.
|
||||
if source_param in _SOURCE_ONLY_ARTIST_SOURCES:
|
||||
return _build_source_only_artist_detail(
|
||||
artist_id, artist_name_arg, source_param
|
||||
)
|
||||
|
||||
logger.error(f"Database returned error: {db_result}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
|
|
|
|||
|
|
@ -764,8 +764,21 @@ async function loadArtistDetailData(artistId, artistName) {
|
|||
// Don't update header until data loads to avoid showing stale data
|
||||
|
||||
try {
|
||||
// Call API to get artist discography data
|
||||
const response = await fetch(`/api/artist-detail/${artistId}`);
|
||||
// Call API to get artist discography data. If this artist came from a
|
||||
// metadata source (not the library), pass source + name so the backend
|
||||
// can synthesize a response from that source instead of 404ing on the
|
||||
// local DB lookup.
|
||||
const params = new URLSearchParams();
|
||||
if (artistDetailPageState.currentArtistSource) {
|
||||
params.set('source', artistDetailPageState.currentArtistSource);
|
||||
}
|
||||
if (artistName) {
|
||||
params.set('name', artistName);
|
||||
}
|
||||
const qs = params.toString();
|
||||
const response = await fetch(
|
||||
`/api/artist-detail/${encodeURIComponent(artistId)}${qs ? '?' + qs : ''}`
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load artist data: ${response.statusText}`);
|
||||
|
|
|
|||
Loading…
Reference in a new issue