Commit graph

38 commits

Author SHA1 Message Date
Deluan
2499de2bac perf(artwork): key blurhash dedup by identity, plus review cleanups
The seen map was keyed by the full ArtworkID, which embeds the client token's
LastUpdate: every scan bump rotated the key, so the same-bytes dedup (and its
cheap version re-persist path) never fired in production and stale entries
accumulated forever. The key now zeroes LastUpdate, making the map bounded by
entity count and restoring the tested dedup behavior.

Cleanups from the same review: the base83 encoder is now exported from the
blurhash package and the DTO's duplicate copy is deleted; the always-set
blurHashes field lost its test-shaped nil guards; clearIfStored dropped its
inert version parameter (cleared rows never have their timestamp read); the
teeReader done flag is replaced by nilling the callback; worker-era comments
(async, cache-miss recompute) were updated to the inline design; and the e2e
specs assert directly instead of polling, since the hash is persisted before
the read helpers return.
2026-07-17 21:46:12 -04:00
Deluan
83e9bb8180 refactor(artwork): compute the blurhash inline, drop the background worker
Decode+encode is a few ms (the encoder downscales before its pixel loops), and the
tee fires on Close after the response is fully written, so the hash can be computed
in the serving goroutine: the queue, wake/stop lifecycle, lazy-start admin context,
and the e2e worker-teardown ordering all become unnecessary and are removed. This
also closes two correctness holes the queue had: a deletion signal could be dropped
behind a pending serve job, and buffered image bytes had no global cap.

The in-memory dedup now remembers a checksum of the served bytes plus the persisted
version: identical serves skip the decode and the write, but the same bytes under a
newer entity version re-persist, so blur_hash_updated_at keeps pace with row updates
and the Jellyfin DTO's staleness gate keeps emitting the stored hash after scans.
Placeholder-triggered clears check the seen map, then the stored row, so coverless
entities cost one row read once per process instead of a probe and write per serve.
UpdateBlurHash is a plain UPDATE with no user filtering, so the request context is
used directly (a client abort is survived via context.WithoutCancel).
2026-07-17 21:20:46 -04:00
Deluan
7307fd716b fix(artwork): close the underlying stream from the tee; rename to tee_reader
The tee was built around io.NopCloser(r) and teeCachedStream.Close only closed the
tee, so the underlying CachedStream (an open cache-file fd, or the raw source stream
when the image cache is disabled) was never closed — one fd leaked per teed serve,
enough to exhaust the process limit during a client's initial cover sync. The tee now
wraps the stream directly and its Close propagates; teeCachedStream is gone (all
artwork handlers io.Copy, none Seek). The file is renamed to tee_reader.go since the
wrapper is generic, not blurhash-specific.
2026-07-17 21:13:03 -04:00
Deluan
e8fac4c335 feat(artwork): trigger blurhash from the served-bytes tee
Get wraps an eligible original-size serve (album/artist/playlist) in a teeCachedStream
so the bytes streamed to the client are also captured; on a fully-consumed Close the
runner hashes exactly what was served. GetOrPlaceholder routes a vanished album/artist
cover through EnqueueClearIfGone, since no bytes flow through the tee on ErrUnavailable.
capAtNow keeps a future mtime out of the stored version. The mtime-preserved cover-swap
characterization test (previously pending) now passes, and the disappearing-cover e2e
serves through GetOrPlaceholder to match the real Jellyfin/Subsonic path.
2026-07-17 20:34:47 -04:00
Deluan
5cd75503cb fix(artwork): backfill warm caches and supersede pending gone on refill
Two issues in the fill-triggered blurhash recompute:

1. Enqueuing was gated on a cache miss (!r.Cached). On an upgraded instance the
   image cache is persisted and adopted across restarts, so already-cached
   artwork serves as a cache hit and never enqueued, leaving its migrated-empty
   blur_hash as a synthetic value indefinitely. Enqueue on every original-size
   serve instead: the worker's freshness guard turns already-hashed rows into a
   cheap read and only recomputes when the hash is stale or missing.

2. Enqueue merged the gone flag with a sticky OR, so a cover restored right
   after a missing-art serve kept gone=true and took the clear-and-return path,
   never recomputing. A successful serve proves the artwork exists, so it now
   clears any pending gone for that artwork; a gone that follows a fill still
   sticks.
2026-07-17 15:36:17 -04:00
Deluan
3759488db5 refactor(artwork): trigger blurhash recompute from the cache fill
The blurhash worker previously recomputed on every artwork serve and
reconciled a row-level freshness oracle against serve-time hints (force,
sourceGone, imageUpdatedAt) to decide whether the served bytes had actually
changed. Every staleness bug found in review was one case where that weak
oracle disagreed with the true one, and each fix imported one more serve-time
fragment into it.

Move the trigger to the image-cache fill instead: an original-size cache miss
is exactly when the served bytes change, so the reader's LastUpdated snapshot
is the truth and no reconciliation is needed. Resized fills recurse through
Get(size=0) and hash the original once; a disabled cache reports every original
serve as a fill, keeping real hashes available (the worker's unchanged-hash
guard keeps that write-free).

This deletes the force/sourceGone/imageUpdatedAt flags, the double-freshness
comparison, and the entire negative cache. Only the two irreducible pieces
survive: the placeholder byte-compare and the ErrUnavailable clear-hook
(EnqueueGone), since deletion-without-rescan is invisible to every passive
signal. Adds an e2e test for in-place cover swaps, the scenario that drove the
deleted machinery, now covered structurally by the fill trigger.

Schema, DTO, repositories and the blurhash encoder package are unchanged.
2026-07-17 14:47:01 -04:00
Deluan
ae36e4dfc7 fix(artwork): recompute on original serves when the image cache is disabled
With ImageCacheSize=0 every serve reads live bytes, so an in-place cover
swap without rescan changed the served image with no signal the worker
could see. Original-size serves now force on disabled caches, and a new
unchanged-hash guard skips the DB write, so the forced path costs only the
background decode those installs already pay per request.
2026-07-17 14:13:09 -04:00
Deluan
7a61776c1c fix(artwork): clear a fresh-looking hash when the serve finds no source
In the window before a rescan records a deleted cover (or after cache
eviction/restart), no row or mtime signal moves, so the freshness skip kept
the stale hash. ErrUnavailable serves now carry a sourceGone signal that
bypasses the skip only while a stored hash remains to clear — afterwards
the negative cache applies, so artwork-less entities still don't re-resolve
per serve. The e2e spec now covers the no-rescan window.
2026-07-17 13:53:08 -04:00
Deluan
a7eec3afc3 fix(artwork): clear the stored hash when the artwork source disappears
A removed cover made Get error with ErrUnavailable before the enqueue, so
the worker never saw the entity and the stale hash kept being emitted. The
unavailable path now enqueues too; the worker's reader signal carries the
folder change and the existing no-result clearing applies. Covered by an
e2e spec exercising the full disappear-and-clear flow.
2026-07-17 13:33:45 -04:00
Deluan
0e74cf0ab1 fix(artwork): only force blurhash recompute on original-size cache misses
Resized cache keys vary per requested size, so a first request for a new
thumbnail size (or an evicted resized entry) forced a recompute with an
unchanged source — wasted work, and a transient failure during it could
clear a valid stored hash. Resized readers re-fetch the original through
Get, so the original-size call still carries the real change signal.
2026-07-17 13:16:07 -04:00
Deluan
f9f6d36ebb fix(artwork): TTL for no-result memoization, prompt Close, no warmup force spike
- memoize every no-result outcome (ErrUnavailable can wrap transient agent
  and storage failures, so it is not a reliable definitive/transient
  discriminator) but bound it with a 1h TTL, which also un-poisons entries
  recorded under future file mtimes
- cancel the worker context and check done in the drain loop, so Close
  returns promptly instead of draining the backlog at up to 30s per item
- only force recompute when the image cache is operational: during warmup
  every serve is a miss, which caused a recompute spike at startup
2026-07-17 12:11:30 -04:00
Deluan
b040345fb0 fix(artwork): stop the blurhash worker on Close to fix test data races
CI's race detector caught the process-lifetime worker goroutine outliving
Ginkgo specs and touching mocks/fake filesystems being torn down. The
worker now has a stop() that waits for in-flight work, exposed as Close()
on the artwork service; unit suites close it up front (they don't exercise
blurhash), the artwork e2e suite closes it on cleanup.
2026-07-17 11:51:24 -04:00
Deluan
696399dab7 fix(artwork): don't memoize transient blurhash failures; track image freshness with cache disabled
Only ErrUnavailable (definitively no artwork) is negative-cached; timeouts
and storage/agent hiccups retry on a later serve. Enqueue now carries the
reader's LastUpdated so file swaps are detected even when the image cache
is disabled (where every serve reads the source and miss-forcing is off).
2026-07-17 11:20:41 -04:00
Deluan
e100c48102 fix(artwork): address blurhash review findings
- force recompute on image-cache miss, so in-place cover/sidecar swaps and
  agent image updates refresh the stored hash even when no entity row moved
- exclude external_info_updated_at from the artwork version: agent TTL
  refreshes bump it with an unchanged image, churning Finamp's cover cache
- lazy-start the worker goroutine and bound each computation with a 30s
  timeout; remember no-result entities per version to avoid re-decoding
  placeholders on every serve
- error (instead of silently skipping) on unknown artwork kinds in persist
2026-07-16 21:52:28 -04:00
Deluan
9f9019df07 feat(artwork): compute and persist blurhashes asynchronously on artwork serve 2026-07-16 21:51:04 -04:00
Deluan Quintão
ba8d427890
feat(ui): add cover art support for internet radio stations (#5229)
* feat(artwork): add KindRadioArtwork and EntityRadio constant

* feat(model): add UploadedImage field and artwork methods to Radio

* feat(model): add Radio to GetEntityByID lookup chain

* feat(db): add uploaded_image column to radio table

* feat(artwork): add radio artwork reader with uploaded image fallback

* feat(api): add radio image upload/delete endpoints

* feat(ui): add radio artwork ID prefix to getCoverArtUrl

* feat(ui): add cover art display and upload to RadioEdit

* feat(ui): add cover art thumbnails to radio list

* feat(ui): prefer artwork URL in radio player helper

* refactor: remove redundant code in radio artwork

- Remove duplicate Avatar rendering in RadioList by reusing CoverArtField
- Remove redundant UpdatedAt assignment in radio image handlers (already set by repository Put)

* refactor(ui): extract shared useImageLoadingState hook

Move image loading/error/lightbox state management into a shared
useImageLoadingState hook in common/. Consolidates duplicated logic
from AlbumDetails, PlaylistDetails, RadioEdit, and artist detail views.

* feat(ui): use radio placeholder icon when no uploaded image

Remove album placeholder fallback from radio artwork reader so radios
without an uploaded image return ErrUnavailable. On the frontend, show
the internet-radio-icon.svg placeholder instead of requesting server
artwork when no image is uploaded, allowing favicon fallback in the
player.

* refactor(ui): update defaultOff fields in useSelectedFields for RadioList

Signed-off-by: Deluan <deluan@navidrome.org>

* fix: address code review feedback

- Add missing alt attribute to CardMedia in RadioEdit for accessibility
- Fix UpdateInternetRadio to preserve UploadedImage field by fetching
  existing radio before updating (prevents Subsonic API from clearing
  custom artwork)
- Add Reader() level tests to verify ErrUnavailable is returned when
  radio has no uploaded image

* refactor: add colsToUpdate to RadioRepository.Put

Use the base sqlRepository.put with column filtering instead of
hand-rolled SQL. UpdateInternetRadio now specifies only the Subsonic API
fields, preventing UploadedImage from being cleared. Image upload/delete
handlers specify only UploadedImage.

* fix: ensure UpdatedAt is included in colsToUpdate for radio Put

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-18 18:57:33 -04:00
Deluan Quintão
49a14d4583
feat(artwork): add per-disc cover art support (#5182)
* feat(artwork): add KindDiscArtwork and ParseDiscArtworkID

Add new disc artwork kind with 'dc' prefix for per-disc cover art
support. The composite ID format is albumID:discNumber, parsed by
the new ParseDiscArtworkID helper.

* feat(conf): add DiscArtPriority configuration option

Default: 'disc*.*, cd*.*, embedded'. Controls how per-disc cover
art is resolved, following the same pattern as CoverArtPriority
and ArtistArtPriority.

* feat(artwork): implement extractDiscNumber helper

Extracts disc number from filenames based on glob patterns by
parsing leading digits from the wildcard-matched portion.
Used for matching disc-specific artwork files like disc1.jpg.

* feat(artwork): implement fromDiscExternalFile source function

Disc-aware variant of fromExternalFile that filters image files
by disc number (extracted from filename) or folder association
(for multi-folder albums).

* feat(artwork): implement discArtworkReader

Resolves disc artwork using DiscArtPriority config patterns.
Supports glob patterns with disc number extraction, embedded
images from first track, and falls back to album cover art.
Handles both multi-folder and single-folder multi-disc albums.

* feat(artwork): register disc artwork reader in dispatcher

Add KindDiscArtwork case to getArtworkReader switch, routing
disc artwork requests to the new discArtworkReader.

* feat(subsonic): add CoverArt field to DiscTitle response

Implements OpenSubsonic PR #220: optional cover art ID in
DiscTitle responses for per-disc artwork support.

* feat(subsonic): populate CoverArt in DiscTitle responses

Each DiscTitle now includes a disc artwork ID (dc-albumID:discNum)
that clients can use with getCoverArt to retrieve per-disc artwork.

* style: fix file permission in test to satisfy gosec

* feat(ui): add disc cover art display and lightbox functionality

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor: simplify disc artwork code

- Add DiscArtworkID constructor to encapsulate the "albumID:discNumber"
  format in one place
- Convert fromDiscExternalFile to a method on discArtworkReader,
  reducing parameter count from 6 to 2
- Remove unused rootFolder field from discArtworkReader

* style: fix prettier formatting in subsonic index

* style(ui): move cursor style to makeStyles in SongDatagrid

* feat(artwork): add discsubtitle option to DiscArtPriority

Allow matching disc cover art by the disc's subtitle/name.
When the "discsubtitle" keyword is in the priority list, image files
whose stem matches the disc subtitle (case-insensitive) are used.
This is useful for box sets with named discs (e.g., "The Blue Disc.jpg").

* feat(configuration): update discartpriority to include cover art options

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-13 18:33:18 -04:00
Deluan Quintão
98fdc42d09
test: fix ignored artwork tests (#4103)
* Fix artwork internal tests

* fix: rename artistReader functions to artistArtworkReader for clarity

Signed-off-by: Deluan <deluan@navidrome.org>

* fix: update artwork internal tests to handle corrupted cover scenarios

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2025-05-22 15:48:24 -04:00
Deluan Quintão
58367afaea
refactor: external_metadata -> external.Provider (#3903)
* tests for TopSongs

Signed-off-by: Deluan <deluan@navidrome.org>

* convert to Ginkgo

Signed-off-by: Deluan <deluan@navidrome.org>

* consolidate tests

Signed-off-by: Deluan <deluan@navidrome.org>

* rename external metadata -wip

Signed-off-by: Deluan <deluan@navidrome.org>

* rename external metadata to extdata.Provider

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor tests - wip

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor test helpers

Signed-off-by: Deluan <deluan@navidrome.org>

* remove reflection

Signed-off-by: Deluan <deluan@navidrome.org>

* use mock.Mock

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor

Signed-off-by: Deluan <deluan@navidrome.org>

* fix

Signed-off-by: Deluan <deluan@navidrome.org>

* receive Agents interface in Provider constructor

Signed-off-by: Deluan <deluan@navidrome.org>

* use mock for Agents

Signed-off-by: Deluan <deluan@navidrome.org>

* tests for SimilarSongs

Signed-off-by: Deluan <deluan@navidrome.org>

* remove duplication

Signed-off-by: Deluan <deluan@navidrome.org>

* ArtistImage tests

Signed-off-by: Deluan <deluan@navidrome.org>

* AlbumImage tests

Signed-off-by: Deluan <deluan@navidrome.org>

* fix provider error handling

Signed-off-by: Deluan <deluan@navidrome.org>

* UpdateAlbumInfo tests - wip

Signed-off-by: Deluan <deluan@navidrome.org>

* UpdateAlbumInfo tests - wip

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor

Signed-off-by: Deluan <deluan@navidrome.org>

* UpdateArtistInfo tests - wip

Signed-off-by: Deluan <deluan@navidrome.org>

* clean up

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor

Signed-off-by: Deluan <deluan@navidrome.org>

* fix test descriptions

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor

Signed-off-by: Deluan <deluan@navidrome.org>

* refactor: rename extdata package to external

Signed-off-by: Deluan <deluan@navidrome.org>

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2025-04-08 21:11:09 -04:00
Caio Cotts
0488fb92cb
Fix image stuttering (#3035)
* Fix image stuttering.

* Fix docker publishing for PRs

* Write tests for new square parameter.

* Simplify code for createImage.

---------

Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2024-05-24 20:19:26 -04:00
Deluan
806713719f Add lastUpdated to coverArt ids. Helps with invalidating art cache client-side. 2023-02-08 20:03:31 -05:00
Deluan
4a7e86e989 Fix file descriptor leaking. 2023-02-02 10:36:49 -05:00
Deluan
d8e794317f Return 404 when artwork is not available in /share/img endpoint 2023-02-01 10:34:02 -05:00
Kendall Garner
93adda66d9
Get album info (when available) from Last.fm, add getAlbumInfo endpoint (#2061)
* lastfm album.getInfo, getAlbuminfo(2) endpoints

* ... for description and reduce not found log level

* address first comments

* return all images

* Update migration timestamp

* Handle a few edge cases

* Add CoverArtPriority option to retrieve albumart from external sources

* Make agents methods more descriptive

* Use Last.fm name consistently

Co-authored-by: Deluan <deluan@navidrome.org>
2023-01-17 20:22:54 -05:00
Deluan
e40da183bb Move artwork id encoding to public package 2023-01-16 15:24:25 -05:00
Deluan
3f2d24695e PreCache artist images 2023-01-13 22:18:34 -05:00
Deluan
69e0a266f4 Remove size from public image ID JWT 2023-01-13 22:18:34 -05:00
Deluan
918fee3ea3 Artwork reader for Artist 2023-01-13 22:18:34 -05:00
Deluan
bf461473ef Add local agent, only for images 2023-01-13 22:18:34 -05:00
Deluan
61e5523457 Handle "naked" CoverArtIDs (IDs of album, mediafiles and playlists) 2022-12-28 15:31:56 -05:00
Deluan
bc09de6640 Better error handling 2022-12-28 15:31:56 -05:00
Deluan
949331ed24 GetCoverArt generates a tiled (2x2) image for playlists 2022-12-28 15:31:56 -05:00
Deluan
501386b11f Parse correctly playlist CoverArt ids 2022-12-28 15:31:56 -05:00
Deluan
8f3387a894 Fix tests and clean up code a bit 2022-12-28 15:31:56 -05:00
Deluan
332900774d Rename DevFastAccessCoverArt to EnableMediaFileCoverArt 2022-12-28 15:31:56 -05:00
Deluan
722a00cacf Fix artwork caching 2022-12-28 15:31:56 -05:00
Deluan
92ddae4a65 Created dedicated artwork readers 2022-12-28 15:31:56 -05:00
Deluan
c1c4645501 Move artwork handling to its own package 2022-12-28 15:31:56 -05:00
Renamed from core/artwork.go (Browse further)