* 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.
* Enhance CachedImage and ArtistDetail components with improved image caching and priority handling
- Refactor CachedImage to utilize a priority system for image loading based on viewport visibility, improving performance during scrolling.
- Update useCachedUrl to accept an optional getPriority function for better cache management.
- Optimize ArtistDetail and Artists components by using useMemo for cover art URLs, reducing redundant calculations and improving rendering efficiency.
- Adjust image loading logic in CachedImage to ensure smoother transitions and avoid unnecessary fetch requests.
* perf(ui): unblock IDB cover art, stabilize mainstage rails and virtual lists
Let IndexedDB reads bypass the network concurrency slot so cached thumbnails
paint without queueing behind remote fetches; debounce disk eviction during
heavy scrolling.
Fix mainstage horizontal rails: dedupe album/song ids for React keys, widen
artwork budget overscan, avoid resetting the budget on list append, and raise
Home initial artwork budgets. CachedImage treats already-decoded images as
loaded; rail cards load cover images eagerly.
Refresh dynamic color extraction and extend virtual scrolling / scroll roots on
Albums, Artists, Playlists, and related surfaces.
Remove local agent-only commit instructions from the repository tree.
* perf(virtual): viewport-based overscan for main scroll lists
Drive TanStack Virtual overscan from measured scroll height so each list
renders about one screen of extra rows above and below the viewport for
snappier scrolling on Albums, Artists (list mode), and Tracks virtual song list.
Introduce useResizeClientHeight helpers (ID + ref) for ResizeObserver-based
clientHeight tracking.
* docs(changelog): note PR #468 UI cover cache, rails, and virtual lists
Add a coarse summary under 1.46.0 Changed for cover-art pipeline,
mainstage rails, viewport-based overscan, and library/chrome polish.
The previous design handed every <img> its own URL.createObjectURL for the
same cached Blob. WebKitGTK shrugged it off, but Chromium/WebView2 keys
its decoded-image cache by URL — so identical thumbnails were re-decoded
once per instance. On Windows this made cover/artist grids painfully slow
even when blobs were warm in memory.
Refactor the URL layer to be refcounted and shared:
- New acquireUrl(cacheKey) / releaseUrl(cacheKey) API. First acquire
creates the URL; subsequent acquires return the same string and bump
the refcount. Revoke is deferred 500 ms after the count hits zero so
in-flight decodes finish cleanly.
- useCachedUrl uses a lazy useState initializer: when the blob is hot,
the very first <img src> is already the blob URL. No fetchUrl→blobUrl
swap, no decode thrash, no race against the LRU.
- CachedImage passes fallbackToFetch=false: previously the <img>
briefly carried the raw server URL while the blob resolved, which
triggered an HTTP fetch that the browser then aborted when src
flipped to blob: — visible in DevTools as a flood of "Pending / 0 B"
requests. Memory hits remain instant via the synchronous acquire
path; cold paths now do a single fetch via getCachedBlob.
- invalidateCacheKey / clearImageCache now also purge URL entries.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous design kept a single global Map<cacheKey, objectURL> with an
LRU cap of 150 and aggressively revoked the oldest URL on overflow. On
libraries with more than 150 cached covers (artist + album grids quickly
exceed that), an in-use URL would get revoked because a different
consumer pushed it out of the cache, producing the "Failed to load
resource: blob:..." flood that several users have reported.
Refactor the cache to be blob-centric:
- Public API is now getCachedBlob() returning the Blob itself.
- In-memory LRU now holds Blobs (cap 200), not URLs. Map-entry eviction
drops the strong reference and lets the GC free the Blob once no
consumer (object URL, <img>, <canvas>) still holds it. No revoke choreography needed.
- useCachedUrl creates its own URL.createObjectURL on blob arrival and
revokes it on cleanup with a 500 ms grace delay so the DOM <img> has
time to finish decoding the URL we just took away.
- All existing callers keep their string-returning API; no call-site
changes outside the hook itself.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove auto-refresh timer from Random Albums (caused 100ms progress
interval + 30 concurrent fetches every 30s, eventually freezing the app)
- Limit concurrent image fetches to 5 (was unbounded)
- Cap in-memory object URL cache at 150 entries with revokeObjectURL eviction
- Add cancellation flag to useCachedUrl to prevent setState on unmounted components
- Fix hardcoded "Neueste" page title in New Releases (now uses i18n)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>