From 89754480beb441ae855d51e126a94b23dc0adbac Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Wed, 22 Apr 2026 15:28:28 -0700 Subject: [PATCH] Source-aware /api/artist-detail: fall back to metadata source when not in library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/?source=&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/ /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=&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. --- web_server.py | 93 ++++++++++++++++++++++++++++++++++++++++- webui/static/library.js | 17 +++++++- 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/web_server.py b/web_server.py index 77ae5381..58914c45 100644 --- a/web_server.py +++ b/web_server.py @@ -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/` + 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//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/') 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=&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, diff --git a/webui/static/library.js b/webui/static/library.js index 3be4d03c..33e58955 100644 --- a/webui/static/library.js +++ b/webui/static/library.js @@ -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}`);