perf: batch artist image loading for library grid view

Replace per-artist sequential GetArtistMBID + GetArtistImageURL
calls (2 Wails round-trips × N artists) with a single batch
GetArtistImages(names[]) call that:

1. Resolves all names → MBIDs via AllArtistMBIDs() (one DB query)
2. Checks disk cache for each MBID via GetCachedImage (no network)
3. Returns map[name]→dataURL in one Wails bridge round-trip

Only returns already-cached images from the disk cache populated
by the index build. No network fetches triggered — artists whose
images haven't been cached yet keep the initial letter fallback
until the index build resolves them in the background.

Result: all cached artist images appear simultaneously on first
render instead of loading one-by-one over several seconds.
This commit is contained in:
2026-03-28 15:22:22 -04:00
parent df1d1785e9
commit 7ec9785463
5 changed files with 113 additions and 17 deletions
+10
View File
@@ -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.
+26
View File
@@ -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
@@ -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`<img
class="avatar-image"
@@ -1006,9 +1006,45 @@ export class ArtistsView
</span>`;
}
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)) {
+11 -3
View File
@@ -7,16 +7,22 @@ export function BrowseReleaseGroups(arg1:string):Promise<Array<explore.MBRelease
export function BrowseReleases(arg1:string):Promise<Array<explore.MBRelease>>;
export function CheckLibraryMBIDs(arg1:Array<string>):Promise<Record<string, string>>;
export function CoverArtGroupURL(arg1:string):Promise<string>;
export function CoverArtURL(arg1:string):Promise<string>;
export function GetArtistImageURL(arg1:string):Promise<string>;
export function GetArtistMBID(arg1:string):Promise<string>;
export function GetThumbnail(arg1:string,arg2:string,arg3:string):Promise<string>;
export function GetThumbnails(arg1:Array<explore.ThumbnailRequest>):Promise<Record<string, string>>;
export function InvalidateIndexDiscographies():Promise<void>;
export function LookupArtist(arg1:string):Promise<explore.MBArtist>;
export function LookupReleaseGroup(arg1:string):Promise<explore.MBReleaseGroup>;
@@ -33,8 +39,10 @@ export function SetContext(arg1:context.Context):Promise<void>;
export function SimilarArtists(arg1:string):Promise<Array<explore.LBSimilarArtist>>;
export function StartIndexBuild():Promise<void>;
export function StopIndexBuild():Promise<void>;
export function TopRecordingsForArtist(arg1:string):Promise<Array<explore.LBTopRecording>>;
export function CheckLibraryMBIDs(arg1:string[]):Promise<Record<string, string>>;
export function GetArtistMBID(arg1:string):Promise<string>;
export function GetArtistImages(arg1:string[]):Promise<Record<string, string>>;
+22 -6
View File
@@ -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);
}