diff --git a/backend/explore/explore.go b/backend/explore/explore.go index 46d6789..2a52a0c 100644 --- a/backend/explore/explore.go +++ b/backend/explore/explore.go @@ -163,6 +163,28 @@ func (e *Service) GetThumbnail(releaseGroupMBID, albumName, artistName string) s return e.artProxy.GetThumbnail(releaseGroupMBID, albumName, artistName) } +// ThumbnailRequest is a single item in a batch thumbnail request. +type ThumbnailRequest struct { + MBID string `json:"mbid"` + AlbumName string `json:"albumName"` + ArtistName string `json:"artistName"` +} + +// GetThumbnails fetches multiple thumbnails in one call and returns +// a map of MBID → base64 data URL. Entries with no art are omitted. +func (e *Service) GetThumbnails(requests []ThumbnailRequest) map[string]string { + result := make(map[string]string, len(requests)) + + for _, req := range requests { + dataURL := e.artProxy.GetThumbnail(req.MBID, req.AlbumName, req.ArtistName) + if dataURL != "" { + result[req.MBID] = dataURL + } + } + + 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/explore-view/explore-view.ts b/frontend/src/components/explore-view/explore-view.ts index 1fe7486..35f0aa2 100644 --- a/frontend/src/components/explore-view/explore-view.ts +++ b/frontend/src/components/explore-view/explore-view.ts @@ -1,7 +1,8 @@ import { LitElement, html, css, nothing } from 'lit'; import { customElement, state, query as litQuery } from 'lit/decorators.js'; import { designTokens } from '../../styles/tokens.css'; -import { Search, GetThumbnail } from '@go/explore/Service'; +import { Search, GetThumbnails } from '@go/explore/Service'; +import type { ThumbnailRequest } from '@go/explore/Service'; import type { MBSearchResult, MBArtist, @@ -566,6 +567,8 @@ export class ExploreView extends LitElement { } this.results = result; + this.loadThumbnails(); + const elapsed = (performance.now() - startTime).toFixed(0); console.log( `[explore] search completed: "${query}" in ${elapsed}ms — ` + @@ -587,21 +590,67 @@ export class ExploreView extends LitElement { /* ── Thumbnail Loading ── */ - private loadThumbnail(mbid: string, albumName: string, artistName: string) { - // Don't re-fetch if already loading or cached. - if (this.thumbnailCache.has(mbid)) return; + private thumbnailBatchPending = false; - // Mark as loading to prevent duplicate requests. - this.thumbnailCache.set(mbid, ''); + /** + * Load thumbnails for all visible album cards in one batched + * Wails call. Called after search results are set. + */ + private loadThumbnails() { + if (this.thumbnailBatchPending || !this.results?.releaseGroups?.length) { + return; + } - GetThumbnail(mbid, albumName || '', artistName || '').then((dataUrl) => { - if (dataUrl) { - this.thumbnailCache.set(mbid, dataUrl); - this.requestUpdate(); + // Collect MBIDs that need fetching. + const requests: ThumbnailRequest[] = []; + + for (const rg of this.results.releaseGroups) { + if (!this.thumbnailCache.has(rg.mbid)) { + requests.push({ + mbid: rg.mbid, + albumName: rg.title || '', + artistName: rg.artistCredit || '', + }); } - }).catch(() => { - // Leave empty string in cache — fallback will show. - }); + } + + if (requests.length === 0) return; + + this.thumbnailBatchPending = true; + + GetThumbnails(requests) + .then((results) => { + let updated = false; + + for (const [mbid, dataUrl] of Object.entries(results)) { + if (dataUrl) { + this.thumbnailCache.set(mbid, dataUrl); + updated = true; + } + } + + // Mark MBIDs with no art so we don't re-request. + for (const req of requests) { + if (!this.thumbnailCache.has(req.mbid)) { + this.thumbnailCache.set(req.mbid, ''); + } + } + + if (updated) { + this.requestUpdate(); + } + }) + .catch(() => { + // Batch failed — mark all as attempted. + for (const req of requests) { + if (!this.thumbnailCache.has(req.mbid)) { + this.thumbnailCache.set(req.mbid, ''); + } + } + }) + .finally(() => { + this.thumbnailBatchPending = false; + }); } /* ── Top Results ── */ @@ -892,11 +941,6 @@ export class ExploreView extends LitElement { const artURL = cachedArt || CoverArtGroupURL(rg.mbid); const year = extractYear(rg.firstReleaseDate); - // Kick off async thumbnail fetch if not cached. - if (!cachedArt) { - this.loadThumbnail(rg.mbid, rg.title, rg.artistCredit); - } - return html`
; export function GetThumbnail(arg1:string, arg2:string, arg3:string):Promise; +export interface ThumbnailRequest { + mbid: string; + albumName: string; + artistName: string; +} + +export function GetThumbnails(arg1:ThumbnailRequest[]):Promise>; + export function LookupArtist(arg1:string):Promise; export function LookupReleaseGroup(arg1:string):Promise; diff --git a/frontend/wailsjs/go/explore/Service.js b/frontend/wailsjs/go/explore/Service.js index 6ac459f..671f5be 100755 --- a/frontend/wailsjs/go/explore/Service.js +++ b/frontend/wailsjs/go/explore/Service.js @@ -22,6 +22,10 @@ export function GetThumbnail(arg1, arg2, arg3) { return window['go']['explore']['Service']['GetThumbnail'](arg1, arg2, arg3); } +export function GetThumbnails(arg1) { + return window['go']['explore']['Service']['GetThumbnails'](arg1); +} + export function LookupArtist(arg1) { return window['go']['explore']['Service']['LookupArtist'](arg1); }