mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
c3d37546cf
* feat(covers): race sibling downscale vs fetch, search thumb priorities Run getCoverArt and client downscale in parallel when another size of the same cover is cached; first successful result wins and aborts the other path. Await both branches so inflight bookkeeping does not detach early. Extend the cover cache size roster so provisional siblings resolve for sizes used in the UI (e.g. 400/600/800, 48/96). CachedImage: fetchQueueBias for live/mobile search (artist thumbnails ahead of albums in fetch-slot ordering); configurable observeRootMargin with a wider default to prepare priority slightly before elements enter view. Mobile search adds round artist-thumb styling; add shared cover blob downscale helper. * perf(image-cache): batch sibling IDB reads and guard cover size registry Use one read transaction when probing IndexedDB for sibling cover keys. Extract COVER_ART_REGISTERED_SIZES and add Vitest coverage so every literal coverArtCacheKey(_, size) in src stays aligned with sibling invalidation. Honor AbortSignal during JPEG encode in downscaleCoverBlob.
63 lines
1.8 KiB
TypeScript
63 lines
1.8 KiB
TypeScript
/**
|
|
* Client-side thumbnail path when a larger cover blob is already in cache:
|
|
* avoids a second HTTP getCoverArt for a smaller `size=` variant.
|
|
*/
|
|
|
|
export async function downscaleCoverBlob(
|
|
blob: Blob,
|
|
maxPx: number,
|
|
signal?: AbortSignal,
|
|
): Promise<Blob | null> {
|
|
if (typeof document === 'undefined' || signal?.aborted || maxPx < 16) return null;
|
|
|
|
let bmp: ImageBitmap | undefined;
|
|
try {
|
|
bmp = await createImageBitmap(blob);
|
|
if (signal?.aborted) return null;
|
|
const w = bmp.width;
|
|
const h = bmp.height;
|
|
if (!w || !h || !Number.isFinite(w) || !Number.isFinite(h)) return null;
|
|
|
|
const maxDim = Math.max(w, h);
|
|
/** Skip encode work when already suitable for tiny list thumbs. */
|
|
if (maxDim <= maxPx * 1.02) return null;
|
|
|
|
const scale = maxPx / maxDim;
|
|
const dw = Math.max(1, Math.round(w * scale));
|
|
const dh = Math.max(1, Math.round(h * scale));
|
|
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = dw;
|
|
canvas.height = dh;
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return null;
|
|
ctx.imageSmoothingEnabled = true;
|
|
ctx.imageSmoothingQuality = 'high';
|
|
ctx.drawImage(bmp, 0, 0, dw, dh);
|
|
|
|
if (signal?.aborted) return null;
|
|
|
|
return await new Promise<Blob | null>(resolve => {
|
|
if (signal?.aborted) {
|
|
resolve(null);
|
|
return;
|
|
}
|
|
const finish = (b: Blob | null) => {
|
|
signal?.removeEventListener('abort', onAbort);
|
|
resolve(signal?.aborted ? null : b);
|
|
};
|
|
const onAbort = () => finish(null);
|
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
canvas.toBlob(b => finish(b ?? null), 'image/jpeg', 0.88);
|
|
});
|
|
} catch {
|
|
return null;
|
|
} finally {
|
|
try {
|
|
bmp?.close();
|
|
} catch {
|
|
/* noop */
|
|
}
|
|
}
|
|
}
|