From df1d1785e90f04a57b029a84096e5cc64fad60f3 Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Sat, 28 Mar 2026 13:50:24 -0400 Subject: [PATCH] feat: artist images in library artists grid view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../components/artists-view/artists-view.ts | 70 +++++++++++++++++-- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/artists-view/artists-view.ts b/frontend/src/components/artists-view/artists-view.ts index c266c45..463afff 100644 --- a/frontend/src/components/artists-view/artists-view.ts +++ b/frontend/src/components/artists-view/artists-view.ts @@ -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(); + private artistImageLoading = new Set(); /** * 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`${name}`; + } + + return html` + ${this.getArtistInitial(name)} + `; + } + + /** + * 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 }} >
- - ${this.getArtistInitial( - artist.Name, - )} - + ${this.renderArtistAvatar(artist.Name)}