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:
@@ -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();
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* ExploreCache — a simple in-memory cache for explore data that
|
||||
* persists across page navigations within a session. Populated by
|
||||
* search results and consumed by detail pages to avoid redundant
|
||||
* API calls.
|
||||
*
|
||||
* Data flows:
|
||||
* search results → cache artist images, album art, release groups
|
||||
* artist detail page → check cache before API calls
|
||||
* album detail page → check cache before API calls
|
||||
*/
|
||||
|
||||
import type { MBReleaseGroup, LBTopRecording } from '@go/explore/Service';
|
||||
|
||||
/** Cached artist data from search results. */
|
||||
export interface CachedArtist {
|
||||
mbid: string;
|
||||
name: string;
|
||||
imageURL?: string; // resolved artist image
|
||||
imageSmall?: string; // library small image
|
||||
imageMedium?: string; // library medium image
|
||||
}
|
||||
|
||||
/** Cached album data from search results. */
|
||||
export interface CachedAlbum {
|
||||
mbid: string;
|
||||
title: string;
|
||||
artistName: string;
|
||||
coverArt?: string; // local cover art URL
|
||||
year?: string;
|
||||
}
|
||||
|
||||
class ExploreCacheStore {
|
||||
private artists = new Map<string, CachedArtist>();
|
||||
private albums = new Map<string, CachedAlbum>();
|
||||
private artistAlbums = new Map<string, MBReleaseGroup[]>();
|
||||
private artistTopTracks = new Map<string, LBTopRecording[]>();
|
||||
|
||||
// -- Artists --
|
||||
|
||||
setArtist(mbid: string, data: CachedArtist) {
|
||||
if (mbid) this.artists.set(mbid, data);
|
||||
}
|
||||
|
||||
getArtist(mbid: string): CachedArtist | undefined {
|
||||
return this.artists.get(mbid);
|
||||
}
|
||||
|
||||
// -- Albums --
|
||||
|
||||
setAlbum(mbid: string, data: CachedAlbum) {
|
||||
if (mbid) this.albums.set(mbid, data);
|
||||
}
|
||||
|
||||
getAlbum(mbid: string): CachedAlbum | undefined {
|
||||
return this.albums.get(mbid);
|
||||
}
|
||||
|
||||
// -- Artist → Albums (release groups) --
|
||||
|
||||
setArtistAlbums(artistMBID: string, albums: MBReleaseGroup[]) {
|
||||
if (artistMBID) this.artistAlbums.set(artistMBID, albums);
|
||||
}
|
||||
|
||||
getArtistAlbums(artistMBID: string): MBReleaseGroup[] | undefined {
|
||||
return this.artistAlbums.get(artistMBID);
|
||||
}
|
||||
|
||||
// -- Artist → Top tracks --
|
||||
|
||||
setArtistTopTracks(artistMBID: string, tracks: LBTopRecording[]) {
|
||||
if (artistMBID) this.artistTopTracks.set(artistMBID, tracks);
|
||||
}
|
||||
|
||||
getArtistTopTracks(artistMBID: string): LBTopRecording[] | undefined {
|
||||
return this.artistTopTracks.get(artistMBID);
|
||||
}
|
||||
|
||||
// -- Bulk populate from search results --
|
||||
|
||||
populateFromSearch(artists: any[], releaseGroups: any[]) {
|
||||
for (const a of artists) {
|
||||
if (a.mbid) {
|
||||
this.setArtist(a.mbid, {
|
||||
mbid: a.mbid,
|
||||
name: a.name,
|
||||
imageSmall: a._imageSmall,
|
||||
imageMedium: a._imageMedium,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const rg of releaseGroups) {
|
||||
if (rg.mbid) {
|
||||
this.setAlbum(rg.mbid, {
|
||||
mbid: rg.mbid,
|
||||
title: rg.title,
|
||||
artistName: rg.artistCredit || '',
|
||||
coverArt: rg._coverArt,
|
||||
year: rg.firstReleaseDate,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const exploreCache = new ExploreCacheStore();
|
||||
-3
@@ -1,7 +1,6 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {explore} from '../models';
|
||||
import {http} from '../models';
|
||||
import {context} from '../models';
|
||||
|
||||
export function BrowseReleaseGroups(arg1:string):Promise<Array<explore.MBReleaseGroup>>;
|
||||
@@ -38,8 +37,6 @@ export function SearchArtists(arg1:string):Promise<Array<explore.MBArtist>>;
|
||||
|
||||
export function SearchLocal(arg1:string):Promise<explore.MBSearchResult>;
|
||||
|
||||
export function SearchLocalHandler():Promise<http.Handler>;
|
||||
|
||||
export function SearchRecordings(arg1:string):Promise<Array<explore.MBRecording>>;
|
||||
|
||||
export function SearchReleaseGroups(arg1:string):Promise<Array<explore.MBReleaseGroup>>;
|
||||
|
||||
@@ -70,10 +70,6 @@ export function SearchLocal(arg1) {
|
||||
return window['go']['explore']['Service']['SearchLocal'](arg1);
|
||||
}
|
||||
|
||||
export function SearchLocalHandler() {
|
||||
return window['go']['explore']['Service']['SearchLocalHandler']();
|
||||
}
|
||||
|
||||
export function SearchRecordings(arg1) {
|
||||
return window['go']['explore']['Service']['SearchRecordings'](arg1);
|
||||
}
|
||||
|
||||
@@ -242,6 +242,7 @@ export namespace library {
|
||||
ID: number;
|
||||
Name: string;
|
||||
ArtistName: string;
|
||||
MBID: string;
|
||||
CoverArtPath: string;
|
||||
CoverArtSmall: string;
|
||||
CoverArtMedium: string;
|
||||
@@ -257,6 +258,7 @@ export namespace library {
|
||||
this.ID = source["ID"];
|
||||
this.Name = source["Name"];
|
||||
this.ArtistName = source["ArtistName"];
|
||||
this.MBID = source["MBID"];
|
||||
this.CoverArtPath = source["CoverArtPath"];
|
||||
this.CoverArtSmall = source["CoverArtSmall"];
|
||||
this.CoverArtMedium = source["CoverArtMedium"];
|
||||
@@ -267,6 +269,7 @@ export namespace library {
|
||||
export class Artist {
|
||||
ID: number;
|
||||
Name: string;
|
||||
MBID: string;
|
||||
ImageSmall: string;
|
||||
ImageMedium: string;
|
||||
ImageLarge: string;
|
||||
@@ -279,6 +282,7 @@ export namespace library {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.ID = source["ID"];
|
||||
this.Name = source["Name"];
|
||||
this.MBID = source["MBID"];
|
||||
this.ImageSmall = source["ImageSmall"];
|
||||
this.ImageMedium = source["ImageMedium"];
|
||||
this.ImageLarge = source["ImageLarge"];
|
||||
|
||||
Reference in New Issue
Block a user