feat: cover art proxy with disk cache for instant thumbnail loading
Add CoverArtProxy that fetches cover art from CAA, caches the image bytes on disk (~/.local/share/yellowjacket/cover-art-cache/), and returns base64 data URLs via the GetThumbnail Wails binding. First load: fetches from CAA (rate-limited), caches to disk. Subsequent loads: instant from disk cache, no network. 404s: cached as empty files to avoid re-fetching. Frontend explore-view loads thumbnails async via GetThumbnail() calls that fire during render. Cached thumbnails appear as data URLs directly in img src, bypassing the browser's HTTP stack. Uncached thumbnails fall back to the CAA URL while the proxy fetches in the background, then re-render with the cached version. Also stores caa_id and caa_release_mbid in the search index's extra_json for future direct Internet Archive URL construction.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
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 } from '@go/explore/Service';
|
||||
import { Search, GetThumbnail } from '@go/explore/Service';
|
||||
import type {
|
||||
MBSearchResult,
|
||||
MBArtist,
|
||||
@@ -74,6 +74,7 @@ export class ExploreView extends LitElement {
|
||||
/** Monotonic counter to discard stale responses. */
|
||||
private searchVersion = 0;
|
||||
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private thumbnailCache = new Map<string, string>();
|
||||
|
||||
@litQuery('input') private inputEl!: HTMLInputElement;
|
||||
|
||||
@@ -584,6 +585,25 @@ export class ExploreView extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Thumbnail Loading ── */
|
||||
|
||||
private loadThumbnail(mbid: 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) => {
|
||||
if (dataUrl) {
|
||||
this.thumbnailCache.set(mbid, dataUrl);
|
||||
this.requestUpdate();
|
||||
}
|
||||
}).catch(() => {
|
||||
// Leave empty string in cache — fallback will show.
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Top Results ── */
|
||||
|
||||
private getTopResults(): ScoredItem[] {
|
||||
@@ -868,8 +888,15 @@ export class ExploreView extends LitElement {
|
||||
<h3 class="section-header">Albums</h3>
|
||||
<div class="horizontal-row">
|
||||
${releaseGroups.map((rg) => {
|
||||
const artURL = CoverArtGroupURL(rg.mbid);
|
||||
const cachedArt = this.thumbnailCache.get(rg.mbid);
|
||||
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);
|
||||
}
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="album-card"
|
||||
|
||||
+24
-85
@@ -1,93 +1,32 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {explore} from '../models';
|
||||
import {context} from '../models';
|
||||
|
||||
// -- Explore types (inline, matching Go JSON tags) ------------------
|
||||
export function BrowseReleaseGroups(arg1:string):Promise<Array<explore.MBReleaseGroup>>;
|
||||
|
||||
export interface MBArtist {
|
||||
mbid: string;
|
||||
name: string;
|
||||
sortName: string;
|
||||
type: string;
|
||||
country: string;
|
||||
disambiguation: string;
|
||||
score: number;
|
||||
}
|
||||
export function BrowseReleases(arg1:string):Promise<Array<explore.MBRelease>>;
|
||||
|
||||
export interface MBReleaseGroup {
|
||||
mbid: string;
|
||||
title: string;
|
||||
primaryType: string;
|
||||
secondaryTypes?: string[];
|
||||
firstReleaseDate: string;
|
||||
artistCredit: string;
|
||||
}
|
||||
|
||||
export interface MBRecording {
|
||||
mbid: string;
|
||||
title: string;
|
||||
length: number;
|
||||
artistCredit: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
export interface MBRelease {
|
||||
mbid: string;
|
||||
title: string;
|
||||
date: string;
|
||||
country: string;
|
||||
status: string;
|
||||
tracks?: MBTrack[];
|
||||
}
|
||||
|
||||
export interface MBTrack {
|
||||
position: number;
|
||||
discNumber: number;
|
||||
title: string;
|
||||
length: number;
|
||||
mbid: string;
|
||||
}
|
||||
|
||||
export interface MBSearchResult {
|
||||
artists?: MBArtist[];
|
||||
releaseGroups?: MBReleaseGroup[];
|
||||
recordings?: MBRecording[];
|
||||
}
|
||||
|
||||
export interface LBTopRecording {
|
||||
recordingMbid: string;
|
||||
artistName: string;
|
||||
trackName: string;
|
||||
totalListenCount: number;
|
||||
}
|
||||
|
||||
export interface LBSimilarArtist {
|
||||
artistMbid: string;
|
||||
name: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
// -- Service methods ------------------------------------------------
|
||||
|
||||
export function Search(arg1:string):Promise<MBSearchResult>;
|
||||
|
||||
export function SearchArtists(arg1:string):Promise<Array<MBArtist>>;
|
||||
|
||||
export function SearchReleaseGroups(arg1:string):Promise<Array<MBReleaseGroup>>;
|
||||
|
||||
export function SearchRecordings(arg1:string):Promise<Array<MBRecording>>;
|
||||
|
||||
export function LookupArtist(arg1:string):Promise<MBArtist>;
|
||||
|
||||
export function LookupReleaseGroup(arg1:string):Promise<MBReleaseGroup>;
|
||||
|
||||
export function BrowseReleaseGroups(arg1:string):Promise<Array<MBReleaseGroup>>;
|
||||
|
||||
export function BrowseReleases(arg1:string):Promise<Array<MBRelease>>;
|
||||
|
||||
export function TopRecordingsForArtist(arg1:string):Promise<Array<LBTopRecording>>;
|
||||
|
||||
export function SimilarArtists(arg1:string):Promise<Array<LBSimilarArtist>>;
|
||||
export function CoverArtGroupURL(arg1:string):Promise<string>;
|
||||
|
||||
export function CoverArtURL(arg1:string):Promise<string>;
|
||||
|
||||
export function CoverArtGroupURL(arg1:string):Promise<string>;
|
||||
export function LookupArtist(arg1:string):Promise<explore.MBArtist>;
|
||||
|
||||
export function LookupReleaseGroup(arg1:string):Promise<explore.MBReleaseGroup>;
|
||||
|
||||
export function Search(arg1:string):Promise<explore.MBSearchResult>;
|
||||
|
||||
export function SearchArtists(arg1:string):Promise<Array<explore.MBArtist>>;
|
||||
|
||||
export function SearchRecordings(arg1:string):Promise<Array<explore.MBRecording>>;
|
||||
|
||||
export function SearchReleaseGroups(arg1:string):Promise<Array<explore.MBReleaseGroup>>;
|
||||
|
||||
export function SetContext(arg1:context.Context):Promise<void>;
|
||||
|
||||
export function SimilarArtists(arg1:string):Promise<Array<explore.LBSimilarArtist>>;
|
||||
|
||||
export function TopRecordingsForArtist(arg1:string):Promise<Array<explore.LBTopRecording>>;
|
||||
|
||||
export function GetThumbnail(arg1:string):Promise<string>;
|
||||
|
||||
Regular → Executable
+26
-18
@@ -2,20 +2,20 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function Search(arg1) {
|
||||
return window['go']['explore']['Service']['Search'](arg1);
|
||||
export function BrowseReleaseGroups(arg1) {
|
||||
return window['go']['explore']['Service']['BrowseReleaseGroups'](arg1);
|
||||
}
|
||||
|
||||
export function SearchArtists(arg1) {
|
||||
return window['go']['explore']['Service']['SearchArtists'](arg1);
|
||||
export function BrowseReleases(arg1) {
|
||||
return window['go']['explore']['Service']['BrowseReleases'](arg1);
|
||||
}
|
||||
|
||||
export function SearchReleaseGroups(arg1) {
|
||||
return window['go']['explore']['Service']['SearchReleaseGroups'](arg1);
|
||||
export function CoverArtGroupURL(arg1) {
|
||||
return window['go']['explore']['Service']['CoverArtGroupURL'](arg1);
|
||||
}
|
||||
|
||||
export function SearchRecordings(arg1) {
|
||||
return window['go']['explore']['Service']['SearchRecordings'](arg1);
|
||||
export function CoverArtURL(arg1) {
|
||||
return window['go']['explore']['Service']['CoverArtURL'](arg1);
|
||||
}
|
||||
|
||||
export function LookupArtist(arg1) {
|
||||
@@ -26,26 +26,34 @@ export function LookupReleaseGroup(arg1) {
|
||||
return window['go']['explore']['Service']['LookupReleaseGroup'](arg1);
|
||||
}
|
||||
|
||||
export function BrowseReleaseGroups(arg1) {
|
||||
return window['go']['explore']['Service']['BrowseReleaseGroups'](arg1);
|
||||
export function Search(arg1) {
|
||||
return window['go']['explore']['Service']['Search'](arg1);
|
||||
}
|
||||
|
||||
export function BrowseReleases(arg1) {
|
||||
return window['go']['explore']['Service']['BrowseReleases'](arg1);
|
||||
export function SearchArtists(arg1) {
|
||||
return window['go']['explore']['Service']['SearchArtists'](arg1);
|
||||
}
|
||||
|
||||
export function TopRecordingsForArtist(arg1) {
|
||||
return window['go']['explore']['Service']['TopRecordingsForArtist'](arg1);
|
||||
export function SearchRecordings(arg1) {
|
||||
return window['go']['explore']['Service']['SearchRecordings'](arg1);
|
||||
}
|
||||
|
||||
export function SearchReleaseGroups(arg1) {
|
||||
return window['go']['explore']['Service']['SearchReleaseGroups'](arg1);
|
||||
}
|
||||
|
||||
export function SetContext(arg1) {
|
||||
return window['go']['explore']['Service']['SetContext'](arg1);
|
||||
}
|
||||
|
||||
export function SimilarArtists(arg1) {
|
||||
return window['go']['explore']['Service']['SimilarArtists'](arg1);
|
||||
}
|
||||
|
||||
export function CoverArtURL(arg1) {
|
||||
return window['go']['explore']['Service']['CoverArtURL'](arg1);
|
||||
export function TopRecordingsForArtist(arg1) {
|
||||
return window['go']['explore']['Service']['TopRecordingsForArtist'](arg1);
|
||||
}
|
||||
|
||||
export function CoverArtGroupURL(arg1) {
|
||||
return window['go']['explore']['Service']['CoverArtGroupURL'](arg1);
|
||||
export function GetThumbnail(arg1) {
|
||||
return window['go']['explore']['Service']['GetThumbnail'](arg1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user