feat: artist images in library artists grid view

The local artists grid now shows Wikimedia artist photos in the
circular avatars. Each visible artist card triggers an async load:
GetArtistMBID(name) → GetArtistImageURL(mbid) → cached data URL.

Images load progressively — the initial letter placeholder shows
immediately, replaced by the photo when it resolves. Results are
cached in-memory per session. Artists without MBIDs or without
Wikimedia photos keep the initial letter fallback.

Uses the same disk-cached artist image pipeline as the explore
views — no extra network requests for previously resolved artists.
This commit is contained in:
2026-03-28 13:50:24 -04:00
parent 7f0c6d362a
commit df1d1785e9
@@ -16,6 +16,7 @@ import {
GetAlbumsByArtistByLibrary,
GetAlbumTracksByLibrary,
} from '@go/library/Library';
import { GetArtistImageURL, GetArtistMBID } from '@go/explore/Service';
import { library } from '@go/models';
import { LibraryController } from '@store/controllers/library-controller';
import { SearchController } from '@store/controllers/search-controller';
@@ -176,6 +177,8 @@ export class ArtistsView
private cachedGridEntries: ArtistEntry[] = [];
private prevFilterArtists: library.Artist[] = [];
private prevFilterTerm = '';
private artistImageCache = new Map<string, string>();
private artistImageLoading = new Set<string>();
/**
* Recompute the filtered-artists and grid-entries
@@ -294,6 +297,13 @@ export class ArtistsView
flex-shrink: 0;
}
.avatar-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
.avatar-placeholder {
color: var(
--yj-text-secondary,
@@ -975,6 +985,60 @@ export class ArtistsView
* Helpers
* ================================================================ */
private renderArtistAvatar(name: string) {
const imageURL = this.artistImageCache.get(name);
// Kick off async image load if not cached.
if (!this.artistImageCache.has(name)) {
this.loadArtistImage(name);
}
if (imageURL) {
return html`<img
class="avatar-image"
src="${imageURL}"
alt="${name}"
/>`;
}
return html`<span class="avatar-placeholder">
${this.getArtistInitial(name)}
</span>`;
}
/**
* Load artist image for a single artist. Resolves MBID by name,
* then fetches the cached image. Sequential to avoid rate limit.
*/
private loadArtistImage(name: string) {
if (this.artistImageCache.has(name) || this.artistImageLoading.has(name)) {
return;
}
this.artistImageLoading.add(name);
GetArtistMBID(name)
.then((mbid) => {
if (!mbid) return Promise.resolve('');
return GetArtistImageURL(mbid);
})
.then((url) => {
if (url) {
this.artistImageCache.set(name, url);
this.requestUpdate();
} else {
this.artistImageCache.set(name, '');
}
})
.catch(() => {
this.artistImageCache.set(name, '');
})
.finally(() => {
this.artistImageLoading.delete(name);
});
}
private getArtistInitial(
name: string,
): string {
@@ -1047,11 +1111,7 @@ export class ArtistsView
}}
>
<div class="avatar-container">
<span class="avatar-placeholder">
${this.getArtistInitial(
artist.Name,
)}
</span>
${this.renderArtistAvatar(artist.Name)}
</div>
<div
class="artist-name"