feat: local-first data pipeline — check library before API calls

Audit of all external calls across explore components, with local
sources checked first:

1. loadThumbnails: seeds thumbnailCache from library album cover
   art (CoverArtMedium/Small by MBID) before building the API
   request list. Library albums show cover art instantly; only
   non-library albums hit the GetThumbnails API.

2. loadArtistImages: seeds artistImageCache from library store
   (ImageMedium/Small by MBID) before the sequential API loop.
   Library artists show images instantly; only non-library artists
   hit GetArtistImageURL.

3. checkLibrary (explore-view): checks library store MBIDs
   frontend-side for artists and albums. Only falls back to
   CheckLibraryMBIDs API for recordings (not in library store).

4. checkLibrary (artist-details): same frontend-first approach
   using library album MBIDs.

5. hydrateFromCache (artist-details): now also checks library
   store directly for artist images when explore cache is empty
   (handles direct navigation without prior search).
This commit is contained in:
2026-03-30 16:18:22 -04:00
parent 0f432dc7ba
commit c530dc6cb8
2 changed files with 127 additions and 14 deletions
@@ -773,15 +773,26 @@ export class ExploreArtistDetails extends LitElement {
* Shows cached data instantly before API calls complete.
*/
private hydrateFromCache(mbid: string) {
// Artist image from explore cache (populated by search results).
const cachedArtist = exploreCache.getArtist(mbid);
if (cachedArtist) {
if (cachedArtist.imageURL) {
this.artistImageURL = cachedArtist.imageURL;
} else if (cachedArtist.imageMedium) {
this.artistImageURL = cachedArtist.imageMedium;
} else if (cachedArtist.imageSmall) {
this.artistImageURL = cachedArtist.imageSmall;
// Artist image: check explore cache first, then library store.
if (!this.artistImageURL) {
const cachedArtist = exploreCache.getArtist(mbid);
if (cachedArtist) {
this.artistImageURL = cachedArtist.imageURL
|| cachedArtist.imageMedium
|| cachedArtist.imageSmall
|| '';
}
}
if (!this.artistImageURL) {
const cachedArtists = libraryStore.cachedArtists;
if (cachedArtists) {
for (const a of cachedArtists) {
if (a.MBID === mbid) {
this.artistImageURL = a.ImageMedium || a.ImageSmall || '';
break;
}
}
}
}
@@ -930,6 +941,30 @@ export class ExploreArtistDetails extends LitElement {
}
private async checkLibrary() {
// Check frontend-side first.
const cachedAlbums = libraryStore.cachedAlbums;
if (cachedAlbums) {
const localMBIDs = new Set<string>();
for (const a of cachedAlbums) {
if (a.MBID) localMBIDs.add(a.MBID);
}
let updated = false;
for (const rg of this.releaseGroups) {
if (rg.mbid && localMBIDs.has(rg.mbid)) {
this.libraryMBIDs.add(rg.mbid);
updated = true;
}
}
if (updated) {
this.requestUpdate();
return;
}
}
// Fallback to backend.
const mbids: string[] = [];
for (const rg of this.releaseGroups) {
@@ -858,7 +858,27 @@ export class ExploreView extends LitElement {
return;
}
// Collect MBIDs that need fetching.
// Seed thumbnails from library album cover art (instant, no API).
const cachedAlbums = libraryStore.cachedAlbums;
if (cachedAlbums) {
const libAlbumsByMBID = new Map<string, string>();
for (const a of cachedAlbums) {
if (a.MBID && (a.CoverArtMedium || a.CoverArtSmall)) {
libAlbumsByMBID.set(a.MBID, a.CoverArtMedium || a.CoverArtSmall);
}
}
for (const rg of this.results.releaseGroups) {
if (!this.thumbnailCache.has(rg.mbid)) {
const localArt = libAlbumsByMBID.get(rg.mbid) || (rg as any)._coverArt;
if (localArt) {
this.thumbnailCache.set(rg.mbid, localArt);
}
}
}
}
// Collect MBIDs that still need fetching from the API.
const requests: ThumbnailRequest[] = [];
for (const rg of this.results.releaseGroups) {
@@ -917,7 +937,29 @@ export class ExploreView extends LitElement {
private async loadArtistImages() {
if (!this.results?.artists?.length) return;
// Load sequentially to avoid hammering the MB rate limiter.
// Seed from library store first (instant, no API).
const cachedArtists = libraryStore.cachedArtists;
if (cachedArtists) {
const libByMBID = new Map<string, string>();
for (const a of cachedArtists) {
if (a.MBID && (a.ImageMedium || a.ImageSmall)) {
libByMBID.set(a.MBID, a.ImageMedium || a.ImageSmall);
}
}
for (const a of this.results.artists) {
if (!this.artistImageCache.has(a.mbid) && a.mbid) {
const local = libByMBID.get(a.mbid) || (a as any)._imageMedium || (a as any)._imageSmall;
if (local) {
this.artistImageCache.set(a.mbid, local);
}
}
}
this.requestUpdate();
}
// Fetch remaining from API (only artists not yet resolved).
for (const a of this.results.artists) {
if (this.artistImageCache.has(a.mbid)) continue;
@@ -942,14 +984,50 @@ export class ExploreView extends LitElement {
private async checkLibrary() {
if (!this.results) return;
const mbids: string[] = [];
// Check frontend-side first using library store MBIDs.
const cachedArtists = libraryStore.cachedArtists;
const cachedAlbums = libraryStore.cachedAlbums;
const localMBIDs = new Set<string>();
if (cachedArtists) {
for (const a of cachedArtists) {
if (a.MBID) localMBIDs.add(a.MBID);
}
}
if (cachedAlbums) {
for (const a of cachedAlbums) {
if (a.MBID) localMBIDs.add(a.MBID);
}
}
let updated = false;
for (const a of this.results.artists ?? []) {
if (a.mbid) mbids.push(a.mbid);
if (a.mbid && localMBIDs.has(a.mbid)) {
this.libraryMBIDs.add(a.mbid);
updated = true;
}
}
for (const rg of this.results.releaseGroups ?? []) {
if (rg.mbid) mbids.push(rg.mbid);
if (rg.mbid && localMBIDs.has(rg.mbid)) {
this.libraryMBIDs.add(rg.mbid);
updated = true;
}
}
if (updated) {
this.requestUpdate();
return;
}
// Fallback to backend check for recordings and edge cases
// (recordings aren't in the library store cache).
const mbids: string[] = [];
for (const r of this.results.recordings ?? []) {
if (r.mbid) mbids.push(r.mbid);
}
if (mbids.length === 0) return;