diff --git a/backend/explore/artistimage.go b/backend/explore/artistimage.go
index bd756ce..b2456d2 100644
--- a/backend/explore/artistimage.go
+++ b/backend/explore/artistimage.go
@@ -76,6 +76,16 @@ func NewArtistImageProvider(
}
}
+// GetCachedImage returns a base64 data URL from the disk cache
+// only — no network fetches. Returns "" if not cached.
+func (p *ArtistImageProvider) GetCachedImage(artistMBID string) string {
+ if artistMBID == "" || p.imageDir == "" {
+ return ""
+ }
+
+ return p.readDiskCache(artistMBID)
+}
+
// GetArtistImage returns a base64 data URL for the artist's photo.
// Checks disk cache first, then resolves via MB/Wikidata and fetches
// the image from Wikimedia Commons. Returns "" if no image.
diff --git a/backend/explore/explore.go b/backend/explore/explore.go
index 150b8b9..d2defb2 100644
--- a/backend/explore/explore.go
+++ b/backend/explore/explore.go
@@ -233,6 +233,32 @@ func (e *Service) GetArtistMBID(artistName string) string {
return e.libMBID.GetArtistMBID(artistName)
}
+// GetArtistImages resolves artist images for multiple artists by
+// name in one call. Returns a map of artist name → base64 data
+// URL. Only artists with cached images are returned — no network
+// fetches are triggered (use GetArtistImageURL for on-demand fetch).
+func (e *Service) GetArtistImages(names []string) map[string]string {
+ result := make(map[string]string, len(names))
+
+ // Batch resolve all names → MBIDs from the library DB.
+ allMBIDs := e.libMBID.AllArtistMBIDs()
+
+ for _, name := range names {
+ mbid, ok := allMBIDs[name]
+ if !ok || mbid == "" {
+ continue
+ }
+
+ // Only return already-cached images — don't trigger fetches.
+ img := e.artistImg.GetCachedImage(mbid)
+ if img != "" {
+ result[name] = img
+ }
+ }
+
+ return result
+}
+
// Search concurrently queries MusicBrainz for artists, release
// groups, and recordings matching the query, then boosts results
// using ListenBrainz popularity data. The final score blends
diff --git a/frontend/src/components/artists-view/artists-view.ts b/frontend/src/components/artists-view/artists-view.ts
index 463afff..0c7f0da 100644
--- a/frontend/src/components/artists-view/artists-view.ts
+++ b/frontend/src/components/artists-view/artists-view.ts
@@ -16,7 +16,7 @@ import {
GetAlbumsByArtistByLibrary,
GetAlbumTracksByLibrary,
} from '@go/library/Library';
-import { GetArtistImageURL, GetArtistMBID } from '@go/explore/Service';
+import { GetArtistImageURL, GetArtistMBID, GetArtistImages } from '@go/explore/Service';
import { library } from '@go/models';
import { LibraryController } from '@store/controllers/library-controller';
import { SearchController } from '@store/controllers/search-controller';
@@ -439,6 +439,8 @@ export class ArtistsView
) {
this.lastArtistsRef = cached;
this.loadArtists();
+ this.imagesBatchLoaded = false;
+ void this.loadArtistImagesBatch();
}
}
@@ -454,6 +456,9 @@ export class ArtistsView
await this.libraryCtrl.getArtists();
this.artists = artists ?? [];
+
+ // Batch load artist images after artists are loaded.
+ void this.loadArtistImagesBatch();
} catch (error) {
console.error(
'Error loading artists:',
@@ -988,11 +993,6 @@ export class ArtistsView
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`
`;
}
+ private imagesBatchLoaded = false;
+
/**
- * Load artist image for a single artist. Resolves MBID by name,
- * then fetches the cached image. Sequential to avoid rate limit.
+ * Batch load all artist images in one Wails call.
+ * Only returns already-cached images (from the disk cache
+ * populated by the index build). Uncached artists fall back
+ * to the initial letter.
+ */
+ private async loadArtistImagesBatch() {
+ if (this.imagesBatchLoaded) return;
+
+ this.imagesBatchLoaded = true;
+
+ const artists = this.libraryCtrl.cachedArtists;
+
+ if (!artists || artists.length === 0) return;
+
+ const names = artists.map((a) => a.Name);
+
+ try {
+ const images = await GetArtistImages(names);
+
+ if (images && Object.keys(images).length > 0) {
+ for (const [name, url] of Object.entries(images)) {
+ if (url) {
+ this.artistImageCache.set(name, url);
+ }
+ }
+
+ this.requestUpdate();
+ }
+ } catch {
+ // Non-critical.
+ }
+ }
+
+ /**
+ * Load artist image for a single artist on-demand (fallback
+ * for artists not resolved by the batch call).
*/
private loadArtistImage(name: string) {
if (this.artistImageCache.has(name) || this.artistImageLoading.has(name)) {
diff --git a/frontend/wailsjs/go/explore/Service.d.ts b/frontend/wailsjs/go/explore/Service.d.ts
index 1714cd0..238d7e2 100755
--- a/frontend/wailsjs/go/explore/Service.d.ts
+++ b/frontend/wailsjs/go/explore/Service.d.ts
@@ -7,16 +7,22 @@ export function BrowseReleaseGroups(arg1:string):Promise>;
+export function CheckLibraryMBIDs(arg1:Array):Promise>;
+
export function CoverArtGroupURL(arg1:string):Promise;
export function CoverArtURL(arg1:string):Promise;
export function GetArtistImageURL(arg1:string):Promise;
+export function GetArtistMBID(arg1:string):Promise;
+
export function GetThumbnail(arg1:string,arg2:string,arg3:string):Promise;
export function GetThumbnails(arg1:Array):Promise>;
+export function InvalidateIndexDiscographies():Promise;
+
export function LookupArtist(arg1:string):Promise;
export function LookupReleaseGroup(arg1:string):Promise;
@@ -33,8 +39,10 @@ export function SetContext(arg1:context.Context):Promise;
export function SimilarArtists(arg1:string):Promise>;
+export function StartIndexBuild():Promise;
+
+export function StopIndexBuild():Promise;
+
export function TopRecordingsForArtist(arg1:string):Promise>;
-export function CheckLibraryMBIDs(arg1:string[]):Promise>;
-
-export function GetArtistMBID(arg1:string):Promise;
+export function GetArtistImages(arg1:string[]):Promise>;
diff --git a/frontend/wailsjs/go/explore/Service.js b/frontend/wailsjs/go/explore/Service.js
index dee8239..5905016 100755
--- a/frontend/wailsjs/go/explore/Service.js
+++ b/frontend/wailsjs/go/explore/Service.js
@@ -10,6 +10,10 @@ export function BrowseReleases(arg1) {
return window['go']['explore']['Service']['BrowseReleases'](arg1);
}
+export function CheckLibraryMBIDs(arg1) {
+ return window['go']['explore']['Service']['CheckLibraryMBIDs'](arg1);
+}
+
export function CoverArtGroupURL(arg1) {
return window['go']['explore']['Service']['CoverArtGroupURL'](arg1);
}
@@ -22,6 +26,10 @@ export function GetArtistImageURL(arg1) {
return window['go']['explore']['Service']['GetArtistImageURL'](arg1);
}
+export function GetArtistMBID(arg1) {
+ return window['go']['explore']['Service']['GetArtistMBID'](arg1);
+}
+
export function GetThumbnail(arg1, arg2, arg3) {
return window['go']['explore']['Service']['GetThumbnail'](arg1, arg2, arg3);
}
@@ -30,6 +38,10 @@ export function GetThumbnails(arg1) {
return window['go']['explore']['Service']['GetThumbnails'](arg1);
}
+export function InvalidateIndexDiscographies() {
+ return window['go']['explore']['Service']['InvalidateIndexDiscographies']();
+}
+
export function LookupArtist(arg1) {
return window['go']['explore']['Service']['LookupArtist'](arg1);
}
@@ -62,14 +74,18 @@ export function SimilarArtists(arg1) {
return window['go']['explore']['Service']['SimilarArtists'](arg1);
}
+export function StartIndexBuild() {
+ return window['go']['explore']['Service']['StartIndexBuild']();
+}
+
+export function StopIndexBuild() {
+ return window['go']['explore']['Service']['StopIndexBuild']();
+}
+
export function TopRecordingsForArtist(arg1) {
return window['go']['explore']['Service']['TopRecordingsForArtist'](arg1);
}
-export function CheckLibraryMBIDs(arg1) {
- return window['go']['explore']['Service']['CheckLibraryMBIDs'](arg1);
-}
-
-export function GetArtistMBID(arg1) {
- return window['go']['explore']['Service']['GetArtistMBID'](arg1);
+export function GetArtistImages(arg1) {
+ return window['go']['explore']['Service']['GetArtistImages'](arg1);
}