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); }