feat: use local library cover art for search results

CoverArtProxy now checks three sources in order:
1. Local library (instant) — matches by album+artist name against
   the release_groups/cover_art tables. Albums the user already
   owns show their local cover art immediately.
2. Disk cache (instant) — previously fetched CAA thumbnails.
3. Cover Art Archive (network) — fetches and caches to disk.

Library index is built once on first access (sync.Once) from a
single SQL query joining release_groups → cover_art → artists.
Keyed by lowercased 'album\x00artist' for exact name matching.

GetThumbnail now takes (mbid, albumName, artistName) so the proxy
can check the library before falling back to CAA. Frontend passes
the album title and artist credit from the search result.
This commit is contained in:
2026-03-25 19:00:29 -04:00
parent d5f34f242f
commit 0761cff408
5 changed files with 113 additions and 34 deletions
@@ -587,14 +587,14 @@ export class ExploreView extends LitElement {
/* ── Thumbnail Loading ── */
private loadThumbnail(mbid: string) {
private loadThumbnail(mbid: string, albumName: string, artistName: string) {
// Don't re-fetch if already loading or cached.
if (this.thumbnailCache.has(mbid)) return;
// Mark as loading to prevent duplicate requests.
this.thumbnailCache.set(mbid, '');
GetThumbnail(mbid).then((dataUrl) => {
GetThumbnail(mbid, albumName || '', artistName || '').then((dataUrl) => {
if (dataUrl) {
this.thumbnailCache.set(mbid, dataUrl);
this.requestUpdate();
@@ -894,7 +894,7 @@ export class ExploreView extends LitElement {
// Kick off async thumbnail fetch if not cached.
if (!cachedArt) {
this.loadThumbnail(rg.mbid);
this.loadThumbnail(rg.mbid, rg.title, rg.artistCredit);
}
return html`