5014 Commits

Author SHA1 Message Date
Deluan
55608b2d20 fix(artwork): resolve private playlists with an admin context 2026-07-22 15:53:32 -04:00
Deluan
7713a6d6b2 fix(artwork): include M3U external art flag in the config fingerprint 2026-07-22 15:53:32 -04:00
Deluan
bc30ce67c6 fix(artwork): keep fresh re-enqueues ahead of stale failure backoff 2026-07-22 15:53:32 -04:00
Deluan
d6434b9929 fix(artwork): reject decompression-bomb dimensions before decoding 2026-07-22 15:53:32 -04:00
Deluan
b3526c0fba test(artwork): make leak and permission tests pass on linux
goleak now ignores notify's nonrecursive-tree goroutines (linux uses
inotify, which spawns dispatch+internal instead of darwin's recursive
dispatch), and the read-only-dir prune spec skips under root, where
permission bits cannot make Remove fail.
2026-07-22 15:53:32 -04:00
Deluan
3d32157403 test(artwork): move soak test into the Ginkgo suite 2026-07-22 15:53:32 -04:00
Deluan
5482784bfc fix(artwork): treat playlist cover URL 404 as definitive miss
The playlist ExternalImageURL step used sources.go's fromURL, which maps any
non-200 to a generic error, so a stale URL returning 404/410 was classified
transient: infinite backoff plus it counted toward the circuit breaker,
blocking valid external work. Add a local fetch in resolve.go that maps
404/410 to model.ErrNotFound (definitive) while keeping other non-200s
transient. sources.go is left untouched.
2026-07-22 15:53:32 -04:00
Deluan
6afcb93a9b fix(artwork): retry higher-priority external art after fallback hit
With CoverArtPriority="external,cover.jpg", a transient external failure
followed by a folder hit dropped the external error: the worker recorded
found and deleted the queue row, so the configured higher-priority external
art was never retried. Carry extError onto the fallback resolution and add
an outcomeFoundStale that persists+serves the art but reschedules via
MarkFailed, giving the external source another chance. When external later
answers definitively-not-found, the hit is not stale and the row is deleted.
2026-07-22 15:53:32 -04:00
Deluan
67f6d8aee8 fix(artwork): cap resolved image reads
A user-editable ExternalImageURL can point at an arbitrarily large endpoint;
a fast server could make the worker buffer hundreds of MB inside the 5s HTTP
timeout. Bound the read to a fixed 20MB cap (no config knob) via io.LimitReader
and fail the item if it is exceeded.
2026-07-22 15:53:32 -04:00
Deluan
87095fab08 refactor(artwork): deduplicate purge loop, backfill table, and extGate alias 2026-07-22 15:53:32 -04:00
Deluan
c57496d50d fix(artwork): treat missing local playlist cover as definitive, not transient
A playlist ExternalImageURL pointing at a local file that fails to open was
routed through extError, causing failed/48h-retry loops that burn a rate
limiter token forever instead of falling through to the generated grid.
2026-07-22 15:53:32 -04:00
Deluan
0fbbd01357 style(artwork): fix comment accuracy and budget; fingerprint ArtistImageFolder
Correct the inverted workerDeps.extGate comment, trim over-budget doc comments, and add conf.Server.ArtistImageFolder to the resolution fingerprint so an image-folder change re-resolves artist artwork.
2026-07-22 15:53:32 -04:00
Deluan
bab9b5cd3a fix(artwork): purge dangling queue rows and guard concurrent re-enqueues
Queue rows for deleted entities failed forever (Get -> ErrNotFound -> failed -> capped retries, unbounded). Add ArtworkQueueRepository.PurgeDangling, called from Prune next to the item_artwork purge. Separately, the found/absent path unconditionally deleted the dequeued row, erasing a concurrent scan re-enqueue; switch to DeleteIfUnchanged, which deletes only while retry_at still matches the dequeued value (verified retry_at is the column an Enqueue upsert resets).
2026-07-22 15:53:32 -04:00
Deluan
454fd24833 fix(artwork): resolve full playlist source chain
resolvePlaylist only built the generated grid, dropping the uploaded-image, sidecar and ExternalImageURL sources the old reader_playlist.go chain serves. Port the full chain before the grid fallback: uploaded (upload), sidecar (folder), and ExternalImageURL routed through extGate with the same extError semantics as the other external steps. Also rewires the artist external step onto ArtistImageResult.
2026-07-22 15:53:32 -04:00
Deluan
c01e9b3184 fix(artwork): propagate transient artist image errors to the worker
callGetImage swallowed all agent errors, so an agent outage surfaced as ErrNotFound and the worker settled artist artwork as a definitive absent (and reset the breaker). Add an additive ArtistImageResult path that returns the underlying agent error on transient failure while keeping ArtistImage byte-identical for existing callers; the worker's artist external step uses it via fromArtistExternalResult.
2026-07-22 15:53:32 -04:00
Deluan
1ed8ebf9b0 test(artwork): leak/soak coverage and deferred assertions 2026-07-22 15:53:32 -04:00
Deluan
ad38cd1d58 feat(artwork): artwork backfill, fingerprint re-resolution and scheduled jobs 2026-07-22 15:53:32 -04:00
Deluan
25a05fd017 feat(artwork): enqueue artwork resolution from scan and CRUD paths 2026-07-22 15:53:32 -04:00
Deluan
57c64e386a feat(artwork): add acquisition worker service 2026-07-22 15:53:32 -04:00
Deluan
d6fc829f84 style(artwork): tighten processor comments to budget 2026-07-22 15:53:32 -04:00
Deluan
e0655dc882 feat(artwork): add acquisition processor
Resolves one queue item end to end: hash/dedup, decode + 128px thumbnail
blurhash, place bytes (store vs source file), and persist found/absent/
failed state for the worker (Task 4) to act on.
2026-07-22 15:53:32 -04:00
Deluan
608db503a7 fix(artwork): propagate playlist tile failures and dedupe external step 2026-07-22 15:53:32 -04:00
Deluan
967de74bf7 feat(artwork): add worker-side artwork resolvers 2026-07-22 15:53:32 -04:00
Deluan
2efa697e52 feat(artwork): import blurhash encoder from #5797 2026-07-22 15:53:32 -04:00
Deluan
1f818e7633 fix(artwork): store backing-file provenance per item, not per hash 2026-07-22 15:52:54 -04:00
Deluan
c04c8ee02a fix(artwork): mock PutImage refreshes created_at like the SQL repository
Prune specs now age fixtures directly instead of seeding stale timestamps through the upsert.
2026-07-22 00:58:22 -04:00
Deluan
ebbe533c6a fix(artwork): reject malformed hashes in ImageStore operations
Known-absent states carry an empty hash and malformed persisted hashes could panic path sharding or inject separators; Write/Open/Remove now return an error for anything but 16 lowercase hex chars.
2026-07-22 00:51:13 -04:00
Deluan
0147cc59b1 fix(artwork): honor the orphan cutoff in the repository mock
The mock's DeleteOrphans now applies createdBefore like the SQL implementation, and a new spec covers a freshly reacquired row surviving prune.
2026-07-22 00:42:42 -04:00
Deluan
034cd17498 fix(artwork): index artwork_queue in dequeue order
The previous leading retry_at range column forced a temp B-tree sort of the whole eligible set on every DequeueBatch; ordering the index by (priority DESC, enqueued_at) lets scans stop after the batch size.
2026-07-22 00:36:22 -04:00
Deluan
8147f7c40b fix(artwork): rewrite vanished duplicates and sweep stale mime variants
Write falls through to a real write when the liveness touch fails, and sweep retention now matches the recorded mime's extension so obsolete variants are reclaimed.
2026-07-22 00:28:51 -04:00
Deluan
bf614e66ad fix(artwork): guard orphan file removal with the prune grace window
Duplicate ImageStore writes refresh the file mtime and Remove skips files newer than the cutoff, so overlapping acquisitions cannot lose their store files to a concurrent prune.
2026-07-22 00:19:45 -04:00
Deluan
623b7d6a6c fix(artwork): atomic orphan deletion and timestamp semantics from review
DeleteOrphans re-checks age+references at delete time, PutItemArtwork defaults attempted_at, queue mock timestamps mirror SQL.
2026-07-22 00:11:15 -04:00
Deluan
6f7f9c6463 fix(artwork): address review findings on prune/sweep races and mock fidelity
Sweep now honors an mtime grace window (in-flight acquisitions and temp files), reacquired orphans reset the prune grace window, and the queue mock implements real stale-absent semantics.
2026-07-21 23:57:58 -04:00
Deluan
8fd7ef19f3 refactor(artwork): apply simplify-pass cleanups
Internal item_artwork sqlRepository helper, toSQLArgs upserts, batched queue enqueue, EnqueueStaleAbsent moved to queue repo, snapshot-based prune sweep, mock/real semantics aligned.
2026-07-21 23:37:05 -04:00
Deluan
1041e45ca7 fix(artwork): chunk unbounded IN clauses and restore interface docs 2026-07-21 23:24:42 -04:00
Deluan
4f835437a9 refactor(artwork): merge item artwork state into ArtworkRepository 2026-07-21 23:14:01 -04:00
Deluan
b16ef725c9 refactor(artwork): fold originals package into core/artwork as ImageStore 2026-07-21 23:09:31 -04:00
Deluan
3e7685adc2 fix(artwork): never sweep files on transient DB errors during prune 2026-07-21 23:02:47 -04:00
Deluan
db16b3de9a feat(artwork): add artwork prune (orphan cleanup) 2026-07-21 22:58:20 -04:00
Deluan
b72597821a feat(artwork): add content-addressed originals store 2026-07-21 22:53:00 -04:00
Deluan
fcff9c63e7 feat(artwork): implement artwork_queue repository 2026-07-21 22:45:08 -04:00
Deluan
14dd57052e feat(artwork): implement item_artwork repository with batched hydration 2026-07-21 22:45:03 -04:00
Deluan
f926539c04 feat(artwork): implement artwork repository 2026-07-21 22:44:24 -04:00
Deluan
6cce65f759 feat(artwork): add artwork models, repository interfaces and mocks 2026-07-21 22:35:34 -04:00
Deluan
7aacb01f4f feat(artwork): add artwork, item_artwork and artwork_queue tables 2026-07-21 22:31:03 -04:00
Deluan Quintão
62257527d7
fix(subsonic): avoid double brackets when appending subtitle or version (#5832)
When AppendSubtitle or AppendAlbumVersion is enabled, the subtitle/version
tag was always wrapped in parentheses and appended to the title/album name.
If the tag value already came wrapped in brackets (e.g. "(non-explicit
version)"), the result was doubled: "Title ((non-explicit version))".

Append the tag as-is when it is already wrapped in a matching bracket pair
- (), [], {} or <> - and trim surrounding whitespace first. The shared
appendSuffix helper is used by MediaFile.FullTitle, MediaFile.FullAlbumName
and Album.FullName so all consumers behave consistently.
2026-07-20 21:49:14 -04:00
Deluan
c35b14dd74 chore(deps): update go-taglib to v2.3.1
Signed-off-by: Deluan <deluan@navidrome.org>
2026-07-20 09:49:48 -04:00
Deluan Quintão
6c2644d208
feat(ui): remember 'items per page' selection across sessions (#5819)
* feat(ui): add perPageStore helper for items-per-page persistence

* feat(ui): persist items-per-page selection in localStorage

* feat(ui): enable per-page persistence on album grid, missing files and playlist tracks

* feat(ui): restore saved album grid page size on fresh load

* feat(ui): restore saved items-per-page on list load

* fix(ui): move defaultRowsPerPageOptions to perPageStore to satisfy react-refresh lint

* fix(ui): correct defaultRowsPerPageOptions import in List and add render test

* refactor(ui): default getStoredPerPage fallback to the first option

The fallback equalled options[0] at three of four call sites; default it so
those sites stop restating it. SongList and useAlbumsPerPage still pass an
explicit fallback where it legitimately differs from the first option.

* fix(ui): persist only user-selected page sizes, validate album size against width

Persisting on every pagination context value let a URL-injected perPage (e.g.
the perPage=15 album link in NowPlayingPanel) or a forced single-option mobile
grid overwrite the saved preference. Persist only a value that is an actual
option in a multi-option selector. Also validate the album grid's redux session
value against the current width so a size chosen at a wider breakpoint can't
leave an out-of-range selector.

* fix(ui): restore saved items-per-page on the radio list

RadioList passed a hard-coded perPage that overrode List's stored seed via the
props spread, so a radio-list page size was persisted but never restored. Seed
it from storage like the other list views.

* fix(ui): persist page size only on an actual selector change

Watching the pagination context value meant any valid value persisted itself:
loading a list at a breakpoint where the saved size is invalid stored the
responsive fallback, and opening a URL with a valid ?perPage= stored that too,
either way discarding the user's real preference. Inject a wrapped setPerPage
instead, so only the rows-per-page selector writes. This also drops the
option-validation heuristics, which the new trigger makes unnecessary.
2026-07-19 18:59:37 -04:00
Deluan Quintão
bf79d2f3a2
refactor(plugins): remove non-functional experimental manifest option (#5821)
The `experimental.threads` manifest option never actually worked, so drop
it from the schema, the generated types, the loader and the docs.
2026-07-19 18:52:30 -04:00
Deluan Quintão
fed9665060
fix(streaming): surface why a transcode decision failed (#5820)
* fix(subsonic): surface the reason a transcode decision failed

getTranscodeDecision returned a bare "failed to make transcode decision"
with no clue why, and the probe command ran ffprobe with -v quiet, so even
the server log bottomed out at "exit status 1". A user whose files had been
moved by an external tool only saw the opaque error.

ProbeAudioStream now returns a typed ProbeError that separates the file path
from the reason: ffprobe runs with -v error so its stderr diagnostic is
captured, and a missing or unreadable file is reported as "file not found"
rather than ffprobe's misleading "Invalid data found". The handler logs the
full detail (including the path) and returns the reason to the client with
the server path stripped out.

Reported-by: Tolriq (Symfonium)

* refactor(ffmpeg): use errors.AsType for ExitError match

probeErrorReason used the older var+errors.As form while the rest of the
codebase (and its sibling transcodeFailureReason) uses the generic
errors.AsType. Switch to it for consistency; behavior is unchanged.

* fix(subsonic): return error 70 when the source file is missing

A getTranscodeDecision probe failure was always reported as generic error 0.
When the source file is gone (moved or deleted out from under the DB), that is
a not-found condition, so return the standard Subsonic error 70 ("data not
found") instead — matching what the endpoint already returns for an unknown
mediaId. Files that exist but are corrupt or unreadable stay error 0.

ProbeError now wraps the underlying cause and implements Unwrap, so the handler
detects the case with errors.Is(err, fs.ErrNotExist).

* fix(ffmpeg): keep probe error paths out of client-facing reasons

Addresses review feedback on the ProbeError type: the Reason field doubled as
both the log detail and the client message, so an ffprobe launch failure (a
*os.PathError from fork/exec) could leak the ffprobe binary path to clients,
and an unexpected stat error was reduced to "file not accessible" in the log.

Split the two concerns: Reason now holds only a path-free, client-safe string
(built at construction), while Error() logs the full underlying cause. Launch
failures return a generic "could not read file" instead of the raw exec error.
SafeReason no longer does substring path-stripping (removing the empty-Path
edge case); the stripping happens once, against ffprobe's stderr.

* fix(subsonic): don't report a broken ffprobe as a missing media file

Two issues from review of the previous commit:

Code 70 was selected with errors.Is(err, fs.ErrNotExist), but a launch failure
of a deleted ffprobe binary is an *os.PathError that also wraps fs.ErrNotExist.
A server-side ffprobe problem was therefore reported to clients as a missing
media file. ProbeError now carries an explicit NotFound flag, set only on the
file-access branch, and the handler keys the code off that instead of the chain.

ffprobe can also exit 0 while yielding no audio stream (an audio-suffixed
container holding only video). That parse failure was returned unwrapped, so
clients got "internal error"; it is now wrapped in a ProbeError too.
2026-07-19 18:51:32 -04:00