feat: MBIDs in library models + local-first search + explore cache

Backend:
- Added mbid column to sqlc schemas for artists and release_groups
- Regenerated sqlc queries to SELECT mbid in artist/album queries
- Added MBID field to library.Artist and library.Album Go structs
- All GetAllArtists/GetAllAlbums variants now populate MBID

Frontend:
- Updated Wails models.ts with MBID fields on Artist and Album
- Added cachedArtists/cachedAlbums getters to LibraryStore
- searchLibraryCache now includes MBIDs and local cover art URLs
  so library results can navigate to explore detail pages
- Added mergeWithLibrary() — when full MB results arrive, library
  entries are enriched with local images and 'In Library' flags
  rather than being replaced by MB-only versions
- Created ExploreCache store for cross-page data sharing: search
  results populate the cache, detail pages can read from it to
  avoid redundant API calls for already-fetched data
This commit is contained in:
2026-03-29 18:54:22 -04:00
parent 1999fdb0f4
commit 8096b28d17
13 changed files with 255 additions and 41 deletions
@@ -10,6 +10,7 @@ import type {
MBRecording,
} from '@go/explore/Service';
import { libraryStore } from '../../store/library-store';
import { exploreCache } from '../../store/explore-cache';
import '@awesome.me/webawesome/dist/components/icon/icon.js';
/* ── Constants ── */
@@ -525,6 +526,10 @@ export class ExploreView extends LitElement {
const localResults = this.searchLibraryCache(query);
if (localResults && (localResults.artists?.length || localResults.releaseGroups?.length)) {
this.results = localResults;
exploreCache.populateFromSearch(
localResults.artists || [],
localResults.releaseGroups || [],
);
this.loadThumbnails();
this.loadArtistImages();
const elapsed = (performance.now() - startTime).toFixed(0);
@@ -541,7 +546,8 @@ export class ExploreView extends LitElement {
/**
* Search the frontend library cache for matching artists and albums.
* Pure JS — no Go calls, guaranteed instant.
* Pure JS — no Go calls, guaranteed instant. Returns results with
* MBIDs and local cover art so they can navigate to explore pages.
*/
private searchLibraryCache(query: string): MBSearchResult | null {
const q = query.toLowerCase();
@@ -552,14 +558,17 @@ export class ExploreView extends LitElement {
for (const a of cachedArtists) {
if (a.Name.toLowerCase().includes(q)) {
artists.push({
mbid: '',
mbid: a.MBID || '',
name: a.Name,
sortName: '',
type: 'Group',
type: '',
country: '',
disambiguation: '',
score: 100,
} as MBArtist);
_imageSmall: a.ImageSmall || '',
_imageMedium: a.ImageMedium || '',
_inLibrary: true,
} as MBArtist & { _imageSmall: string; _imageMedium: string; _inLibrary: boolean });
if (artists.length >= 5) break;
}
}
@@ -571,12 +580,14 @@ export class ExploreView extends LitElement {
for (const a of cachedAlbums) {
if (a.Name.toLowerCase().includes(q) || a.ArtistName.toLowerCase().includes(q)) {
releaseGroups.push({
mbid: '',
mbid: a.MBID || '',
title: a.Name,
primaryType: 'Album',
artistCredit: a.ArtistName,
firstReleaseDate: a.Year ? String(a.Year) : '',
} as MBReleaseGroup);
_coverArt: a.CoverArtMedium || a.CoverArtSmall || '',
_inLibrary: true,
} as MBReleaseGroup & { _coverArt: string; _inLibrary: boolean });
if (releaseGroups.length >= 5) break;
}
}
@@ -589,6 +600,61 @@ export class ExploreView extends LitElement {
return { artists, releaseGroups, recordings: [] } as MBSearchResult;
}
/**
* Merge full search results with library data: library entries
* take priority (local art, "In Library" badge). MB-only results
* are appended after library matches.
*/
private mergeWithLibrary(result: MBSearchResult): MBSearchResult {
const cachedArtists = libraryStore.cachedArtists;
const cachedAlbums = libraryStore.cachedAlbums;
// Build MBID→library lookups.
const libArtistsByMBID = new Map<string, typeof cachedArtists extends (infer T)[] | null ? T : never>();
const libArtistsByName = new Map<string, typeof cachedArtists extends (infer T)[] | null ? T : never>();
if (cachedArtists) {
for (const a of cachedArtists) {
if (a.MBID) libArtistsByMBID.set(a.MBID, a);
libArtistsByName.set(a.Name.toLowerCase(), a);
}
}
const libAlbumsByMBID = new Map<string, typeof cachedAlbums extends (infer T)[] | null ? T : never>();
if (cachedAlbums) {
for (const a of cachedAlbums) {
if (a.MBID) libAlbumsByMBID.set(a.MBID, a);
}
}
// Enrich artists: if MB result matches a library artist, add local images.
if (result.artists) {
for (let i = 0; i < result.artists.length; i++) {
const a = result.artists[i];
const lib = (a.mbid && libArtistsByMBID.get(a.mbid)) ||
libArtistsByName.get(a.name.toLowerCase());
if (lib) {
(a as any)._imageSmall = lib.ImageSmall || '';
(a as any)._imageMedium = lib.ImageMedium || '';
(a as any)._inLibrary = true;
}
}
}
// Enrich release groups: if MB result matches a library album, use local art.
if (result.releaseGroups) {
for (let i = 0; i < result.releaseGroups.length; i++) {
const rg = result.releaseGroups[i];
const lib = rg.mbid ? libAlbumsByMBID.get(rg.mbid) : undefined;
if (lib) {
(rg as any)._coverArt = lib.CoverArtMedium || lib.CoverArtSmall || '';
(rg as any)._inLibrary = true;
}
}
}
return result;
}
private async executeFullSearch(version: number, query: string, startTime: number) {
try {
const result = await Search(query);
@@ -601,7 +667,11 @@ export class ExploreView extends LitElement {
return;
}
this.results = result;
this.results = this.mergeWithLibrary(result);
exploreCache.populateFromSearch(
this.results.artists || [],
this.results.releaseGroups || [],
);
this.loadThumbnails();
this.loadArtistImages();
this.checkLibrary();