From 9adebad31d0e2084a326da43d86407f51cc90b5b Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Mon, 30 Mar 2026 16:42:25 -0400 Subject: [PATCH] feat: album art fallback for artists without images MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When no artist image is available from any source (library store, explore cache, MB/Wikidata API), fall back to using the artist's most popular album's cover art. Uses local library data first. Applied in three contexts: 1. Search results (explore-view): - After library image seed: checks library albums by artist name - After API fetch loop: final fallback for unresolved artists - Library-only search: checks album art in the seed pass 2. Artist detail page header (explore-artist-details): - In hydrateFromCache: checks library albums after image sources - After fetchArtistImage API call: fallback if API returned nothing The album art is displayed as a circular crop in the artist avatar, which naturally looks like an artist photo — no visual distinction needed. --- .../explore-artist-details.ts | 39 ++++++++++++- .../components/explore-view/explore-view.ts | 55 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/explore-artist-details/explore-artist-details.ts b/frontend/src/components/explore-artist-details/explore-artist-details.ts index 2f52f43..edc6735 100644 --- a/frontend/src/components/explore-artist-details/explore-artist-details.ts +++ b/frontend/src/components/explore-artist-details/explore-artist-details.ts @@ -818,6 +818,23 @@ export class ExploreArtistDetails extends LitElement { this.loadingReleases = false; } } + + // Fallback: use album cover art if no artist image found. + if (!this.artistImageURL && cachedAlbums) { + const artistName = this.artistName.toLowerCase(); + + for (const a of cachedAlbums) { + if (a.ArtistName.toLowerCase() === artistName) { + const art = a.CoverArtMedium || a.CoverArtSmall || a.CoverArtPath; + + if (art) { + this.artistImageURL = art; + + break; + } + } + } + } } private async fetchArtist(mbid: string) { @@ -925,7 +942,27 @@ export class ExploreArtistDetails extends LitElement { this.artistImageURL = url; } } catch { - // No image available — avatar stays as initial letter. + // No image available. + } + + // Fallback: album cover art if no artist image resolved. + if (!this.artistImageURL) { + const cachedAlbums = libraryStore.cachedAlbums; + if (cachedAlbums) { + const artistName = this.artistName.toLowerCase(); + + for (const a of cachedAlbums) { + if (a.ArtistName.toLowerCase() === artistName) { + const art = a.CoverArtMedium || a.CoverArtSmall; + + if (art) { + this.artistImageURL = art; + + break; + } + } + } + } } } diff --git a/frontend/src/components/explore-view/explore-view.ts b/frontend/src/components/explore-view/explore-view.ts index 0ade603..1b3f5fb 100644 --- a/frontend/src/components/explore-view/explore-view.ts +++ b/frontend/src/components/explore-view/explore-view.ts @@ -104,6 +104,26 @@ function extractYear(dateStr: string): string { return dateStr.substring(0, 4); } +/** + * Find album cover art for an artist from the library store. + * Returns the best available cover art URL, or '' if none. + */ +function getArtistAlbumArt(artistName: string): string { + const cachedAlbums = libraryStore.cachedAlbums; + if (!cachedAlbums) return ''; + + const name = artistName.toLowerCase(); + + for (const a of cachedAlbums) { + if (a.ArtistName.toLowerCase() === name) { + const art = a.CoverArtMedium || a.CoverArtSmall || a.CoverArtPath; + if (art) return art; + } + } + + return ''; +} + @customElement('explore-view') export class ExploreView extends LitElement { /* ── State ── */ @@ -606,6 +626,16 @@ export class ExploreView extends LitElement { } } + // Fallback: album art for artists without images. + for (const a of localResults.artists || []) { + if (a.mbid && !this.artistImageCache.get(a.mbid)) { + const albumArt = getArtistAlbumArt(a.name); + if (albumArt) { + this.artistImageCache.set(a.mbid, albumArt); + } + } + } + // In library-only mode, local results already have cover art // and artist images from the library store — no API calls needed. if (!exploreSettings.libraryOnly) { @@ -1001,6 +1031,16 @@ export class ExploreView extends LitElement { this.requestUpdate(); } + // Fallback: use album cover art for artists without images. + for (const a of this.results.artists) { + if (a.mbid && !this.artistImageCache.get(a.mbid)) { + const albumArt = getArtistAlbumArt(a.name); + if (albumArt) { + this.artistImageCache.set(a.mbid, albumArt); + } + } + } + // Fetch remaining from API (only artists not yet resolved). for (const a of this.results.artists) { if (this.artistImageCache.has(a.mbid)) continue; @@ -1018,6 +1058,21 @@ export class ExploreView extends LitElement { // No image — leave empty string. } } + + // Final fallback: album art for artists the API couldn't resolve. + let fallbackUpdated = false; + + for (const a of this.results.artists) { + if (a.mbid && !this.artistImageCache.get(a.mbid)) { + const albumArt = getArtistAlbumArt(a.name); + if (albumArt) { + this.artistImageCache.set(a.mbid, albumArt); + fallbackUpdated = true; + } + } + } + + if (fallbackUpdated) this.requestUpdate(); } /**