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:
@@ -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.
|
// GetArtistImage returns a base64 data URL for the artist's photo.
|
||||||
// Checks disk cache first, then resolves via MB/Wikidata and fetches
|
// Checks disk cache first, then resolves via MB/Wikidata and fetches
|
||||||
// the image from Wikimedia Commons. Returns "" if no image.
|
// the image from Wikimedia Commons. Returns "" if no image.
|
||||||
|
|||||||
@@ -233,6 +233,32 @@ func (e *Service) GetArtistMBID(artistName string) string {
|
|||||||
return e.libMBID.GetArtistMBID(artistName)
|
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
|
// Search concurrently queries MusicBrainz for artists, release
|
||||||
// groups, and recordings matching the query, then boosts results
|
// groups, and recordings matching the query, then boosts results
|
||||||
// using ListenBrainz popularity data. The final score blends
|
// using ListenBrainz popularity data. The final score blends
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
GetAlbumsByArtistByLibrary,
|
GetAlbumsByArtistByLibrary,
|
||||||
GetAlbumTracksByLibrary,
|
GetAlbumTracksByLibrary,
|
||||||
} from '@go/library/Library';
|
} 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 { library } from '@go/models';
|
||||||
import { LibraryController } from '@store/controllers/library-controller';
|
import { LibraryController } from '@store/controllers/library-controller';
|
||||||
import { SearchController } from '@store/controllers/search-controller';
|
import { SearchController } from '@store/controllers/search-controller';
|
||||||
@@ -439,6 +439,8 @@ export class ArtistsView
|
|||||||
) {
|
) {
|
||||||
this.lastArtistsRef = cached;
|
this.lastArtistsRef = cached;
|
||||||
this.loadArtists();
|
this.loadArtists();
|
||||||
|
this.imagesBatchLoaded = false;
|
||||||
|
void this.loadArtistImagesBatch();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,6 +456,9 @@ export class ArtistsView
|
|||||||
await this.libraryCtrl.getArtists();
|
await this.libraryCtrl.getArtists();
|
||||||
|
|
||||||
this.artists = artists ?? [];
|
this.artists = artists ?? [];
|
||||||
|
|
||||||
|
// Batch load artist images after artists are loaded.
|
||||||
|
void this.loadArtistImagesBatch();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(
|
||||||
'Error loading artists:',
|
'Error loading artists:',
|
||||||
@@ -988,11 +993,6 @@ export class ArtistsView
|
|||||||
private renderArtistAvatar(name: string) {
|
private renderArtistAvatar(name: string) {
|
||||||
const imageURL = this.artistImageCache.get(name);
|
const imageURL = this.artistImageCache.get(name);
|
||||||
|
|
||||||
// Kick off async image load if not cached.
|
|
||||||
if (!this.artistImageCache.has(name)) {
|
|
||||||
this.loadArtistImage(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imageURL) {
|
if (imageURL) {
|
||||||
return html`<img
|
return html`<img
|
||||||
class="avatar-image"
|
class="avatar-image"
|
||||||
@@ -1006,9 +1006,45 @@ export class ArtistsView
|
|||||||
</span>`;
|
</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private imagesBatchLoaded = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load artist image for a single artist. Resolves MBID by name,
|
* Batch load all artist images in one Wails call.
|
||||||
* then fetches the cached image. Sequential to avoid rate limit.
|
* 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) {
|
private loadArtistImage(name: string) {
|
||||||
if (this.artistImageCache.has(name) || this.artistImageLoading.has(name)) {
|
if (this.artistImageCache.has(name) || this.artistImageLoading.has(name)) {
|
||||||
|
|||||||
+11
-3
@@ -7,16 +7,22 @@ export function BrowseReleaseGroups(arg1:string):Promise<Array<explore.MBRelease
|
|||||||
|
|
||||||
export function BrowseReleases(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 CoverArtGroupURL(arg1:string):Promise<string>;
|
||||||
|
|
||||||
export function CoverArtURL(arg1:string):Promise<string>;
|
export function CoverArtURL(arg1:string):Promise<string>;
|
||||||
|
|
||||||
export function GetArtistImageURL(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 GetThumbnail(arg1:string,arg2:string,arg3:string):Promise<string>;
|
||||||
|
|
||||||
export function GetThumbnails(arg1:Array<explore.ThumbnailRequest>):Promise<Record<string, 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 LookupArtist(arg1:string):Promise<explore.MBArtist>;
|
||||||
|
|
||||||
export function LookupReleaseGroup(arg1:string):Promise<explore.MBReleaseGroup>;
|
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 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 TopRecordingsForArtist(arg1:string):Promise<Array<explore.LBTopRecording>>;
|
||||||
|
|
||||||
export function CheckLibraryMBIDs(arg1:string[]):Promise<Record<string, string>>;
|
export function GetArtistImages(arg1:string[]):Promise<Record<string, string>>;
|
||||||
|
|
||||||
export function GetArtistMBID(arg1:string):Promise<string>;
|
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ export function BrowseReleases(arg1) {
|
|||||||
return window['go']['explore']['Service']['BrowseReleases'](arg1);
|
return window['go']['explore']['Service']['BrowseReleases'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function CheckLibraryMBIDs(arg1) {
|
||||||
|
return window['go']['explore']['Service']['CheckLibraryMBIDs'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function CoverArtGroupURL(arg1) {
|
export function CoverArtGroupURL(arg1) {
|
||||||
return window['go']['explore']['Service']['CoverArtGroupURL'](arg1);
|
return window['go']['explore']['Service']['CoverArtGroupURL'](arg1);
|
||||||
}
|
}
|
||||||
@@ -22,6 +26,10 @@ export function GetArtistImageURL(arg1) {
|
|||||||
return window['go']['explore']['Service']['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) {
|
export function GetThumbnail(arg1, arg2, arg3) {
|
||||||
return window['go']['explore']['Service']['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);
|
return window['go']['explore']['Service']['GetThumbnails'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function InvalidateIndexDiscographies() {
|
||||||
|
return window['go']['explore']['Service']['InvalidateIndexDiscographies']();
|
||||||
|
}
|
||||||
|
|
||||||
export function LookupArtist(arg1) {
|
export function LookupArtist(arg1) {
|
||||||
return window['go']['explore']['Service']['LookupArtist'](arg1);
|
return window['go']['explore']['Service']['LookupArtist'](arg1);
|
||||||
}
|
}
|
||||||
@@ -62,14 +74,18 @@ export function SimilarArtists(arg1) {
|
|||||||
return window['go']['explore']['Service']['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) {
|
export function TopRecordingsForArtist(arg1) {
|
||||||
return window['go']['explore']['Service']['TopRecordingsForArtist'](arg1);
|
return window['go']['explore']['Service']['TopRecordingsForArtist'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CheckLibraryMBIDs(arg1) {
|
export function GetArtistImages(arg1) {
|
||||||
return window['go']['explore']['Service']['CheckLibraryMBIDs'](arg1);
|
return window['go']['explore']['Service']['GetArtistImages'](arg1);
|
||||||
}
|
|
||||||
|
|
||||||
export function GetArtistMBID(arg1) {
|
|
||||||
return window['go']['explore']['Service']['GetArtistMBID'](arg1);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user