List-view covers, pre-warm + scroller memory fix (requires #676)#731
List-view covers, pre-warm + scroller memory fix (requires #676)#731Paelsmoessan wants to merge 15 commits into
Conversation
365288e to
71bffc7
Compare
…dling Per PR Listenarrs#733 review (therobbiedavis): avoid juggling a second DB. Remove the IImageCacheStore / ImageCacheStore sidecar SQLite negative cache and its DI registration, and strip the negative-cache logic from ImagesController. The user-visible re-fetch hammering it was meant to prevent is already handled without it: the Listenarrs#731 render-loop fix stops the grid re-issuing image requests at rest, and a cover miss now returns a cacheable placeholder (Cache-Control: public, max-age=300) instead of the old uncacheable 500, so the browser stops re-asking. The only thing dropped is the server-side skip of provider re-lookups for known-coverless items, which is minor now that the loop and 500 are fixed. Thumbnails, the getImageUrl size fix, and the pre-warm service are unaffected. Build clean, 19/19 image tests pass. TODO.md: record the two follow-ups surfaced in review, canonical cover identity (hash/entity-id keying instead of ASIN) and an oversized-thumbnail byte guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Heads up, I just found #676 (closing #675) by @s3ntin3l8, opened a few weeks before this PR, and it fixes the same His #676 is actually the more complete scroller fix: it also handles the oversized scroll area, overscroll past both ends, the unreachable last row, and header height in Where this PR doesn't overlap is the second fix: the cover-metadata fallback returning uncacheable 500s (an internal anonymous type read via Happy to consolidate whichever way you'd prefer:
Mainly flagging so the effort isn't duplicated and #676 gets a look, it's been open and unreviewed for a few weeks. |
|
@Paelsmoessan Agreed, split these changes. #676 will own the virtual-scroller fix and #675. Remove AudiobooksView.vue from this PR and retain only the cover-metadata fallback fix and its tests. |
`GetMetadataAsync` returns `Task<object?>` and, on success, an *internal* anonymous
type `new { metadata = ... }` declared in listenarr.application. The controller read
it via `dynamic env.metadata`; because the anonymous type is internal to another
assembly, the dynamic binder sees it as `object` and throws RuntimeBinderException
("'object' does not contain a definition for 'metadata'"), so this fallback never
resolved those covers and always 500'd. Browsers don't cache 500s, so grid views
re-fired them on every render pass.
Fix: read the property via reflection, which reaches public members across the
assembly boundary; a genuinely absent member yields null and falls through to the
cacheable placeholder. Adds a controller test proving the anonymous envelope resolves
(the image URL is fetched) and that a missing member no longer 500s.
Per maintainer review on Listenarrs#731: the AudiobooksView virtual-scroller fix is split out
(Listenarrs#676 owns it); this PR now contains only the cover-metadata fallback fix and its test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
71bffc7 to
015145c
Compare
015145c to
e014b82
Compare
New CoverThumbnailService downscales cached originals to allow-listed sizes (grid=400px, grid2x=800px) on demand, caches them under config/cache/images/thumbs/, and serves them via ImagesController when ?size= is passed. Frontend getImageUrl / getProtectedImageSrc gain an optional size option (backend image URLs only). Cuts grid cover payloads from MBs to ~30KB. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… 500s The grid's custom virtual scroller re-measured row height inside a watcher that then called updateVisibleRange() (which writes visibleRange), forming an infinite measure->update loop when card heights vary. Removed that self-retrigger; row height is measured on mount/resize/view-mode/details instead. ImageCandidateLookupWorkflow used `dynamic env.metadata`, throwing RuntimeBinderException (=> 500, uncacheable) for covers whose envelope lacks a metadata property, so every render pass re-fetched them. Replaced with safe reflection so misses fall through to the cacheable placeholder. Also wires the library grid to request ?size=grid thumbnails. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…test The cover-thumbnail commit made ICoverThumbnailService a required 9th ctor arg on ImagesController, breaking 10 existing image tests that use the 8-arg form. Make it an optional parameter (DI still injects the registered service; ActivatorUtilities resolves it over the default). Also correct the ImageCandidateLookupWorkflow comment to the real root cause (internal anonymous type + cross-assembly dynamic, not a missing property) and add ImagesController_MetadataEnvelopeReflectionTests proving the anonymous-envelope fallback now resolves the cover via reflection and no longer 500s. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Book covers are stored as already-built /api/.../images/{id} paths, which matched none of
getImageUrl's buildApiImageUrl branches, so the ?size= param was never appended and the grid
fetched full-size originals. Append the size in the passthrough branch when the URL targets the
images endpoint. Cuts grid cover payloads from ~530KB to ~37KB.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dedicated SQLite index (config/cache/images/ImageCache.db) beside listenarr.db — no EF migration, rebuildable. ImagesController consults it before provider lookups: a known 'no-cover' result (re- checked after 23h) returns the cacheable placeholder immediately instead of re-hitting Audible/ Audnexus on every refresh; resolutions are recorded. ICoverThumbnailService/IImageCacheStore are optional ctor params so existing 8-arg image tests still compile. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CoverThumbnailWarmupService pre-generates grid thumbnails for all library covers on startup (1 min after boot) and every 12h, throttled to 3 concurrent ImageSharp decodes. Idempotent — fresh thumbnails are skipped. Removes the on-demand generation lag on first library load; covers are ready on disk before the page is opened. Verified: regenerated deleted thumbnails in ~7s with no browser involved. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sidebar sub-items (.nav-subitem) had CSS for only their default and active states, so they never highlighted on hover or keyboard focus. On top of that, .nav-sub was collapsed with pointer-events:none until opened via a parent hover, so a non-active section's children were unreachable until you hovered the top node first. Add a :hover / :focus-visible highlight, and keep the sub-navs expanded so children are always interactive and highlight on their own hover. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dling Per PR Listenarrs#733 review (therobbiedavis): avoid juggling a second DB. Remove the IImageCacheStore / ImageCacheStore sidecar SQLite negative cache and its DI registration, and strip the negative-cache logic from ImagesController. The user-visible re-fetch hammering it was meant to prevent is already handled without it: the Listenarrs#731 render-loop fix stops the grid re-issuing image requests at rest, and a cover miss now returns a cacheable placeholder (Cache-Control: public, max-age=300) instead of the old uncacheable 500, so the browser stops re-asking. The only thing dropped is the server-side skip of provider re-lookups for known-coverless items, which is minor now that the loop and 500 are fixed. Thumbnails, the getImageUrl size fix, and the pre-warm service are unaffected. Build clean, 19/19 image tests pass. TODO.md: record the two follow-ups surfaced in review, canonical cover identity (hash/entity-id keying instead of ASIN) and an oversized-thumbnail byte guard. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adopts Bjorn Hansen's list-view scroller fix (upstream PR Listenarrs#676, closing Listenarrs#675): fixed list-row heights, rAF-throttled scroll handler, measure-grid-once instead of per-scroll re-measure, header height reserved in totalHeight, and overscroll clamping. This supersedes our earlier freeze fix (52d0299) with a more complete approach, and fixes the last-row text clipping our version had. REQUIRES upstream PR Listenarrs#676. Carried in canary until that PR merges upstream; the next upstream sync will reconcile it automatically. Our cover-thumbnail (?size=grid) changes in the same file are preserved. Co-Authored-By: s3ntin3l8 <58235613+s3ntin3l8@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Return to the Audiobooks list after viewing a book/author/series and land back at your previous scroll position instead of the top. Two scroll models, handled natively: - Authors/series scroll the window: a router scrollBehavior restores savedPosition on back/forward and scrolls to top on forward nav (also fixes detail pages opening at the list's scroll offset). - Books scroll an inner container: save/restore scrollContainer.scrollTop across the remount (reliable now that Listenarrs#676 made row heights deterministic). - Silent background refresh on return so the remount does not flash a spinner. No keep-alive (deliberately): the view remounts, so there is a brief render-then- restore jump. That is the robust trade-off; keep-alive removes the jump but reintroduces the route-watcher nav-hijack complexity (see reason-notes). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wrap the list view in <KeepAlive> so authors/series (a non-virtualized grouped
view) no longer re-render every cover on back-out. The blocker that broke earlier
keep-alive attempts was AudiobooksView's route.query.group watcher calling
router.replace('/audiobooks') while cached-but-inactive, which hijacked every
detail click back to the list; now guarded to run only on the active list route.
- App.vue: <KeepAlive :include=['AudiobooksView']> (no key, no transition).
- Watcher guard: bail unless route.name is the list route (audiobooks/home).
- Lifecycle moved to onActivated/onDeactivated (the view no longer remounts):
save/restore books inner scroll, silent background refresh, click-listener guard.
Builds on the scrollBehavior + manual-restore scroll preservation. Known minor:
the cold first view can land ~2 rows high because grouped-view covers load after
the restore (layout shift); to be polished by reserving cover box size.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ack-out)" This reverts commit 502a2c8.
…sibleRange watcher (RAM bleed)
e014b82 to
eef3be3
Compare
Requires #676 (prerequisite)
This PR builds on @s3ntin3l8's list-view virtual-scroller fix (#676) and needs it to work, #676 should land first. Our changes sit on top of Björn's scroller rather than replacing it.
Summary
On top of #676:
dynamic(the internal anonymous type across the assembly boundary threwRuntimeBinderException→ uncacheable 500). This is the original List-view covers, pre-warm + scroller memory fix (requires #676) #731 fix, retained.visibleRangewatcher whose re-measure loop leaked DOM/RAM over a session on large libraries. Row height is measured on mount / resize / view-mode change / details toggle instead.Notes
.vuescroller works well enough now, but has a ceiling on very large libraries. A virtualization rewrite (TanStack Virtual) has been prototyped and may be pursued separately in a fork; this PR keeps the existing scroller and just stops the freeze/leak.Testing
Verified locally on a large library: scrolling is smooth, RAM holds steady over a session, and covers pre-warm.