Skip to content

feat(hub): downsample external cover art and dedupe cache variants (#145) - #260

Merged
benders merged 1 commit into
mainfrom
feature/145-art-cache-downsample
Jul 14, 2026
Merged

feat(hub): downsample external cover art and dedupe cache variants (#145)#260
benders merged 1 commit into
mainfrom
feature/145-art-cache-downsample

Conversation

@benders

@benders benders commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Closes #145.

Problem

The art cache filled its 100 MB cap almost immediately. Two causes (see investigation on the issue):

  1. The external-art branch of getCoverArt (fanart.tv / Last.fm) ignored the size param but keyed the cache by it — identical full-resolution ~1000×1000 bytes (100–500 KB) were cached once per requested size. The SPA requests five variants (48/80/300/400/unsized), so one cover could occupy ~1.5 MB across five slots.
  2. Unsized requests cached/served original bytes — hi-res embedded art can be several MB per image.

Changes

  • Effective size normalization (hub/src/routes/subsonic/art-size.ts): every request resolves to a clamped size in [16, 1024]; absent/invalid defaults to 1024. Cache key is always ${id}:${effSize}, so unsized and size=1024 dedupe; old-format entries LRU-evict naturally (no migration).
  • Hub-side downsampling of external art (hub/src/services/art-resize.ts, new sharp dep): the external branch resizes fetched bytes to fit the effective size before caching and transmitting — a 48px PlayerBar thumbnail no longer downloads 300 KB. Local/peer art is untouched (already resized upstream by Navidrome, which now always receives the effective size). GIF/SVG pass through unresized; sharp failure on corrupt bytes falls back to the original buffer.
  • Default cache cap 100 MB → 1 GB — even downsampled, one 300px variant across a merged library is hundreds of MB; 100 MB churned as a revolving door. Still overridable via ART_CACHE_MAX_BYTES env and the admin cache API.
  • Docs: hub-internals.md (env table, art-cache section), opensubsonic.md (getCoverArt size cap caveat).

Behavior change note

size values above 1024 are now clamped and unsized requests return at most 1024px (this caps the #200 lightbox at 1024 — deemed sufficient for a music player UI).

Testing

  • pnpm verify green: hub 799 tests / frontend 145, typecheck clean, boundary lint clean.
  • pnpm lint exits zero with no warnings.
  • pnpm test:federation green (full py-sonic compat + eviction/re-admission cluster suite).
  • New coverage: effectiveArtSize unit cases; external resize asserted via sharp().metadata() on the response body; dedupe via x-cache: HIT + single upstream fetch; 4096→1024 clamp; corrupt-bytes fallback; GIF passthrough.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SwwPcrohddTUN7kmQVedQC

…loses #145)

getCoverArt now normalizes every request to an effective size (default
and max 1024px, min 16): the cache key always carries it, local/peer
branches forward it to Navidrome, and the external fanart.tv/Last.fm
branch — which previously cached full-resolution bytes once per
requested size — downsamples hub-side via sharp before caching and
transmitting. GIF/SVG pass through unresized; unparseable image bytes
fall back to the original buffer. Default art-cache cap raised
100 MB -> 1 GB (still overridable via ART_CACHE_MAX_BYTES / admin API).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SwwPcrohddTUN7kmQVedQC
@benders
benders marked this pull request as ready for review July 14, 2026 14:00
@benders
benders merged commit b8b3659 into main Jul 14, 2026
2 checks passed
@benders
benders deleted the feature/145-art-cache-downsample branch July 14, 2026 14:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Should cached images be reduced in size?

2 participants