feat: album art fallback for artists without images

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.
This commit is contained in:
2026-03-30 16:42:25 -04:00
parent cec69dede2
commit 9adebad31d
2 changed files with 93 additions and 1 deletions
@@ -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;
}
}
}
}
}
}
@@ -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();
}
/**