113 Commits

Author SHA1 Message Date
Deluan
2efa697e52 feat(artwork): import blurhash encoder from #5797 2026-07-22 15:53:32 -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
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
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 Quintão
b38054b29c
perf(artwork): faster image resize + update gen2brain/webp to v0.6.0 (#5652)
* fix(artwork): convert decoded images to a fast-path type before resizing

x/image/draw's CatmullRom scaler only has optimized paths for *image.RGBA,
*image.NRGBA, *image.Gray and *image.YCbCr. Other concrete types — notably
*image.NYCbCrA (from WebP) and *image.Paletted (indexed PNGs) — fall back to
a generic per-pixel At()/RGBA() loop that is several times slower.

Convert such images to *image.RGBA once before scaling; fast-path types are
returned unchanged. This makes resize performance independent of which decoder
wins the image.Decode("webp") registration, and also speeds up indexed PNGs.

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

* chore(deps): update gen2brain/webp to v0.6.0

v0.6.0 replaces the wazero WASM runtime with a self-contained
wasm2go-transpiled WebP decoder/encoder. This drops the webp -> wazero
dependency edge (wazero is still used by the plugin system) and makes the
WASM-only build path (32-bit / nodynamic) faster and far lighter on
allocations.

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

* perf(artwork): defer fast-path conversion until a resize is needed

Move toFastScaleType to just before the CatmullRom.Scale call, after the
no-upscale early return. Previously the conversion ran right after decode, so a
request for a size >= the source dimensions would allocate and walk a full RGBA
copy only to discard it when resizeStaticImage returns nil. The resize path is
unchanged; the no-op path drops ~30-40% time and up to ~79% memory for large
indexed/WebP artwork.

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-06-22 16:33:01 -04:00
Deluan Quintão
c466f6b612
fix(artwork): prevent WebP segfault on 32-bit and disable WebP-by-default in Docker (#5606)
* fix(artwork): avoid WebP segfault on 32-bit ARM

On 32-bit ARM, the gen2brain/webp native libwebp path uses ebitengine/purego
reverse callbacks, which purego does not support on that architecture. Selecting
it crashes the process with a SIGSEGV when encoding or decoding WebP cover art,
taking down the whole server on the first web UI artwork request (issue #5597).

Force the safe WASM path on armv7/v6 in two layers: build the Docker arm binary
with the gen2brain/webp "nodynamic" tag so purego is never linked, and add a
runtime GOARCH guard in the init hook so source builds on 32-bit ARM are also
protected. arm64 keeps the native libwebp path.

* fix(artwork): also disable native WebP on 32-bit x86

purego's callback implementation is built with the constraint !386 && !arm,
so 32-bit x86 (386) crashes with the same SIGSEGV as 32-bit ARM when the native
libwebp path is used. Navidrome ships linux/386 and windows/386 builds, so guard
386 alongside arm: extend the runtime GOARCH check and the Docker nodynamic build
tag to cover both. 64-bit arches keep the native libwebp path.

* fix(artwork): rely on nodynamic build tag, drop ineffective runtime guard

The previous runtime GOARCH guard did not actually prevent the crash:
gen2brain/webp selects the native (purego) vs WASM backend in its own package
init() and registers the purego write callback at import time, before any
Navidrome hook runs. webp.Dynamic() is only a status getter, and Decode/Encode
branch on the library's unexported flag, so the guard merely skipped a log line
while the native path stayed active.

The effective fix is the nodynamic build tag (applied for 32-bit ARM and x86 in
the Dockerfile), which compiles gen2brain/webp WASM-only so purego is never
linked. Drop the misleading guard and document that source builds on 32-bit
architectures must be built with -tags nodynamic.

* fix(artwork): don't enable WebP encoding by default in Docker

The Docker image set ND_ENABLEWEBPENCODING=true, which (a) forced cover-art
thumbnails through WebP for every install and (b) overrode any
EnableWebPEncoding=false set in the user's navidrome.toml, since env vars take
precedence over the config file in Viper.

On 32-bit platforms the only available WebP backend is the WASM encoder, which
is slow on the underpowered hardware those builds typically run on, so enabling
it by default is the wrong tradeoff there. Remove the env default and leave
EnableWebPEncoding off unless the user opts in. Combined with the nodynamic
build tag, 32-bit images neither crash nor pay the WASM cost out of the box.

A smarter automatic policy (use WebP only when native libwebp is available) can
be revisited separately.
2026-06-13 13:58:26 -04:00
Deluan Quintão
af78bdeb3a
fix(artwork): never serve artist folder images as album art (#5596)
* test(artwork): add failing e2e tests for artist image leaking as album art

Reproduces a v0.62.0 regression (#5451/#5457): the album cover-art
parent-folder fallback can include the artist folder, serving the artist
thumbnail (e.g. Artist/folder.jpg) as album art for any album without
image files in its own folder(s). Covers three scenarios: a plain
Artist/Album layout with no album images, a single-disc album spread
across sibling folders under the artist folder, and a spread album whose
own front.jpg is shadowed by the artist's cover.jpg via CoverArtPriority
order. Also adds an albumByName test helper for multi-album layouts.

The tests are expected to fail until the parent-folder inclusion is
gated by a structural check (skip the common parent when audio from
other albums lives under it).

* fix(artwork): never serve artist folder images as album art

The album cover-art parent-folder fallback (introduced in #5451/#5457)
could include the artist folder as a source of album images, serving the
artist thumbnail (e.g. Artist/folder.jpg) as cover art for any album
without image files in its own folder(s). This affected both plain
Artist/Album layouts and single-disc albums spread across sibling
folders under the artist folder.

Gate the common-parent inclusion with a structural check: the parent
only qualifies as an album root when no audio belonging to other albums
lives in it or anywhere beneath it. An artist folder contains other
albums' tracks, while an album root above disc subfolders contains only
this album's, so the check works for any disc folder naming scheme and
never affects the multi-disc fixes from #5376/#5456. A single-album
artist with no images anywhere remains structurally indistinguishable
from an album root and is a known residual case.

* refactor(artwork): move album-root audio check into folder repository

Replace the raw subtree SQL (LIKE/ESCAPE expression and wildcard
escaping) that lived in core/artwork with an explicit
FolderRepository.HasAudioOutsideFolders method, implemented in the
persistence layer next to the existing folder-subtree query pattern.
This also removes the test mock's brittle dispatch that sniffed the
generated SQL to recognize the query; the fake now overrides the new
method directly.

Extract the whole parent-folder resolution from loadAlbumFoldersPaths
into an albumRootParent helper, flattening four levels of nesting back
into a linear flow. Behavior is unchanged; the unit test for a parent
containing audio moved to the persistence suite, with added coverage
for subtree boundaries, missing folders, and LIKE-wildcard escaping in
folder paths.

* refactor(persistence): use exists helper in HasAudioOutsideFolders

Replace the hand-rolled count(*) query with the repository's canonical
exists helper, as suggested in PR review.
2026-06-13 13:29:29 -04:00
Deluan
2a43c4683e chore: go fix
Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-28 22:13:05 -03:00
Deluan Quintão
8f0b4930ff
refactor(conf): replace eager dir creation with lazy Dir type (#5495)
* feat(conf): add Dir type with lazy directory creation

Introduces the Dir type that wraps a directory path string and defers
os.MkdirAll until the first call to Path() or MustPath(), using sync.Once
to ensure the creation happens exactly once. Implements fmt.Stringer,
encoding.TextMarshaler, and encoding.TextUnmarshaler for config integration.
Includes Ginkgo/Gomega tests covering all methods and error paths.

* refactor(conf): replace eager dir creation with lazy Dir type

Change DataFolder, CacheFolder, Plugins.Folder, and Backup.Path from
string to Dir. Remove all os.MkdirAll calls from Load() so directories
are created lazily on first Path()/MustPath() call. Artwork folder
creation was already handled at point-of-use in image_upload.go.

Add SnapshotConfig() to conf package for safe test config save/restore
that avoids copying sync.Once inside Dir fields. Fix copy-lock vet
warning in nativeapi/config.go by marshalling pointer instead of value.

* refactor(conf): migrate tests and db init to lazy Dir type

Update all test files to use conf.NewDir() for Dir field assignments.
Ensure DataFolder is created lazily when the database is first opened
in db.Db(). Remove eager directory creation from conf.Load() tests.

* fix(conf): address review findings for Dir type

- Use os.ModePerm for DataFolder/CacheFolder (was 0700, should match
  original behavior). Add NewDirWithPerm for PluginsFolder (0700).
- Use Path() instead of MustPath() in db.Prune() to avoid logFatal
  from background cron job.
- Panic on marshal/unmarshal errors in SnapshotConfig (test helper).
- Clean up redundant String()/MustPath() calls in plugin manager.
- Remove dead code in dir_test.go.

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

* fix(conf): add GoString to Dir for clean config dump output

Implement fmt.GoStringer on Dir so pretty.Sprintf shows the path
string instead of internal struct fields (sync.Once, perm, err).
Also add TODO comment to configtest about removing the indirection.

* fix(dir): improve error logging in MustPath method

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

* refactor(tests): remove redundant tests for unwritable DataFolder and CacheFolder

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

* fix(conf): address PR review feedback

- Ensure Plugins.Folder always uses 0700, even when user-configured
  (previously only the derived default got restrictive permissions).
- Create LogFile parent directory before opening, so LogFile paths
  inside a not-yet-created DataFolder work correctly.

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-13 17:44:22 -03:00
Deluan Quintão
f48416685f
fix(artwork): fix stale cache and top-level album artwork for multi-disc albums (#5457)
* fix(artwork): include top-level album folders in parent cover art lookup

The Path != "." guard added in #5451 was too aggressive — it excluded
any folder with Path=".", which includes top-level album folders (not
just the library root). Changed to ParentID != "" which correctly
excludes only the actual library root folder.

Fixes #5456

* fix: correct comment in test — album is under library root, not artist root

* test: add ascii tree diagram to top-level album e2e test

* test: replace internal bug references with issue link in e2e comments

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

* test: add e2e test matching reporter's exact library layout (#5456)

Adds a deeply nested test (Genre/Artist/Album/Disc) with 12 discs
using the reporter's actual folder names to verify artwork resolution
works for non-top-level album folders too.

* fix(scanner): use a syntectic admin user when no admin user is found

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

* fix(scanner): bump album UpdatedAt on Phase 3 refresh to invalidate artwork cache

When Phase 3 corrects an album's FolderIDs (or any other field), bump
UpdatedAt to the current time. This ensures the artwork cache key changes,
invalidating any stale artwork that was resolved and cached during Phase 1
when the album had incomplete folder data.

* fix(artwork): include ImportedAt in artwork cache key to invalidate stale cache

Reverts the Phase 3 UpdatedAt bump (which would change album.UpdatedAt
semantics) and instead includes album.ImportedAt in the artwork cache key
computation. Since ImportedAt is bumped to time.Now() on every album Put,
any Phase 3 correction naturally invalidates cached artwork that was
resolved mid-scan with incomplete folder data.

* fix(artwork): simplify lastUpdate logic using TimeNewest utility

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-04 17:26:39 -04:00
Deluan Quintão
a00152397e
fix(artwork): prefer album-root images over disc-subfolder images for multi-disc albums (#5451)
Fixed two bugs in album cover art resolution for multi-disc layouts:

1. compareImageFiles now sorts by path depth (shallower first) when basenames
   tie, so album-root images like Artist/Album/cover.jpg are preferred over
   disc-subfolder images like Artist/Album/CD1/cover.jpg.

2. commonParentFolder now includes the parent folder for single-disc-subfolder
   albums, with a Path != "." guard to avoid pulling artist-folder images.

Closes #5376
2026-05-02 19:48:44 -04:00
Deluan Quintão
5d1c1157b5
refactor(artwork): migrate readers to storage.MusicFS and add e2e suite (#5379)
* test(artwork): add e2e suite documenting album/disc resolution

Adds core/artwork/e2e/ with a real-tempdir + scanner harness that exercises
artwork resolution end-to-end. Covers album and disc kinds; pending (PIt)
cases document two known bugs in reader_album.go for regression-guard
flipping once they are fixed.

* refactor(artwork): add libraryFS helper to resolve MusicFS for a library

* test(artwork): tighten libraryFS test isolation and add scheme-error case

* test(artwork): update libraryFS test description to match implementation

* refactor(artwork): convert fromExternalFile to use fs.FS

Add a temporary fromExternalFileAbs shim so existing absolute-path callers
still compile; the shim is removed once all readers are migrated.

* refactor(artwork): make fromExternalFileAbs a thin delegator

Introduce a minimal osDirectFS adapter so the shim no longer duplicates
the matching loop. Both will be removed in Task 9.

* refactor(artwork): convert fromTag to taglib.OpenStream over fs.FS

Add a temporary fromTagAbs shim so existing absolute-path callers still
compile; removed in Task 9. Reuses the osDirectFS adapter from Task 2.

* refactor(artwork): defer fs.File close until after taglib reads finish

Mirror the lifetime pattern used by adapters/gotaglib/gotaglib.go:
keep the underlying fs.File open until taglib.File is closed, and
pass WithFilename so format detection doesn't rely on content sniffing.

* docs(artwork): note ffmpeg's path-based API limitation

* refactor(artwork): migrate album reader to MusicFS

- Add libFS (storage.MusicFS) field to albumArtworkReader; resolved
  once at construction time via libraryFS()
- Switch fromCoverArtPriority from abs-path shims to FS-based
  fromTag/fromExternalFile; only fromFFmpegTag retains absolute path
- Build imgFiles as library-relative forward-slash paths in
  loadAlbumFoldersPaths using path.Join(f.Path, f.Name, img)
- Guard embedAbs so that an empty EmbedArtPath never produces a
  non-empty absolute path (prevents accidental ffmpeg invocation)
- Register testfile:// storage scheme in artwork test suite to provide
  an os.DirFS-backed MusicFS without requiring the taglib extractor
- Update test assertions from filepath.FromSlash(abs) to bare
  forward-slash relative strings

* fix(artwork): use path package in compareImageFiles for forward-slash relative paths

* refactor(artwork): migrate disc reader to MusicFS

Replace os.Open absolute-path access with libFS.Open on library-relative
forward-slash paths. Rename discFolders→discFoldersRel, split
firstTrackPath into firstTrackRelPath (for fromTag) and firstTrackAbsPath
(for fromFFmpegTag), and switch path.Dir/Base/Ext for forward-slash safety.

* refactor(artwork): build discFoldersRel directly and guard empty first track

* refactor(artwork): migrate mediafile reader to MusicFS

* refactor(artwork): migrate artist album-art lookup to MusicFS

* refactor(artwork): remove temporary path-based shims

All readers now use the FS-based fromTag and fromExternalFile directly,
so the absolute-path adapters and the osDirectFS helper that backed
them can go away.

* test(artwork): rewrite e2e suite to use storagetest.FakeFS

Switches from real-tempdir + local storage to FakeFS via the storage
registry. Adds a proper multi-disc scenario using the disc tag, which
previously required curated MP3 fixtures we did not have.

* test(artwork): use maps.Copy in trackFile tag merge

Lint cleanup: replace the manual map-copy loop flagged by mapsloop.

* test(artwork): reuse tests.MockFFmpeg in e2e harness

Replace the hand-rolled noopFFmpeg stub with tests.NewMockFFmpeg, which
already satisfies the full ffmpeg.FFmpeg interface and won't drift when
new methods are added. Also tie imageBytes to imageFile so they cannot
silently disagree on the on-disk encoding.

* test(artwork): add e2e scenarios from artwork documentation

Covers the behaviors documented at
https://www.navidrome.org/docs/usage/library/artwork/:

- Album: folder.*/front.* fallbacks and priority order with cover.*.
- Disc: cd*.* match, cover.* inside disc folder, DiscArtPriority="" skip
  path, the documented multi-disc layout, and the discsubtitle keyword.
- MediaFile: disc-level fallback for multi-disc tracks and album-level
  fallback for single-disc tracks (doc section "MediaFiles" items 2-3).
- Artist: album/artist.* lookup via libFS (passes). The artist-folder
  branch is XIt-marked because fromArtistFolder still calls os.DirFS
  directly on an absolute path and can't read from a FakeFS-backed
  library — migrating that to storage.MusicFS is a follow-up.

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

* refactor(artwork): scope artist folder traversal to library root

Route fromArtistFolder reads through storage.MusicFS and bound the
parent-directory walk at the library root. This keeps artwork
resolution scoped to the configured library and unblocks FakeFS-backed
e2e scenarios that depend on the artist folder.

Also consolidate the libraryFS + core.AbsolutePath pairing (used by
three readers) into a single libraryFSAndRoot helper.

* test(artwork): add ASCII file-tree diagrams to e2e scenarios

Each It/PIt block now shows the on-disk layout it exercises, with
arrows indicating which file wins (or should win, for the known-bug
PIt cases). Makes scenarios readable at a glance without having to
parse the MapFS map.

* test(artwork): add e2e tests for playlist and radio artwork resolution

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

* test(artwork): enhance e2e tests with real MP3 fixtures for embedded artwork

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

* test(ffmpeg): add support for animated WebP encoder detection and fallback handling

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

* test(artwork): cover additional edge cases in e2e suite

Add high-value scenarios uncovered by the existing specs:

- Album: three-way basename tie (unsuffixed wins), unknown pattern in
  CoverArtPriority is skipped, embedded-first with no embedded art
  falls through.
- Disc: discsubtitle with no matching image falls through.
- Artist: ArtistArtPriority can reach images via album/<pattern>.
- Playlist: generates a 2x2 tiled cover from album art when the playlist
  has no uploaded/sidecar/external image.

New helper realPNG() produces real taglib/image-decodable bytes so the
tiled-cover test can exercise the generator's decode + compose path.

* test(artwork): refactor image upload logic in e2e tests for consistency

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

* test(ffmpeg): simplify animated WebP encoder check by removing context parameter

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

* fix(artwork): normalize rel path for fs.Glob on Windows

filepath.Rel returns backslash-separated paths on Windows, but fs.Glob
and path.Join require forward slashes. Convert with filepath.ToSlash
after computing the relative path and use path.Dir for the parent walk
so the artist-folder lookup works cross-platform.

* fix(ffmpeg): retry animated WebP probe on transient failure

The probe previously used the caller's request context inside sync.Once,
so a single cancelled first request would permanently disable animated
WebP for the rest of the process. Switch to a mutex + probed flag, use
a fresh background context with its own timeout, and only cache the
result when the probe actually succeeds.

* test(ffmpeg): reset ffOnce so ConvertAnimatedImage test is order-independent

The ConvertAnimatedImage stand-in test sets ffmpegPath directly but
does not reset ffOnce. If ffmpegCmd() has not been called earlier in
the test process, the next call inside hasAnimatedWebPEncoder runs
ffOnce.Do and re-resolves the real ffmpeg binary, overwriting the
stand-in and breaking the test. Reset ffOnce and conf.Server.FFmpegPath
alongside the other globals to pin resolution to the stand-in.

* test(artwork): unblock Windows CI — forward-slash fs paths and suite-level DB lifetime

The internal artwork test planted a Windows absolute path (backslashes) into
Folder.Path and then fed it through libFS.Open, which fs.ValidPath rejects.
Rooting the testfile library at the temp dir directly and using
filepath.ToSlash keeps the path model library-relative and forward-slash,
matching production.

The e2e suite opened a per-spec DB in a per-spec TempDir, but the go-sqlite3
singleton kept the file open across specs. Ginkgo's per-spec TempDir cleanup
then tried to unlink a file still held by that handle — fine on POSIX, fails
on Windows. Moving the DB to a suite-level tempdir and closing it in
AfterSuite avoids the race.

* test(artwork): keep Windows drive letters intact in testfile library URLs

url.Parse on `testfile://C:/path` reads `C` as the host and the path loses
the drive letter, so Windows libFS lookups go to `/path` and fail.
testFileLibPath now prepends a `/` when the OS path has no leading slash,
and the testfile constructor strips that extra slash back off before
handing the path to os.Stat / os.DirFS.

* refactor(artwork): consolidate libFS + root into libraryView helper

Collapses the per-reader libFS/libPath/rootFolder/firstTrackAbsPath fields
into a single libraryView{FS, absRoot} with an Abs(rel) method. Also folds
the two library lookups (ds.Library.Get + core.AbsolutePath) into one, and
uses mf.Path directly instead of stripping libRoot off an absolute path.

* refactor(ffmpeg): replace hasAnimatedWebPEncoder with encoderProbe for state management

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

* fix: escape artist folder names in artwork glob

Escape glob metacharacters in the library-relative artist folder path before composing the fs.Glob pattern for artist image lookup. This preserves literal folder names such as Artist [Live] while keeping the configured filename pattern behavior unchanged, and adds a regression test for bracketed artist folders.

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

* fix(artwork): correct test path assertions after MusicFS migration

Source functions (fromTag, fromExternalFile) now return forward-slash
fs.FS-relative paths, so test assertions should compare against plain
forward-slash strings, not filepath.FromSlash(). The artistArtPriority
test needs filepath.FromSlash() on the suffix because findImageInFolder
returns OS-native absolute paths via filepath.Join.

* fix(artwork): normalize path separators in artistArtPriority assertion

The two table entries exercise different code paths: entry 1 goes through
fromArtistFolder (returns OS-native paths via filepath.Join), while entry 2
goes through fromExternalFile (returns forward-slash fs.FS paths). Using
filepath.FromSlash on the expected value only works for entry 1.

Normalize the actual path to forward slashes with filepath.ToSlash so a
single HaveSuffix assertion works for both code paths on all platforms.

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-26 18:16:14 -04:00
Deluan Quintão
a756cad1dc
test: enable artwork tests on Windows (#5416)
* fix(test): enable artwork tests on Windows by using OS-aware path assertions

Replace hardcoded forward-slash path expectations with filepath.FromSlash()
so assertions match OS-native separators on Windows. Removes all 8
SkipOnWindows("#TBD-path-sep-artwork") guards from artwork unit tests.

* test: add comment explaining forward-slash paths in test fixtures
2026-04-26 17:34:39 -04:00
Deluan Quintão
64c8d3f4c5
ci: run Go tests on Windows (#5380)
* ci(windows): add skeleton go-windows job (compile-only smoke test)

* ci(windows): fix comment to reference Task 7 not Task 6

* ci(windows): harden PATH visibility and set explicit bash shell

* ci(windows): enable full go test suite and ndpgen check

* test(gotaglib): skip Unix-only permission tests on Windows

* test(lyrics): skip Windows-incompatible tests

* test(utils): skip Windows-incompatible tests

* test(mpv): skip Windows-incompatible playback tests

Skip 3 subprocess-execution tests that rely on Unix-style mpv
invocation; .bat output includes \r-terminated lines that break
argument parsing (#TBD-mpv-windows).

* test(storage): skip Windows-incompatible tests

Skip relative-path test where filepath.Join uses backslash but the
storage implementation returns a forward-slash URL path
(#TBD-path-sep-storage).

* test(storage/local): skip Windows-incompatible tests

Skip 13 tests that fail because url.Parse("file://" + windowsPath)
treats the drive letter colon as an invalid port; also skip the
Windows drive-letter path test that exposes a backslash vs
forward-slash normalisation bug (#TBD-path-sep-storage-local).

* test(playlists): skip Windows-incompatible tests

* test(model): skip Windows-incompatible tests

* test(model/metadata): skip Windows-incompatible tests

* test(core): skip Windows-incompatible tests

AbsolutePath uses filepath.Join which produces OS-native path separators;
skip the assertion test on Windows until the production code is fixed
(#TBD-path-sep-core).

* test(artwork): skip Windows-incompatible tests

Artwork readers produce OS-native path separators on Windows while tests
assert forward-slash paths; skip 11 affected tests pending a fix in
production code (#TBD-path-sep-artwork).

* test(persistence): skip Windows-incompatible tests

Skip flaky timestamp comparison (#TBD-flake-persistence) and path-separator
real-bugs (#TBD-path-sep-persistence) in FolderRepository.GetFolderUpdateInfo
which uses filepath.Clean/os.PathSeparator converting stored forward-slash paths
to backslashes on Windows.

* test(scanner): skip Windows-incompatible tests

Skip symlink tests (Unix-assumption), ndignore path-separator bugs
(#TBD-path-sep-scanner) in processLibraryEvents/resolveFolderPath where
filepath.Rel/filepath.Split return backslash paths incompatible with fs.FS
forward-slash expectations, error message mismatch on Windows, and file
format upgrade detection (#TBD-path-sep-scanner).

* test(plugins): skip Windows-incompatible tests

Add //go:build !windows tags to test files that reference the suite
bootstrap (testManager, testdataDir, createTestManager) which is only
compiled on non-Windows. Add a Windows-only suite stub that skips all
specs via BeforeEach to prevent [build failed] on Windows CI.

* test(server): skip Windows-incompatible tests

Skip createUnixSocketFile tests that rely on Unix file permission bits
(chmod/fchmod) which are not supported on Windows.

* test(nativeapi): skip Windows-incompatible tests

Skip the i18n JSON validation test that uses filepath.Join to build
embedded-FS paths; filepath.Join produces backslashes on Windows which
breaks fs.Open (embedded FS always uses forward slashes).

* test(e2e): skip Windows-incompatible tests

On Windows, SQLite holds file locks that prevent the Ginkgo TempDir
DeferCleanup from deleting the DB file. Register an explicit db.Close
DeferCleanup (LIFO before TempDir cleanup) on Windows so the file lock
is released before the temp directory is removed.

* test(windows): fix e2e AfterSuite and skip remaining scanner path test

* test(scanner): skip another Windows path-sep test (#TBD-path-sep-scanner)

* test(subsonic): skip timing-flaky test on Windows (#TBD-flake-time-resolution-subsonic)

* test(scanner): skip 'detects file moved to different folder' on Windows

* test(scanner): consolidate 'Library changes' Windows skips into BeforeEach

* test(scanner): close DB before TempDir cleanup to fix Windows file lock

* test(scanner): skip ScanFolders suite on Windows instead of closing shared DB

* ci: retrigger for Windows soak run 2/3

* ci: retrigger for Windows soak run 3/3

* ci: retrigger for Windows soak run 3/3 (take 2)

* test(scanner): skip Multi-Library suite on Windows (SQLite file lock)

* ci(windows): promote go-windows to blocking status check

* test(plugins): run platform-neutral specs on Windows, drop blanket Skip

* test(windows): make tests cross-platform instead of skipping

- subsonic: back-date submissionTime baseline by 1s so
  BeTemporally(">") passes under millisecond clock resolution
- persistence: sleep briefly between Put calls so UpdatedAt is
  strictly after CreatedAt on low-resolution clocks
- utils/files: close tempFile before os.Remove so the test works on
  Windows (where an open handle holds a file lock)
- tests.TempFile: close the handle before returning; metadata tests
  no longer leak the open file into Ginkgo's TempDir cleanup

Resolves Copilot review comments on #5380.

* test(tests): add SkipOnWindows helper to reduce boilerplate

Introduces tests.SkipOnWindows(reason) that wraps the 3-line
runtime.GOOS guard pattern used in every Windows-skipped spec.

* test(adapters): use tests.SkipOnWindows helper

* test(core): use tests.SkipOnWindows helper

* test(model): use tests.SkipOnWindows helper

* test(persistence): use tests.SkipOnWindows helper

* test(scanner): use tests.SkipOnWindows helper

* test(server): use tests.SkipOnWindows helper

* test(plugins): run pure-Go unit tests on Windows

config_validation_test, manager_loader_test, and migrate_test have no
WASM/exec dependencies and don't rely on the make-built test plugins
from plugins_suite_test.go. Let them run on Windows too.
2026-04-19 13:16:47 -04:00
bobo-xxx
28eba567a7
fix(artwork): return correct timestamp when disc or album coverart changes (#5378)
* fix(artwork): return imagesUpdatedAt in LastUpdated when cover art changes

When cover art (cover.jpg) is updated in an album folder, the HTTP
Last-Modified header was incorrectly returning album.UpdatedAt (which
only tracks media file changes) instead of imagesUpdatedAt (which
tracks cover art changes).

This caused browsers to use their cached cover art because the
Last-Modified header didn't change, even though the actual cover art
image data was new (due to cache key changing based on imagesUpdatedAt).

The fix ensures LastUpdated() returns a.lastUpdate (which is the max of
album.UpdatedAt and imagesUpdatedAt) instead of always returning
album.UpdatedAt.

Fixes navidrome/navidrome#5377

* refactor tests

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

* fix(artwork): return imagesUpdatedAt in disc LastUpdated

The discArtworkReader had the same bug as albumArtworkReader (fixed in
9a741859f): LastUpdated() returned album.UpdatedAt while Key() used the
max of album.UpdatedAt and ImagesUpdatedAt. This mismatch caused browsers
to keep stale disc cover art in cache when only the image file changed.

Also strengthen the album LastUpdated tests and add matching tests for
the disc reader. The tests use DescribeTable and were verified to fail
when the fix is reverted.

---------

Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan <deluan@navidrome.org>
2026-04-17 21:35:33 -04:00
Deluan Quintão
de6475bb49
fix(artwork): allow shared disc art from unnumbered filenames in single-folder albums (#5344)
* test(artwork): expect shared disc art for unnumbered filenames in single-folder albums

* fix(artwork): match unnumbered disc art for every disc in single-folder albums

* test(artwork): verify shared disc art resolves for every disc number

* test(artwork): regression guard for numbered disc filter with mixed filenames

* test(artwork): verify DiscArtPriority order decides numbered vs shared disc art

* test(artwork): strengthen regression guard to exercise both disc art branches

* refactor(artwork): simplify disc art matching and drop redundant comments

- Lowercase the pattern and filename once in fromExternalFile and pass
  lowered values into extractDiscNumber, eliminating the duplicate
  strings.ToLower calls inside that helper.
- Drop narrating comments in reader_disc.go and reader_disc_test.go that
  duplicated information already conveyed by nearby code or doc comments.

* fix(artwork): prefer numbered disc art over shared fallback within a pattern

Review feedback: with files [disc.jpg, disc1.jpg, disc2.jpg] in a single
folder, the previous single-folder fall-through returned the first match
in imgFiles order. Because compareImageFiles sorts 'disc' before 'disc1'
and 'disc2', disc.jpg would mask the per-disc numbered files for every
disc, regressing the behavior from before the shared-disc-art change.

Within a single pattern the loop now records the first viable unnumbered
candidate as a fallback and keeps scanning for a numbered match equal to
the target disc. Numbered matches still win immediately; the shared file
is only returned when no numbered match for the target disc exists.

Also drops the redundant strings.ToLower(pattern) at the top of
fromExternalFile; fromDiscArtPriority already lowercases the whole
priority string before splitting, so the function contract is now
'pattern must be lowercase' (documented on the function).

* refactor(artwork): trim disc art matching comments and table-drive tests

Doc comment on fromExternalFile is trimmed to the one non-obvious
contract (caller must pre-lowercase the pattern) plus the headline
behavior; the bulleted restatement of the branch logic went away.
Two inline comments that narrated what the code already shows are
also gone.

Hoisting a `hasWildcard := strings.ContainsRune(pattern, '*')` check
out of the loop avoids per-iteration extractDiscNumber calls for
literal patterns (e.g. `shellac.png`) and lets the loop break as
soon as a viable fallback is found, since literal patterns can never
be beaten by a numbered match. Wildcard patterns keep the original
scan-to-end-for-numbered-match behavior.

The two regression tests added in the previous commit were
structurally identical apart from discNumber/expected, so they are
collapsed into a DescribeTable with two entries — matching the
existing table style used for extractDiscNumber tests in the same
file.

* fix(artwork): support '?' and '[...]' wildcards in disc art patterns

filepath.Match understands three glob metacharacters ('*', '?', '[')
but extractDiscNumber only looked for '*'. A pattern like 'disc?.jpg'
or 'cd[12].jpg' would therefore be treated as unnumbered, and every
disc of a multi-disc album would resolve to the same (first-sorted)
file instead of the per-disc numbered art.

extractDiscNumber now finds the literal prefix of the pattern by
scanning for the first '*', '?', or '[' (via strings.IndexAny),
strips it from the filename, and parses the leading digits that
follow. The standalone filepath.Match check is dropped; HasPrefix
plus the leading-digits requirement is enough to reject non-matches,
and the caller already verifies the glob match before calling.

fromExternalFile's literal-pattern optimization is widened
correspondingly: a pattern is treated as literal only when it
contains none of '*', '?', '['. Any wildcard form now keeps the
scan-to-end behavior so a numbered match can beat a fallback.

Adds table entries for both the extractDiscNumber parser and the
fromExternalFile higher-level behavior, covering '?' and '[...]'
patterns as well as a literal-pattern baseline.

* refactor(artwork): tidy extractDiscNumber after glob-wildcard support

- Name the '*?[' charset as globMetaChars, used by both extractDiscNumber
  and fromExternalFile so the two call sites can't drift.
- Trim the extractDiscNumber doc comment: keep the non-obvious caller
  contract, drop the algorithm narration.
- Replace the byte-slice digit accumulator with a direct filename slice
  fed to strconv.Atoi.
- Rename the four new non-'*' wildcard Entry descriptions so they read
  like the existing extractDiscNumber table ('pattern, target → expected')
  instead of the ambiguous 'disc 1' shorthand.

* fix(artwork): retry remaining fallbacks when the first one fails to open

Review feedback: the previous shape remembered only the first unnumbered
candidate and fell through to a generic error if os.Open failed on it,
even though other matching unnumbered files in imgFiles could have
succeeded. The pre-PR code was more resilient because it looped and
continued on open failure.

fromExternalFile now collects every viable unnumbered candidate into a
slice during the scan, then tries them in order after the loop, mirroring
the pre-PR retry-on-open-failure behavior. Numbered matches still return
immediately on first success and skip the candidate list entirely — an
open failure on a numbered match means no other file has that number
anyway.

Also:
- globMetaChars doc comment now notes that '\' escape is intentionally
  excluded (filepath.Match supports it but treating it as a metachar here
  would misalign extractDiscNumber's literal-prefix extraction with no
  benefit for realistic config patterns).
- The 'cover.jpg doesn't match disc*.*' Entry in the extractDiscNumber
  table is renamed to 'cover.jpg with disc*.* (no prefix match)' to
  reflect that the test now exercises the HasPrefix defensive guard,
  not the removed internal filepath.Match check.

Regression test added: a single-folder album with a deleted first
candidate file resolves to the second candidate.

* fix(artwork): scan all literal-pattern matches so fallback retry works

Review feedback: the 'break on first literal match' optimization
assumed only one file in imgFiles could match a literal basename,
but filepath.Match compares basenames only — multiple folders can
contribute files with the same basename, and the fallback-list retry
in 5d79f751c is defeated if the loop breaks after recording just
the first one.

Removing the break makes literal and wildcard patterns follow the
same scan-to-end path, preserving the retry-on-open-failure
resilience regained in 5d79f751c. The efficiency cost is negligible
— imgFiles is 5-20 entries per album and this is a cache-miss path.
2026-04-11 21:19:57 -04:00
Deluan Quintão
c87db92cee
fix(artwork): address WebP performance regression on low-power hardware (#5286)
* refactor(artwork): rename DevJpegCoverArt to EnableWebPEncoding

Replaced the internal DevJpegCoverArt flag with a user-facing
EnableWebPEncoding config option (defaults to true). When disabled, the
fallback encoding now preserves the original image format — PNG sources
stay PNG for non-square resizes, matching v0.60.3 behavior. The previous
implementation incorrectly re-encoded PNG sources as JPEG in non-square
mode. Also added EnableWebPEncoding to the insights data.

* feat: add configurable UICoverArtSize option

Converted the hardcoded UICoverArtSize constant (600px) into a
configurable option, allowing users to reduce the cover art size
requested by the UI to mitigate slow image encoding. The value is
served to the frontend via the app config and used by all components
that request cover art. Also simplified the cache warmer by removing
a single-iteration loop in favor of direct code.

* style: fix prettier formatting in subsonic test

* feat: log WebP encoder/decoder selection

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

* fix(artwork): address PR review feedback

- Add DevJpegCoverArt to logRemovedOptions so users with the old config
  key get a clear warning instead of a silent ignore.
- Include EnableWebPEncoding in the resized artwork cache key to prevent
  stale WebP responses after toggling the setting.
- Skip animated GIF to WebP conversion via ffmpeg when EnableWebPEncoding
  is false, so the setting is consistent across all image types.
- Fix data race in cache warmer by reading UICoverArtSize at construction
  time instead of per-image, avoiding concurrent access with config
  cleanup in tests.
- Clarify cache warmer docstring to accurately describe caching behavior.

* Revert "fix(artwork): address PR review feedback"

This reverts commit 3a213ef03e401930977138afe0e84c83290df683.

* fix(artwork): avoid data race in cache warmer config access

Capture UICoverArtSize at construction time instead of reading from
conf.Server on each doCacheImage call. The background goroutine could
race with test config cleanup, causing intermittent race detector
failures in CI.

* fix(configuration): clamp UICoverArtSize to be within 200 and 1200

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

* fix(artwork): preserve album cache key compatibility with v0.60.3

Restored the v0.60.3 hash input order for album artwork cache keys
(Agents + CoverArtPriority) so that existing caches remain valid on
upgrade when EnableExternalServices is true. Also ensures
CoverArtPriority is always part of the hash even when external services
are disabled, fixing a v0.60.3 bug where changing CoverArtPriority had
no effect on cache invalidation.

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

* fix: default EnableWebPEncoding to false and reduce artwork parallelism

Changed EnableWebPEncoding default to false so that upgrading users get
the same JPEG/PNG encoding behavior as v0.60.3 out of the box, avoiding
the WebP WASM overhead until native libwebp is available. Users can
opt in to WebP by setting EnableWebPEncoding=true. Also reduced the
default DevArtworkMaxRequests to half the CPU count (min 2) to lower
resource pressure during artwork processing.

* fix(configuration): update DefaultUICoverArtSize to 300

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

* fix(Makefile): append EXTRA_BUILD_TAGS to GO_BUILD_TAGS

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-04 15:17:01 -04:00
Deluan
4030bfe06f fix(artwork): preserve animation for square thumbnails with animated images
Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-01 08:38:29 -04:00
Deluan
420d2c8e5a fix(artwork): validate ffmpeg pipe before returning in cover art fallback
ffmpeg.ExtractImage returns a pipe-based reader immediately, before ffmpeg
finishes processing. When the audio file has no embedded image stream (e.g.
a plain MP3), ffmpeg exits with an error that closes the pipe asynchronously.
The selectImageReader function saw the non-nil reader as a success and
returned it instead of falling through to the next source in the chain
(album art). This caused getCoverArt to return an error response for tracks
on albums where the disc artwork reader was invoked but no embedded art
existed.

Fixed by reading one byte from the pipe to validate the stream delivers
data before returning it. If the read fails, the reader is closed and nil
is returned, allowing the fallback chain to continue to album artwork.

Closes #5265
2026-03-30 07:01:38 -04:00
Deluan
cb396f3dba feat(ui): increase cover art size to 600px and use CatmullRom scaling
Increased the UI cover art request size from 300px to 600px for sharper
images on high-DPI displays. Replaced BiLinear with CatmullRom (bicubic)
interpolation for higher quality image resizing. Extracted the hardcoded
size into a COVER_ART_SIZE constant in the frontend and consolidated
backend sizes into a CacheWarmerImageSizes slice. Removed the unused
UIThumbnailSize constant.

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-22 14:55:14 -04:00
Deluan
f7b60c7952 fix(tests): fix race condition in CacheWarmer pre-cache size test
The test was checking that the buffer was drained before asserting on
cached sizes, but the buffer is cleared before processBatch completes.
Use Eventually on getCachedSizes() directly to properly wait for the
artwork caching to finish.
2026-03-19 13:14:24 -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
00b8fbd789 feat(artwork): add UIThumbnailSize constant and update cache warmer to pre-cache thumbnails
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-18 08:52:52 -04:00
Deluan Quintão
2f5b2b5135
fix(artwork): fallback mediafile cover art to disc artwork before album (#5216)
* fix(artwork): fallback mediafile cover art to disc artwork before album

Changed the mediafile cover art fallback chain to go through disc artwork
before album artwork (mediafile → disc → album). Previously, mediafiles
without embedded art fell back directly to album cover, bypassing any
disc-specific artwork. Renamed AlbumCoverArtID() to DiscCoverArtID() to
encapsulate the disc-vs-album decision in a single method, used by both
CoverArtID() and the mediafile artwork reader.

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

* fix(artwork): fix cache invalidation for mediafile and album cover art

Include imagesUpdatedAt from album folders in the mediafile artwork
reader's cache key, so that when a cover image file changes on disk
(without audio metadata changes) the mediafile cache properly
invalidates. Also include CoverArtPriority unconditionally in the album
artwork reader's cache key hash, so that changing the priority order
with external services disabled correctly invalidates the album cache.

* fix(artwork): skip disc artwork resolution for single-disc albums

Single-disc albums with DiscNumber=1 were unnecessarily routed through
discArtworkReader, which does extra DB queries only to fall through to
album art anyway. Now only multi-disc albums use the disc fallback path.

* refactor(artwork): restore AlbumCoverArtID as a separate method

Extract AlbumCoverArtID back out of DiscCoverArtID so the single-disc
fallback path in reader_mediafile can reference it by name instead of
inlining the artwork ID construction.

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-16 18:08:39 -04:00
Deluan Quintão
ab8a58157a
feat: add artist image uploads and image-folder artwork source (#5198)
* feat: add shared ImageUploadService for entity image management

* feat: add UploadedImage field and methods to Artist model

* feat: add uploaded_image column to artist table

* feat: add ArtistImageFolder config option

* refactor: wire ImageUploadService and delegate playlist file ops to it

Wire ImageUploadService into the DI container and refactor the playlist
service to delegate image file operations (SetImage/RemoveImage) to the
shared ImageUploadService, removing duplicated file I/O logic. A local
ImageUploadService interface is defined in core/playlists to avoid an
import cycle between core and core/playlists.

* feat: artist artwork reader checks uploaded image first

* feat: add image-folder priority source for artist artwork

* feat: cache key invalidation for image-folder and uploaded images

* refactor: extract shared image upload HTTP helpers

* feat: add artist image upload/delete API endpoints

* refactor: playlist handlers use shared image upload helpers

* feat: add shared ImageUploadOverlay component

* feat: add i18n keys for artist image upload

* feat: add image upload overlay to artist detail pages

* refactor: playlist details uses shared ImageUploadOverlay component

* fix: add gosec nolint directive for ParseMultipartForm

* refactor: deduplicate image upload code and optimize dir scanning

- Remove dead ImageFilename methods from Artist and Playlist models
  (production code uses core.imageFilename exclusively)
- Extract shared uploadedImagePath helper in model/image.go
- Extract findImageInArtistFolder to deduplicate dir-scanning logic
  between fromArtistImageFolder and getArtistImageFolderModTime
- Fix fileInputRef in useCallback dependency array

* fix: include artist UpdatedAt in artwork cache key

Without this, uploading or deleting an artist image would not
invalidate the cached artwork because the cache key was only based
on album folder timestamps, not the artist's own UpdatedAt field.

* feat: add Portuguese translations for artist image upload

* refactor: use shared i18n keys for cover art upload messages

Move cover art upload/remove translations from per-entity sections
(artist, playlist) to a shared top-level "message" section, avoiding
duplication across entity types and translation files.

* refactor: move cover art i18n keys to shared message section for all languages

* refactor: simplify image upload code and eliminate redundancies

Extracted duplicate image loading/lightbox state logic from
DesktopArtistDetails and MobileArtistDetails into a shared
useArtistImageState hook. Moved entity type constants to the consts
package and replaced raw string literals throughout model, core, and
nativeapi packages. Exported model.UploadedImagePath and reused it in
core/image_upload.go to consolidate path construction. Cached the
ArtistImageFolder lookup result in artistReader to eliminate a redundant
os.ReadDir call on every artwork request.

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

* style: fix prettier formatting in ImageUploadOverlay

* fix: address code review feedback on image upload error handling

- RemoveImage now returns errors instead of swallowing them
- Artist handlers distinguish not-found from other DB errors
- Defer multipart temp file cleanup after parsing

* fix: enforce hard request size limit with MaxBytesReader for image uploads

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-15 22:19:55 -04:00
Deluan Quintão
69e7d163fc
remove built-in Spotify integration (#5197)
* refactor: remove built-in Spotify integration

Remove the Spotify adapter and all related configuration, replacing
the built-in integration with the plugin system. This deletes the
adapters/spotify package, removes Spotify config options (ID/Secret),
updates the default agents list from "deezer,lastfm,spotify" to
"deezer,lastfm", and cleans up all references across configuration,
metrics, logging, artwork caching, and documentation. Users with
Spotify config options will now see a warning that the options are
no longer available.

* feat: add ListenBrainz to list of default agents

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-15 13:18:54 -04:00
Deluan
d79b812467 refactor(natural): replace maruel/natural with custom natural sort implementation 2026-03-14 10:18:56 -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
a50b2a1e72
feat(artwork): preserve animated image artwork during resize (#5184)
* feat(artwork): preserve animated image artwork during resize

Detect animated GIFs, WebPs, and APNGs via lightweight byte scanning
and preserve their animation when serving resized artwork. Animated GIFs
are converted to animated WebP via ffmpeg with optional downscaling;
animated WebP/APNG are returned as-is since ffmpeg cannot re-encode them.

Adds ConvertAnimatedImage to the FFmpeg interface for piping stdin data
through ffmpeg with animated WebP output.

* fix(artwork): address code review feedback for animated artwork

Fix ReadCloser leak where ffmpeg pipe's Close was discarded by
io.NopCloser wrapping — now preserves ReadCloser semantics when the
resized reader already supports Close. Use uint64 for PNG chunk position
to prevent potential overflow on 32-bit platforms. Add integration tests
for the animation branching logic in resizeImage.
2026-03-13 18:11:12 -04:00
Deluan Quintão
4ddb0774ec
perf(artwork): improve image serving performance with WebP encoding and optimized pipeline (#5181)
* test(artwork): add benchmark helpers for generating test images

* test(artwork): add image decode benchmarks for JPEG/PNG at various sizes

* test(artwork): add image resize benchmarks for Lanczos at various sizes

* test(artwork): add image encode benchmarks for JPEG quality levels and PNG

* test(artwork): add full resize pipeline benchmark (decode+resize+encode)

* test(artwork): add tag extraction benchmark for embedded art

* test(cache): add file cache benchmarks for read, write, and concurrent access

* test(artwork): add E2E benchmarks for artwork.Get with cache on/off and concurrency

* fix(test): use absolute path for tag extraction benchmark fixture

* test(artwork): add resize alternatives benchmark comparing resamplers

* perf(artwork): switch to CatmullRom resampler and JPEG for square images

Replace imaging.Lanczos with imaging.CatmullRom for image resizing
(30% faster, indistinguishable quality at thumbnail sizes). Stop forcing
PNG encoding for square images when the source is JPEG — JPEG is smaller
and faster to encode. Square images from JPEG sources went from 52ms to
10ms (80% improvement). Add sync.Pool for encode buffers to reduce GC
pressure under concurrent load.

* perf(artwork): increase cache warmer concurrency from 2 to 4 workers

Resize is CPU-bound, so more workers improve throughput on multi-core
systems. Doubled worker count to better utilize available cores during
background cache warming.

* perf(artwork): switch to xdraw.ApproxBiLinear and always encode as JPEG

Replace disintegration/imaging with golang.org/x/image/draw for image
resizing. This eliminates ~92K allocations per resize (from imaging's
internal goroutine parallelism) down to ~20, reducing GC pressure under
concurrent load.

Always encode resized artwork as JPEG regardless of source format, since
cover art doesn't need transparency. This is ~5x faster than PNG encode
and produces much smaller output (e.g. 18KB JPEG vs 124KB PNG).

* perf(artwork): skip external API call when artist image URL is cached

ArtistImage() was always calling the external agent (Spotify/Last.fm)
to get the image URL, even when the artist already had URLs stored in
the database. This caused every artist image request to block on an
external API call, creating severe serialization when loading artist
grids (5-20 seconds for the first page).

Now use the stored URL directly when available. Artists with no stored
URL still fetch synchronously. Background refresh via UpdateArtistInfo
handles TTL-based URL updates.

* perf(artwork): increase getCoverArt throttle from NumCPU/3 to NumCPU

The previous default of max(2, NumCPU/3) was too aggressive for artist
images which are I/O-bound (downloading from external CDNs), not
CPU-bound. On an 8-core machine this meant only 2 concurrent requests,
causing a staircase pattern where 12 images took ~2.4s wall-clock.

Bumping to max(4, NumCPU) cuts wall-clock time by ~50% for artist image
grids while still preventing unbounded concurrency for CPU-bound resizes.

* perf(artwork): encode resized images as WebP instead of JPEG

Switch from JPEG to WebP encoding for resized artwork using gen2brain/webp
(libwebp via WASM, no CGo). WebP produces ~74% smaller output at the same
quality with only ~25% slower full-pipeline encode time (cached, so only
paid once per artwork+size).

Use NRGBA image type to preserve alpha channel in WebP output, and
transparent padding for square canvas instead of black.

Also removes the disintegration/imaging dependency entirely by replacing
imaging.Fill in playlist tile generation with a custom fillCenter function
using xdraw.ApproxBiLinear.

* perf(artwork): switch from ApproxBiLinear to BiLinear scaling for improved image processing

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

* refactor(configuration): rename CoverJpegQuality to CoverArtQuality and update references

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

* feat(artwork): add DevJpegCoverArt option to control JPEG encoding for cover art

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

* fix(artwork): remove redundant transparent fill and handle encode errors in resizeImage

Removed a no-op draw.Draw call that filled the NRGBA canvas with
transparent pixels — NewNRGBA already zero-initializes to fully
transparent. Also added an early return on encode failure to avoid
allocating and copying potentially corrupt buffer data before returning
the error.

* fix(configuration): reorder default agents (deezer is faster)

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

* fix(test): resolve dogsled lint warning in tag extraction benchmark

Use all return values from runtime.Caller instead of discarding three
with blank identifiers, which triggered the dogsled linter.

* fix(artwork): revert cache key format

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

* fix(configuration): remove deprecated CoverJpegQuality field and update references to CoverArtQuality

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-13 09:35:59 -04:00
Deluan Quintão
a25306f2c1
fix(artwork): search parent folders for album cover art in multi-disc layouts (#5157)
* fix(artwork): search parent folders for album cover art in multi-disc layouts

When albums have tracks in subdirectories (e.g., CD1/, CD2/), Navidrome
only searched those subdirectories for cover images. This meant cover art
placed in the album's root folder (e.g., "Artist/Album/cover.jpg") was
not found. Now loadAlbumFoldersPaths also queries parent folders of the
album's media folders, so cover art in the album root is discovered.

* fix(artwork): simplify parent folder detection for album cover art lookup

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

* fix(album): propagate non-ErrNotFound errors from parent folder lookup

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-09 10:52:13 -04:00
Deluan
1ce561cc8e refactor(server): remove legacy embedded coverart logic
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-05 19:53:59 -05:00
Deluan Quintão
acd69f6a4f
feat(playlist): support #EXTALBUMARTURL directive and sidecar images (#5131)
* feat(playlist): add migration for playlist image field rename and external URL

* refactor(playlist): rename ImageFile to UploadedImage and ArtworkPath to UploadedImagePath

Rename playlist model fields and methods for clarity in preparation for
adding external image URL and sidecar image support. Add the new
ExternalImageURL field to the Playlist model.

* feat(playlist): parse #EXTALBUMARTURL directive in M3U imports

* feat(playlist): always sync ExternalImageURL on re-scan, preserve UploadedImage

* feat(artwork): add sidecar image discovery and cache invalidation for playlists

Add playlist sidecar image support to the artwork reader fallback chain.
A sidecar image (e.g., MyPlaylist.jpg next to MyPlaylist.m3u) is discovered
via case-insensitive base name matching using model.IsImageFile(). Cache
invalidation uses max(playlist.UpdatedAt, imageFile.ModTime()) to bust
stale artwork when sidecar or ExternalImageURL local files change.

* feat(artwork): add external image URL source to playlist artwork reader

Add fromPlaylistExternalImage source function that resolves playlist
cover art from ExternalImageURL, supporting both HTTP(S) URLs (via
the existing fromURL helper) and local file paths (via os.Open).
Insert it in the Reader() fallback chain between sidecar and tiled cover.

* refactor(artwork): simplify playlist artwork source functions

Extract shared fromLocalFile helper, use url.Parse for scheme check,
and collapse sidecar directory scan conditions.

* test(artwork): remove redundant fromPlaylistSidecar tests

These tests duplicated scenarios already covered by findPlaylistSidecarPath
tests combined with fromLocalFile (tested via fromPlaylistExternalImage).
After refactoring fromPlaylistSidecar to a one-liner composing those two
functions, the wrapper tests add no value.

* fix(playlist): address security review comments from PR #5131:

- Use url.PathUnescape instead of url.QueryUnescape for file:// URLs so
  that '+' in filenames is preserved (not decoded as space).
- Validate all local image paths (file://, absolute, relative) against
  known library boundaries via libraryMatcher, rejecting paths outside
  any configured library.
- Harden #EXTALBUMARTURL against path traversal and SSRF by adding EnableM3UExternalAlbumArt config flag (default false, also
  disabled by EnableExternalServices=false) to gate HTTP(S) URL storage
  at parse time and fetching at read time (defense in depth).
- Log a warning when os.ReadDir fails in findPlaylistSidecarPath for
  diagnosability.
- Extract resolveLocalPath helper to simplify resolveImageURL.

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

* feat(playlist): implement human-friendly filename generation for uploaded playlist cover images

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-02 11:39:59 -05:00
adrbn
d004f99f8f
feat(playlist): add custom playlist cover art upload (#5110)
* feat(playlist): add custom playlist cover art upload - #406

Allow users to upload, view, and remove custom cover images for playlists.
Custom images take priority over the auto-generated tiled artwork.

Backend:
- Add `image_path` column to playlist table (migration with proper rollback)
- Add `SetImage`/`RemoveImage` methods to playlist service
- Add `POST/DELETE /api/playlist/{id}/image` endpoints
- Prioritize custom image in artwork reader pipeline
- Clean up image files on playlist deletion
- Use glob-based cleanup to prevent orphaned files across format changes
- Reject uploads with undetermined image type (400)

Frontend:
- Hover overlay on playlist cover with upload (camera) and remove (trash) buttons
- Lightbox for full-size cover art viewing
- Cover art thumbnails in the playlist list view
- Loading/error states and i18n strings

Closes #406

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: adrbn <128328324+adrbn@users.noreply.github.com>

* refactor: rename playlist image path migration file

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

* fix(playlist): address review feedback for cover art upload - #406

- Use httpClient instead of raw fetch for image upload/remove
- Revert glob cleanup to simple imagePath check
- Add log.Error before all error HTTP responses
- Add backend tests for SetImage and RemoveImage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: adrbn <128328324+adrbn@users.noreply.github.com>

* refactor(playlist): use Playlist.ArtworkPath() for image storage

Migrate all playlist image path handling to use the new
Playlist.ArtworkPath() method as the single source of truth. The DB now
stores only the filename (e.g. "pls-1.jpg") instead of a relative path,
and images are stored under {DataFolder}/artwork/playlist/ instead of
{DataFolder}/playlist_images/. The artwork root directory is created at
startup alongside DataFolder and CacheFolder. This also removes the
conf dependency from reader_playlist.go since path resolution is now
fully encapsulated in the model.

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

* refactor(playlist): streamline artwork image selection logic

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

* refactor: move translation keys, add pt-BR translations

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

* refactor(playlist): rename image_path to image_file

Rename the playlist cover art column and field from image_path/ImagePath
to image_file/ImageFile across the migration, model, service, tests, and
UI. The new name more accurately describes what the field stores (a
filename, not a path) and aligns with the existing ImageFiles/IsImageFile
naming conventions in the codebase.

---------

Signed-off-by: adrbn <128328324+adrbn@users.noreply.github.com>
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2026-03-01 14:07:18 -05:00
Deluan
5fa8356b31 chore(deps): bump golangci-lint to v2.10.0 and suppress new gosec false positives
Bump golangci-lint from v2.9.0 to v2.10.0, which includes a newer gosec
with additional taint-analysis rules (G117, G703, G704, G705) and a
stricter G101 check. Added inline //nolint:gosec comments to suppress
21 false positives across 19 files: struct fields flagged as secrets
(G117), w.Write calls flagged as XSS (G705), HTTP client calls flagged
as SSRF (G704), os.Stat/os.ReadFile/os.Remove flagged as path traversal
(G703), and a sort mapping flagged as hardcoded credentials (G101).

Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-17 09:28:42 -05:00
Maximilian
a704e86ac1
refactor: run Go modernize (#5002) 2026-02-08 09:57:30 -05:00
Deluan
77367548f6 fix(artwork): clamp requested square size to original dimensions for cover art, to avoid upscaling
Signed-off-by: Deluan <deluan@navidrome.org>
2026-01-28 12:46:46 -05:00
Deluan
66474fc9f4 feat: add support for reading embedded images using taglib by default
Signed-off-by: Deluan <deluan@navidrome.org>
2026-01-18 22:14:21 -05:00
Deluan Quintão
03a45753e9
feat(plugins): New Plugin System with multi-language PDK support (#4833)
* chore(plugins): remove the old plugins system implementation

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

* feat(plugins): implement new plugin system with using Extism

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

* feat(plugins): add capability detection for plugins based on exported functions

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

* feat(plugins): add auto-reload functionality for plugins with file watcher support

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

* feat(plugins): add auto-reload functionality for plugins with file watcher support

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

* refactor(plugins): standardize variable names and remove superfluous wrapper functions

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

* fix(plugins): improve error handling and logging in plugin manager

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

* refactor(plugins): implement plugin function call helper and refactor MetadataAgent methods

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

* fix(plugins): race condition in plugin manager

* tests(plugins): change BeforeEach to BeforeAll in MetadataAgent tests

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

* tests(plugins): optimize tests

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

* tests(plugins): more optimizations

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

* test(plugins): ignore goroutine leaks from notify library in tests

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

* feat(plugins): add Wikimedia plugin for Navidrome to fetch artist metadata

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

* feat(plugins): enhance plugin logging and set User-Agent header

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

* feat(plugins): implement scrobbler plugin with authorization and scrobbling capabilities

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

* feat(plugins): integrate logs

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

* refactor(plugins): clean up manifest struct and improve plugin loading logic

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

* feat(plugins): add metadata agent and scrobbler schemas for bootstrapping plugins

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

* feat(hostgen): add hostgen tool for generating Extism host function wrappers

- Implemented hostgen tool to generate wrappers from annotated Go interfaces.
- Added command-line flags for input/output directories and package name.
- Introduced parsing and code generation logic for host services.
- Created test data for various service interfaces and expected generated code.
- Added documentation for host services and annotations for code generation.
- Implemented SubsonicAPI service with corresponding generated code.

* feat(subsonicapi): update Call method to return JSON string response

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

* feat(plugins): implement SubsonicAPI host function integration with permissions

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

* fix(generator): error-only methods in response handling

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

* feat(plugins): generate client wrappers for host functions

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

* refactor(generator): remove error handling for response.Error in client templates

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

* feat(scheduler): add Scheduler service interface with host function wrappers for scheduling tasks

* feat(plugins): add WASI build constraints to client wrapper templates, to avoid lint errors

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

* feat(scheduler): implement Scheduler service with one-time and recurring scheduling capabilities

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

* refactor(manifest): remove unused ConfigPermission from permissions schema

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

* feat(scheduler): add scheduler callback schema and implementation for plugins

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

* refactor(scheduler): streamline scheduling logic and remove unused callback tracking

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

* refactor(scheduler): add Close method for resource cleanup on plugin unload

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

* docs(scheduler): clarify SchedulerCallback requirement for scheduling functions

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

* fix: update wasm build rule to include all Go files in the directory

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

* feat: rewrite the wikimedia plugin using the XTP CLI

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

* refactor(scheduler): replace uuid with id.NewRandom for schedule ID generation

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

* refactor: capabilities registration

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

* test: add scheduler service isolation test for plugin instances

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

* refactor: update plugin manager initialization and encapsulate logic

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

* feat: add WebSocket service definitions for plugin communication

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

* feat: implement WebSocket service for plugin integration and connection management

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

* feat: add Crypto Ticker example plugin for real-time cryptocurrency price updates via Coinbase WebSocket API

Also add the lifecycle capability

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

* fix: use context.Background() in invokeCallback for scheduled tasks

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

* refactor: rename plugin.create() to plugin.instance()

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

* refactor: rename pluginInstance to plugin for consistency across the codebase

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

* refactor: simplify schedule cloning in Close method and enhance plugin cleanup error handling

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

* feat: implement Artwork service for generating artwork URLs in Navidrome plugins - WIP

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

* refactor: moved public URL builders to avoid import cycles

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

* feat: add Cache service for in-memory TTL-based caching in plugins

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

* feat: add Discord Rich Presence example plugin for Navidrome integration

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

* refactor: host function wrappers to use structured request and response types

- Updated the host function signatures in `nd_host_artwork.go`, `nd_host_scheduler.go`, `nd_host_subsonicapi.go`, and `nd_host_websocket.go` to accept a single parameter for JSON requests.
- Introduced structured request and response types for various cache operations in `nd_host_cache.go`.
- Modified cache functions to marshal requests to JSON and unmarshal responses, improving error handling and code clarity.
- Removed redundant memory allocation for string parameters in favor of JSON marshaling.
- Enhanced error handling in WebSocket and cache operations to return structured error responses.

* refactor: error handling in various plugins to convert response.Error to Go errors

- Updated error handling in `nd_host_scheduler.go`, `nd_host_websocket.go`, `nd_host_artwork.go`, `nd_host_cache.go`, and `nd_host_subsonicapi.go` to convert string errors from responses into Go errors.
- Removed redundant error checks in test data plugins for cleaner code.
- Ensured consistent error handling across all plugins to improve reliability and maintainability.

* refactor: rename fake plugins to test plugins for clarity in integration tests

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

* feat: add help target to Makefile for plugin usage instructions

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

* feat: add Cover Art Archive plugin as an example of Python plugin

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

* feat: update Makefile and README to clarify Go plugin usage

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

* feat: include plugin capabilities in loading log message

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

* feat: add trace logging for plugin availability and error handling in agents

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

* feat: add Now Playing Logger plugin to showcase calling host functions from Python plugins

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

* feat: generate Python client wrappers for various host services

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

* feat: add generated host function wrappers for Scheduler and SubsonicAPI services

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

* feat: update Python plugin documentation and usage instructions for host function wrappers

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

* feat: add Webhook Scrobbler plugin in Rust to send HTTP notifications on scrobble events

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

* feat: enable parallel loading of plugins during startup

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

* docs: update README to include WebSocket callback schema in plugin documentation

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

* feat: extend plugin watcher with improved logging and debounce duration adjustment

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

* add trace message for plugin recompiles

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

* feat: implement plugin cache purging functionality

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

* test: move purgeCacheBySize unit tests

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

* feat(plugins UI): add plugin repository and database support

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

* feat(plugins UI): add plugin management routes and middleware

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

* feat(plugins UI): implement plugin synchronization with database for add, update, and remove actions

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

* feat(plugins UI): add PluginList and PluginShow components with plugin management functionality

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

* feat(plugins): optimize plugin change detection

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

* refactor(plugins UI): improve PluginList structure

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

* feat(plugins UI): enhance PluginShow with author, website, and permissions display

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

* feat(plugins UI): refactor to use MUI and RA components

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

* feat(plugins UI): add error handling for plugin enable/disable actions

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

* refactor(plugins): inject PluginManager into native API

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

* refactor(plugins): update GetManager to accept DataStore parameter

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

* feat(plugins): add subsonicRouter to Manager and refactor host service registration

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

* refactor(plugins): enhance debug logging for plugin actions and recompile logic

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

* refactor(plugins): break manager.go into smaller, focused files

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

* refactor(plugins): streamline error handling and improve plugin retrieval logic

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

* refactor(plugins): update newWebSocketService to use WebSocketPermission for allowed hosts

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

* refactor(plugins): introduce ToggleEnabledSwitch for managing plugin enable/disable state

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

* docs: update READMEs

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

* feat(library): add Library service for metadata access and filesystem integration

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

* feat(plugins): add Library Inspector plugin for periodic library inspection and file size logging

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

* docs: update README to reflect JSON configuration format for plugins

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

* fix(build): update target to wasm32-wasip1 for improved WASI support

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

* feat(plugins): implement configuration management UI with key-value pairs support

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

* feat(ui): adjust grid layout in InfoRow component for improved responsiveness

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

* feat(plugins): rename ErrorIndicator to EnabledOrErrorField and enhance error handling logic

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

* feat(i18n): add Portuguese translations for plugin management and notifications

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

* feat(plugins): add support for .ndp plugin packages and update build process

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

* docs: update README for .ndp plugin packaging and installation instructions

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

* feat(plugins): implement KVStore service for persistent key-value storage

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

* docs: enhance README with Extism plugin development resources and recommendations

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

* feat(plugins): integrate event broker into plugin manager

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

* feat(plugins): update config handling in PluginShow to track last record state

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

* feat(plugins): add Rust host function library and example implementation of Discord Rich Presence plugin in Rust

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

* feat(plugins): generate Rust lib.rs file to expose host function wrappers

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

* refactor(plugins): update JSON field names to camelCase for consistency

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

* refactor: reduce cyclomatic complexity by refactoring main function

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

* feat(plugins): enhance Rust code generation with typed struct support and improved type handling

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

* feat(plugins): add Go client library with host function wrappers and documentation

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

* feat(plugins): generate Go client stubs for non-WASM platforms

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

* feat(plugins): update client template file names for consistency

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

* feat(plugins): add initial implementation of the Navidrome Plugin Development Kit code generator - Pahse 1

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

* feat(plugins): implementation of the Navidrome Plugin Development Kit with generated client wrappers and service interfaces - Phase 2

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

* feat(plugins): implementation of the Navidrome Plugin Development Kit with generated client wrappers and service interfaces - Phase 2 (2)

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

* feat(plugins): implementation of the Navidrome Plugin Development Kit with generated client wrappers and service interfaces - Phase 3

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

* feat(plugins): implementation of the Navidrome Plugin Development Kit with generated client wrappers and service interfaces - Phase 4

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

* feat(plugins): implementation of the Navidrome Plugin Development Kit with generated client wrappers and service interfaces - Phase 5

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

* refactor(plugins): consistent naming/types across PDK

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

* refactor(plugins): streamline plugin function signatures and error handling

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

* refactor(plugins): update scrobbler interface to return errors directly instead of response structs

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

* test: make all test plugins use the PDK

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

* refactor(plugins): reorganize and sort type definitions for consistency

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

* refactor(plugins): update error handling for methods to return errors directly

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

* refactor(plugins): update function signatures to return values directly instead of response structs

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

* refactor(plugins): update request/response types to use private naming conventions

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

* build: mark .wasm files as intermediate for cleanup after building .ndp

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

* refactor(plugins): consolidate PDK module path and update Go version to 1.25

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

* feat: implement Rust PDK

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

* refactor(plugins): reorganize Rust output structure to follow standard conventions

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

* refactor(plugins): update Discord Rich Presence and Library Inspector plugins to use nd-pdk for service calls and implement lifecycle management

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

* refactor(plugins): update macro names for websocket and metadata registration to improve clarity and consistency

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

* refactor(plugins): rename scheduler callback methods for consistency and clarity

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

* refactor(plugins): update export wrappers to use `//go:wasmexport` for WebAssembly compatibility

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

* docs: update plugin registration docs

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

* fix(plugins): generate host wrappers

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

* test(plugins): conditionally run goleak checks based on CI environment

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

* docs: update README to reflect changes in plugin import paths

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

* refactor(plugins): update plugin instance creation to accept context for cancellation support

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

* fix(plugins): update return types in metadata interfaces to use pointers

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

* fix(plugins): enhance type handling for Rust and XTP output in capability generation

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

* fix(plugins): update IsAuthorized method to return boolean instead of response object

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

* test(plugins): add unit tests for rustOutputType and isPrimitiveRustType functions

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

* feat(plugins): implement XTP JSONSchema validation for generated schemas

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

* fix(plugins): update response types in testMetadataAgent methods to use pointers

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

* docs: update Go and Rust plugin developer sections for clarity

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

* docs: correct example link for library inspector in README

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

* docs: clarify artwork URL generation capabilities in service descriptions

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

* docs: update README to include Rust PDK crate information for plugin developers

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

* fix: handle URL parsing errors and use atomic upsert in plugin repository

Added proper error handling for url.Parse calls in PublicURL and AbsoluteURL
functions. When parsing fails, PublicURL now falls back to AbsoluteURL, and
AbsoluteURL logs the error and returns an empty string, preventing malformed
URLs from being generated.

Replaced the non-atomic UPDATE-then-INSERT pattern in plugin repository Put
method with a single atomic INSERT ... ON CONFLICT statement. This eliminates
potential race conditions and improves consistency with the upsert pattern
already used in host_kvstore.go.

* feat: implement mock service instances for non-WASM builds using testify/mock

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

* refactor: Discord RPC struct to encapsulate WebSocket logic

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

* feat: add support for experimental WebAssembly threads

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

* feat: add PDK abstraction layer with mock support for non-WASM builds

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

* feat: add unit tests for Discord plugin and RPC functionality

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

* fix: update return types in minimalPlugin and wikimediaPlugin methods to use pointers

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

* fix: context cancellation and implement WebSocket callback timeout for improved error handling

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

* feat: conditionally include error handling in generated client code templates

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

* feat: implement ConfigService for plugin configuration management

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

* feat: enhance plugin manager to support metrics recording

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

* refactor: make MockPDK private

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

* refactor: update interface types to use 'any' in plugin repository methods

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

* refactor: rename List method to Keys for clarity in configuration management

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

* test: add ndpgen plugin tests in the pipeline and update Makefile

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

* feat: add users permission management to plugin system

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

* refactor: streamline users integration tests and enhance plugin user management

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

* refactor: remove UserID from scrobbler request structure

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

* test: add integration tests for UsersService enable gate behavior

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

* feat: implement user permissions for SubsonicAPI and scrobbler plugins

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

* fix: show proper error in the UI when enabling a plugin fails

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

* feat: add library permission management to plugin system

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

* feat: add user permission for processing scrobbles in Discord Rich Presence plugin

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

* fix: implement dynamic loading for buffered scrobbler plugins

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

* feat: add GetAdmins method to retrieve admin users from the plugin

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

* feat: update Portuguese translations for user and library permissions

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

* reorder migrations

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

* fix: remove unnecessary bulkActionButtons prop from PluginList component

* feat: add manual plugin rescan functionality and corresponding UI action

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

* feat: implement user/library and plugin management integration with cleanup on deletion

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

* feat: replace core mock services with test-specific implementations to avoid import cycles

* feat: add ID fields to Artist and Song structs and enhance track loading logic by prioritizing ID matches

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

* feat: update plugin permissions from allowedHosts to requiredHosts for better clarity and consistency

* feat: refactor plugin host permissions to use RequiredHosts directly for improved clarity

* fix: don't record metrics for plugin calls that aren't implemented at all

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

* fix: enhance connection management with improved error handling and cleanup logic

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

* feat: introduce ArtistRef struct for better artist representation and update track metadata handling

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

* feat: update user configuration handling to use user key prefix for improved clarity

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

* feat: enhance ConfigCard input fields with multiline support and vertical resizing

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

* fix: rust plugin compilation error

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

* feat: implement IsOptionPattern method for better return type handling in Rust PDK generation

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-01-14 19:22:48 -05:00
Deluan
bca76069c3 fix(server): prioritize artist base image filenames over numeric suffixes and add tests for sorting
Signed-off-by: Deluan <deluan@navidrome.org>
2025-11-14 13:15:50 -05:00