Commit graph

485 commits

Author SHA1 Message Date
Deluan Quintão
c22ce9ebb2
feat(api): add the API v1 foundation behind DevAPIv1 (#6227)
* feat(api): add OpenAPI v1 spec skeleton, lint ruleset and bundle tooling

vacuum v0.30.6's `bundle --composed` mangles component names for this
spec's multi-file layout (duplicates Problem as Problem__schemas etc.),
so api-bundle uses the Redocly CLI (npx @redocly/cli bundle) instead.

* fix(api): pin the Redocly CLI version

Tried moving components out of the root document (per libopenapi's
nested_files example) so vacuum's own bundler could produce clean
names, but any component declared via $ref inside components.* still
gets a __<parent>-suffixed twin regardless of collisions elsewhere, so
vacuum's --composed bundler can't cleanly bundle this spec. Pin the
already-working Redocly fallback to an exact version instead of
@latest.

* fix(api): bundle the OpenAPI spec with vacuum

vacuum's --composed bundler suffixes any component reached via a $ref
written directly inside the root document's own components.* block,
regardless of collisions elsewhere. Dropping the root-level schemas/
parameters/responses declarations (keeping only securitySchemes, and
leaving every component file under api/openapi/components/ untouched)
lets vacuum bundle cleanly with no __ suffixes, going back to Go-only
tooling. Components nothing references yet (ListMeta, offset, limit,
BadRequest, Unauthorized, Forbidden, NotFound) are absent from the
bundle until a later task's operation references them.

* fix(api): make spec lint rules cover all schemas and error codes

nd-schema-property-descriptions targeted $.components.schemas, but our
schemas live in path/response files, not the root document, so it was
dead code; switched to $..properties[*] to walk every resolved schema
wherever it ends up. nd-error-responses-are-problems only checked a
hardcoded status-code list; switched to a patternProperties schema
matching the full 4xx/5xx range. Also: api-diff now diffs against the
merge-base with API_DIFF_BASE (falling back to its tip with a notice
if no merge-base exists), gen no longer depends on api-gen until Task
3 wires up oapi-codegen, and api-lint suppresses vacuum's banner.

* feat(api): embed the bundled OpenAPI spec and expose its version

* feat(api): generate the v1 server interface with oapi-codegen

* feat(api): add RFC 9457 problem responses for API v1

* feat(api): add API v1 router with /server discovery and spec routes

* fix(api): serve the OpenAPI document without range support

* feat(api): mount API v1 behind the DevAPIv1 flag

* chore(ci): lint, regenerate and diff the OpenAPI v1 spec

* refactor(api): tighten spec version access, lint rules and test naming

* refactor(api): simplify spec routes, tests and OpenAPI tooling

Share one If-None-Match parser (utils/req) between the image and spec
routes, declare the YAML spec response as an object so tests need no
decoder override, and reuse ETag/304 spec components.

Install the OpenAPI tools only when missing or at a different version,
fail api-diff when its base ref does not exist, and in CI cache the
tools, fold regeneration into the go generate check, and fetch only the
PR base commit for the breaking-change gate.

* refactor(api): raise the list limit maximum to 2000 and drop the flag test

* feat(api): treat added enum values as non-breaking

Enums in API v1 are open: clients must accept unknown values. api-diff
now downgrades response-property-enum-value-added to INFO, while
removing a value from a request enum stays breaking.

* feat(api): gate breaking changes on x-stability-level

Every operation declares x-stability-level (alpha, beta, stable). oasdiff
ignores breaking changes to alpha operations and rejects lowering a
level, so unreleased endpoints can evolve while beta and stable ones
stay additive. All current operations start as alpha.

* feat(api): declare loginMethods as an enum

Prefix generated enum constants with their type name so enums sharing a
value (for example password) cannot collide in package apiv1.

* feat(api): send Allow on 405 and answer HEAD wherever GET is routed

chi only sets Allow in its default 405 handler, so the problem-format
handler now builds it by matching each method against the v1 router.
HEAD requests fall back to the GET route, as RFC 9110 expects.

* refactor(api): hash the spec ETag with xxh3

The bytes are compiled in, and the digest was truncated to 64 bits
anyway, so this matches the artwork ETags instead of paying for
cryptographic strength we discard.

* docs(api): explain the about:blank problem type

* feat(api): make code the problem identifier and omit a blank type

RFC 9457 says clients switch on the type URI, but no adopter surveyed
ships both a populated type and a separate code. Declare code as an
enum, and send type only once a problem has semantics of its own.

* fix(api): advertise the configured base path in the served OpenAPI spec

With BaseURL=/music the API is mounted at /music/api/v1, but the spec
told clients to call /api/v1 at the host root. The server now rewrites
servers[0].url to BasePath + /api/v1 when it serves the document.

Relative server URLs were tested first: "." and "../v1" work in
openapi-generator, Swagger UI and Redoc, but Scalar resolves them
against the page origin, so it breaks even without a base path. The
committed bundle keeps /api/v1, and a test pins that it appears exactly
once, which the rewrite relies on.
2026-09-26 15:27:23 -04:00
Deluan
6b05189190 chore: update Go dependencies to latest versions 2026-09-25 18:18:58 -04:00
Deluan Quintão
b293b96256
refactor(persistence): stateless repositories with per-call context (#6149)
* refactor(persistence): adopt generic deluan/rest repository API

Pin deluan/rest to the refactor branch. REST-facing repository methods
take a context and return typed values. Drop DataStore.Resource and
ResourceRepository; the native API names typed repositories directly
through a per-request adapter that later commits remove.

* refactor(persistence): base repository helpers take a context

* refactor(persistence): LibraryRepository takes a context per call

* refactor(persistence): PropertyRepository takes a context per call

* refactor(persistence): UserPropsRepository takes a context per call

* refactor(persistence): TranscodingRepository takes a context per call

* refactor(persistence): ShareRepository takes a context per call

* refactor(persistence): PlayerRepository takes a context per call

* refactor(persistence): RadioRepository takes a context per call

* refactor(persistence): PlayQueueRepository takes a context per call

* refactor(persistence): Tag and Genre repositories take a context per call

* refactor(persistence): PluginRepository takes a context per call

* refactor(persistence): Scrobble repositories take a context per call

* refactor(persistence): FolderRepository takes a context per call

* refactor(persistence): Artwork repositories take a context per call

* refactor(persistence): UserRepository takes a context per call

* refactor(persistence): ArtistRepository takes a context per call

ReadAll no longer rewrites the shared sort mappings for the role filter;
it works on a per-call copy.

* test(persistence): assert artist role sort sanitization in ReadAll

* refactor(persistence): AlbumRepository takes a context per call

* test(persistence): pass the test context to album repository helpers

* refactor(persistence): MediaFileRepository takes a context per call

* refactor(persistence): Playlist repositories take a context per call

* refactor(persistence): build all repositories once per store

* refactor(core): REST repository wrappers are built once

* refactor(persistence): repositories are stateless

Remove the context field from the base repository and the per-request
REST adapter. Enable the containedctx linter so no repository can hold a
request context again.

* chore(lint): skip containedctx in test files

* refactor: share simplifications from the stateless repositories sweep

Add deleteOwnedAll on sqlRepository and use it in player/share Delete
to remove the duplicated bulk-delete loop; have Share.Repository()
return model.ShareRepository so subsonic sharing.go drops its repeated
type assertions.

* chore(core): assert REST wrappers implement Persistable

* chore: reformat imports

* perf(persistence): build repositories on first use

Each transaction store used to construct all 21 repositories up front,
paying for filter and sort mapping setup the block never touched. Fields
are now sync.OnceValue thunks, so a store only builds what it uses.

* fix(persistence): clean plugin references per deleted user

A bulk user delete that fails on a later id had already removed the
earlier rows but skipped their plugin cleanup. Cleanup now runs right
after each successful delete.

* fix(core): unload disabled plugins even when a user delete fails

A bulk delete can fail on a later id after earlier users were removed
and their plugins auto-disabled. The wrapper returned before unloading,
leaving those plugins running until the next successful delete or a
restart.

* chore(deps): pin deluan/rest to v1.0.1

Replaces the pseudo-version of the refactor branch with the tagged
release. REST error messages now name the bare type (Artist, not
model.Artist).

* test: use the spec context instead of context.Background()

Replace the context.Background()/context.TODO() calls this branch added
to tests with the spec's ctx, GinkgoT().Context(), or t/b.Context(), so
repository calls are bound to the running spec's lifetime.

* test: declare the spec context once per Describe

Set ctx from GinkgoT().Context() first in each top-level BeforeEach and reuse it, building user contexts on top of it instead of repeating inline calls.
2026-09-25 18:06:10 -04:00
Deluan Quintão
3f4b6a642c
fix(server): return 404 instead of 500 for missing native API resources (#6131)
* fix: return 404 instead of 500 for missing native API resources

The deluan/rest controller only maps rest.ErrNotFound to 404, comparing with ==.
Most repositories return model.ErrNotFound, which had the same message but was a
different value, so requesting a missing playlist, album, artist, song, radio,
player, transcoding or library returned 500. This also applied to other users'
private playlists.

Make model.ErrNotFound the same value as rest.ErrNotFound. This fixes every REST
route at once, with no per-route wrapping. errors.Is checks against either
error keep working, and nothing wraps model.ErrNotFound before it reaches the
controller.

Fixes #6130

* fix(radio): return not found when deleting a missing radio station

radioRepository.Delete used the shared delete helper, which never reports a
missing row because SQL DELETE on zero rows is not an error. Deleting an unknown
id silently succeeded: DELETE /api/radio/{id} returned 200, and the Subsonic
deleteInternetRadioStation endpoint returned ok.

Delete now checks the affected row count and returns model.ErrNotFound when
nothing was deleted. The native API returns 404, and deleteInternetRadioStation
returns error 70 (data not found). This matches Subsonic 6.1.6, gonic (both
verified live) and Ampache (verified in source). Airsonic-Advanced does not
implement this endpoint.

The shared delete helper is unchanged, as several callers rely on deletes of
absent rows succeeding.

* fix(ui): return 404 for missing files that are not missing or do not exist

missingRepository.Read filtered media files by bare "id" and "missing" columns.
The media file query joins the library table, so SQLite rejected the query as
ambiguous and GET /api/missing/{id} returned 500. Read now loads the file with
MediaFileRepository.Get, which qualifies the column, and returns not found
when the file does not exist or is not marked missing.

* fix: report missing rows on single-item deletes and adopt deluan/rest errors.Is

Bump github.com/deluan/rest to the version whose controller matches errors
with errors.Is and errors.As. model.ErrNotFound stays the same value as
rest.ErrNotFound, so the many hand-written conversions from model.ErrNotFound
to rest.ErrNotFound in repositories, core services and test mocks did nothing.
Remove them, along with the duplicate rest.ErrNotFound check in the Subsonic
error mapper. Mappings from model.ErrNotAuthorized stay, as those are
different errors.

User and transcoding deletes had the same silent success as radio: the shared
delete helper never reports a missing row, so their not-found checks never
fired and DELETE /api/user/{id} and /api/transcoding/{id} returned 200 for
unknown ids. Add deleteByID, which returns model.ErrNotFound when no row
matched, and use it for radio, user and transcoding. Also drop the dead
sql.ErrNoRows branch from delete, since a DELETE never returns it.

The Subsonic deleteUser endpoint is not implemented (501), so this does not
change the Subsonic API. Plugin deletes keep the silent helper: there is no
REST route for them, and the plugin manager only deletes rows it just read.

* chore: drop ErrNotFound comment and its identity test

The alias to rest.ErrNotFound is self-explanatory, and the identity test only
restated the declaration.

* refactor: alias model.ErrNotAuthorized to rest.ErrPermissionDenied

Like ErrNotFound, make model.ErrNotAuthorized the same value as the rest
library's error, so REST endpoints map it to 403 directly. This removes the
ErrNotAuthorized to rest.ErrPermissionDenied mappings in the library and
playlist REST adapters and the duplicate check in the Subsonic error mapper.

Handlers that check model.ErrNotAuthorized now also recognize
rest.ErrPermissionDenied returned by repositories, so writePlaylistError, the
image upload handlers and the public share handler return 403 for it instead
of their fallback status. The error message changes from "not authorized" to
"permission denied".
2026-09-14 22:46:21 -04:00
Deluan
d00c84716b fix(scanner): update go-taglib to fix permission denied on shared hosts
The go-taglib WASM compilation cache lived in a shared $TMPDIR/go-taglib-wasm
directory, created with 0700 by whichever user ran first. A second Navidrome
instance running as another user on the same machine failed to read every
file with "permission denied", so its scan found no files.

The updated fork uses a per-user cache directory (go-taglib-wasm-<uid>) and
falls back to running without the cache when it cannot be created or used.
2026-09-13 11:03:56 -04:00
Deluan
964d3c778b build(deps): update direct Go dependencies and taglib fork
Bumps all 13 direct dependencies that had newer releases, plus the
go-taglib fork pin. No source changes were needed.

The jwx bump to v3.3.0 carries a security fix (GHSA-4cf7-xm37-g63h):
custom claim, header and JWK names were written unescaped, so a name
containing a quote could inject extra members. Navidrome is not
affected - every claim name we emit is a hardcoded literal - but the
fix is worth taking. cascadia v1.3.5 similarly limits selector nesting
to avoid a stack overflow, and our only selector is a constant.

go-sqlite3 v1.14.52 is the only bump with real behavior change: it
flushes the statement cache on schema changes, steps cached statements
eagerly, and drops the per-row goroutine used for query cancellation.
goose v3.28.0 raises its minimum to Go 1.26 and otherwise only touches
MySQL, ClickHouse and Azure SQL, which we do not use. The golang.org/x
bumps are routine. govulncheck reports no reachable vulnerabilities.

The taglib fork pin picks up two fixes. Audio properties are now
clamped with std::max(0, ...) before the unsigned conversion, so a
malformed file no longer reports a duration of ~49 days; this ports
upstream sentriz/go-taglib 0524e91 and additionally covers
bitsPerSample, which is specific to this fork. Bit depth is also now
reported for DSDIFF, TrueAudio and Shorten, which previously returned
0. Both values reach media_file only on re-extraction, so existing
libraries need a full scan to pick them up.
2026-09-10 15:00:01 -04:00
Deluan Quintão
330da83eff
chore(deps): bump TagLib to 2.3.2 (#6088)
See https://github.com/taglib/taglib/releases/tag/v2.3.2
2026-09-05 13:52:07 -04:00
Deluan
b3ecaddd9c chore(deps): update fscache and stream dependencies to latest versions
Signed-off-by: Deluan <deluan@navidrome.org>
2026-08-30 12:44:09 -04:00
Deluan
d7ca00d018 chore(deps): update fscache fork to the CancelWithErr simplification
stream v1.5.0 added CancelWithErr, which delivers a cancellation cause to
blocked reads, future reads, and NextReader. The fscache fork now delegates
CloseWithError to it, dropping its own cause recording and reader wrappers.
Behavior is unchanged on the Navidrome side.
2026-08-29 17:07:17 -04:00
Deluan
b5f530e90c chore(deps): update Go dependencies to latest versions
Signed-off-by: Deluan <deluan@navidrome.org>
2026-08-27 20:19:16 -04:00
Deluan Quintão
97da9993d7
fix(stream): abort the response when a transcoded stream is truncated (#6035)
* fix(stream): abort the response when a transcoded stream is truncated

When a transcode failed after some audio had already been sent, Serve logged
the error and returned nil, so Go finished the chunked body normally and the
client received an apparently complete, silently short file. Symfonium users
hit this on large offline syncs, and the worst path, ffmpeg dying mid-write
behind the transcoding cache, produced no error and nothing in the log above
Debug: the cache writer was closed plainly, so readers drained the truncated
entry to a clean EOF.

The root cause of that silence is an fscache limitation: Close is the only way
to end a cache write, and Close always means "complete". This adopts the
deluan/fscache fork, which adds CloseWithError: on failure copyAndClose now
cancels the entry with the cause, so every attached reader fails mid-read with
the real error instead of EOF, a late Get for the entry is refused, and the
entry never reports a final size. The error travels inside the entry each
reader holds, which makes per-generation delivery automatic and needs no
bookkeeping on our side.

With the failure arriving in-band, one change in Serve covers every mode: an
io.Copy error after bytes are on the wire panics with http.ErrAbortHandler.
Go aborts the response without the terminating chunk (RST_STREAM on HTTP/2),
chi's Recoverer re-panics that value, and the deferred stream.Close() still
runs, so the transcode limiter slot is released as before.

Two behaviors improve as side effects. A transcoder that dies before its first
byte now yields a Subsonic error response instead of a 200 with an empty body,
since the failure reaches Serve as an error while the status is still
unsent; genuinely empty output (clean EOF, exit 0) keeps the 200. And a failed
entry's invalidation no longer defers its unlink past a replacement entry
re-creating the same file, because canceling already closed its readers.

* fix(cache): warn when the cache writer cannot report failures to readers

The CloseWithError capability comes from the fscache fork via a go.mod
replace directive, and a type assertion picks it up. If that directive is
ever lost, the assertion fails silently, readers of a dead writer go back to
draining a truncated entry to a clean EOF, and nothing says so.

Two layers against that: a warning on the failure path when the writer lacks
the capability, and a test that asserts the writer fscache returns carries
it, so losing the fork fails CI instead of a listener's download.

* build: point the fscache replace at the fork's master

deluan/fscache#1 is merged; pin the merge commit instead of the review
branch. Pinned by sha because the module proxy still resolves the fork's
master ref to its pre-merge commit.

* build: reference the upstream fscache PR in the replace comment

The replace itself must keep pointing at the fork: the commit only exists in
djherbis/fscache under refs/pull/22/head, which the Go module fetcher cannot
resolve (verified: unknown revision for both short and full sha). The same
commit is advertised on the fork's master, so that is the fetchable source.
2026-08-25 18:48:43 -04:00
Deluan
4c0ab074a3 chore(deps): update module dependencies in go.mod and go.sum to latest versions
Signed-off-by: Deluan <deluan@navidrome.org>
2026-08-17 20:36:13 -04:00
Deluan
c35b14dd74 chore(deps): update go-taglib to v2.3.1
Signed-off-by: Deluan <deluan@navidrome.org>
2026-07-20 09:49:48 -04:00
Deluan
edddc1acb5 chore(deps): update go-sqlite3, reflex, and golang.org/x dependencies to latest versions
Signed-off-by: Deluan <deluan@navidrome.org>
2026-07-13 14:07:00 -04:00
Deluan
6c95a66ad6 chore(deps): update Go dependencies to latest versions
Signed-off-by: Deluan <deluan@navidrome.org>
2026-07-07 07:54:39 -04:00
Deluan
5f2b7ee105 chore(deps): update webp, gomega, and go-toml dependencies to latest versions
Signed-off-by: Deluan <deluan@navidrome.org>
2026-06-29 21:24:20 -04:00
Deluan
9bd3400d0e chore(deps): update ttlcache and sqlite3 dependencies to latest versions 2026-06-22 16:35:09 -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
6f7af6650c chore(deps): update Go dependencies
Signed-off-by: Deluan <deluan@navidrome.org>
2026-06-19 18:30:24 -04:00
Deluan
59b6755014 chore(deps): update dependencies to latest versions in go.mod and go.sum
Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-28 20:15:40 -03:00
Deluan
2b3b879c57 chore(deps): update dependencies to latest versions
Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-11 20:34:04 -03:00
Deluan
e55a35544b chore(deps): update TagLib to 2.3
Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-11 20:33:14 -03:00
Deluan
dd2b6865b0 chore(deps): update Go dependencies in go.mod and go.sum
Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-03 12:34:00 -04:00
Deluan
a4c1fa6378 chore(deps): update dependencies to latest versions
Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-27 20:24:01 -04:00
Deluan Quintão
3e25ca3868
test: enable Subsonic response snapshot tests on Windows (#5427)
* fix(test): enable Subsonic response snapshot tests on Windows

Replaced cupaloy with a simple custom snapshot matcher that normalizes
CRLF line endings before comparison. The tests were skipped on Windows
via a //go:build unix tag because Git for Windows checks out snapshot
files with CRLF, while Go's xml/json.MarshalIndent always produces LF,
causing direct string comparison to fail. The new matcher reads snapshot
files with os.ReadFile and normalizes \r\n to \n before comparing.
Also added a .gitattributes in the .snapshots directory to enforce LF
checkout, and removed the now-unused cupaloy dependency.

* fix(test): add UPDATE_SNAPSHOTS support to custom snapshot matcher

Restore the ability to update snapshots via `make snapshots`
(UPDATE_SNAPSHOTS=true), which was lost when replacing cupaloy
with the custom matcher.
2026-04-27 20:19:28 -04:00
Deluan
02c9fc3359 chore(deps): update go-sqlite3 and other dependencies to latest versions
Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-13 20:32:42 -04:00
Deluan
1de4e43d29 fix(gotaglib): update go-taglib to fix issue with empty id3v2 frames
Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-07 15:30:21 -04:00
Deluan
6109bf5192 chore(deps): update go-sqlite3 to v1.14.38 and go-toml to v2.3.0
Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-01 08:51:10 -04:00
dependabot[bot]
c5bb920b88
chore(deps): bump golang.org/x/image from 0.37.0 to 0.38.0 (#5268)
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.37.0 to 0.38.0.
- [Commits](https://github.com/golang/image/compare/v0.37.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-version: 0.38.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 18:57:43 -04:00
Deluan
ad92b752be chore(deps): update dependencies for go-sqlite3, golang.org/x packages
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-17 18:34:13 -04:00
Deluan
c63346de04 chore: run go mod tidy after dependency replacements 2026-03-14 10:23:45 -04:00
Deluan
d042fc138c refactor(nanoid): replace gonanoid with custom nanoid implementation for ID generation
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-13 21:06:26 -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
d76b49c6d1 chore(deps): update golang.org/x/sync, golang.org/x/sys, golang.org/x/time, and go.opentelemetry.io/proto/otlp to latest versions
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-09 17:19:12 -04:00
Deluan
e1b3412999 fix(scanner): update gotaglib version to reflect actual dependency version
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-07 12:00:09 -05: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
157c917ca5 chore(deps): update golang.org/x/net to v0.51.0
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-02 17:01:12 -05:00
Deluan
82f9f88c0f refactor(auth): replace untyped JWT claims with typed Claims struct
Introduced a typed Claims struct in core/auth to replace the raw
map[string]any approach used for JWT claims throughout the codebase.
This provides compile-time safety and better readability when creating,
validating, and extracting JWT tokens. Also upgraded lestrrat-go/jwx
from v2 to v3 and go-chi/jwtauth to v5.4.0, adapting all callers to
the new API where token accessor methods now return tuples instead of
bare values. Updated all affected handlers, middleware, and tests.

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-02 14:03:27 -05:00
Deluan
14343d91b0 chore(deps): update goose to 3.27.0
Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-24 21:44:04 -05:00
Deluan
fc36f1daa6 chore(deps): update go-taglib dependency to latest version (mka fix)
Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-24 21:19:11 -05:00
Deluan
6f5f58ae9d chore(deps): update go-taglib to v0.0.0-20260221220301-2fab4903f48e
Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-21 17:04:59 -05:00
Deluan Quintão
821f22a86f
feat(scanner): upgrade TagLib to 2.2, with MKA/Matroska support (#5071)
* chore(deps): update go-taglib fork with MKA/Matroska support

Bump deluan/go-taglib to cf75207bfff8, which upgrades the underlying
taglib to v2.2 and adds Matroska container format detection and
metadata handling (MKA audio files).

* chore(deps): update cross-taglib version to 2.2.0-1

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

* chore(make): rename run-docker target to docker-run for consistency

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

* chore(go-taglib): update version to 2.2 WASM and add debug logging

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

* chore(deps): update go-taglib to v0.0.0-20260220032326 for MKA fixes

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-21 16:52:48 -05:00
Deluan
b774133cd1 chore(deps): update go-sqlite3 to v1.14.34 and pocketbase/dbx to v1.12.0
Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-17 08:35:02 -05:00
Deluan
199cde4109 fix: upgrade go-taglib to latest version
Updated the go-taglib dependency to pick up the latest bug fixes from
the forked repository. This resolves an issue reported in #5037.
2026-02-12 10:12:04 -05:00
Deluan Quintão
ed79a8897b
fix(scanner): pass filename hint to gotaglib's OpenStream for format detection (#5012)
* fix: split reflex -R flags to preserve directory exclusion optimization

Combining the _test.go exclusion pattern (which uses $) into the same -R
regex as the directory prefixes (^ui, ^data, ^db/migrations) disabled
reflex's ExcludePrefix optimization. Reflex disables prefix-based
directory skipping when the regex AST contains $, \z, or \b operators,
causing it to traverse into ui/node_modules and hit "too many open files".

Splitting into two separate -R flags fixes this: the directory prefix
regex remains $-free so ExcludePrefix works, while the _test.go pattern
gets its own flag where the $ anchor doesn't affect directory skipping.

* fix(gotaglib): pass filename hint to OpenStream for format detection

OpenStream relies on content-sniffing when no filename is provided,
which fails for some files (e.g. OPUS). Pass the filename via the new
WithFilename option so TagLib can use the file extension as a hint.

Also adds an OPUS test fixture and test entry.

Relates to https://github.com/navidrome/navidrome/issues/4604#issuecomment-3868569113, #4998, #5010
2026-02-09 16:16:28 -05:00
Deluan
302d99aa8b chore(deps): update dependencies in go.mod and go.sum
Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-09 16:05:37 -05:00
Deluan
29f98b889b chore(deps): update dependencies in go.mod and go.sum to latest versions
Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-07 13:23:58 -05:00
Deluan
fae58bb390 chore(deps): update Go dependencies to latest versions
Signed-off-by: Deluan <deluan@navidrome.org>
2026-01-20 06:51:19 -05:00
Deluan Quintão
f1e75c40dc
feat(plugins): add JSONForms-based plugin configuration UI (#4911)
* feat(plugins): add JSONForms schema for plugin configuration

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

* feat: enhance error handling by formatting validation errors with field names

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

* feat: enforce required fields in config validation and improve error handling

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

* format JS code

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

* feat: add config schema validation and enhance manifest structure

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

* feat: refactor plugin config parsing and add unit tests

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

* feat: add config validation error message in Portuguese

* feat: enhance AlwaysExpandedArrayLayout with description support and improve array control testing

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

* feat: update Discord Rust plugin configuration to use JSONForm for user tokens and enhance schema validation

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

* fix: resolve React Hooks linting issues in plugin UI components

* Apply suggestions from code review

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* format code

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

* feat: migrate schema validation to use santhosh-tekuri/jsonschema and improve error formatting

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

* address PR comments

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

* fix flaky test

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

* feat: enhance array layout and configuration handling with AJV defaults

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

* feat: implement custom tester to exclude enum arrays from AlwaysExpandedArrayLayout

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

* feat: add error boundary for schema rendering and improve error messages

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

* feat: refine non-enum array control logic by utilizing JSONForms schema resolution

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

* feat: add error styling to ToggleEnabledSwitch for disabled state

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

* feat: adjust label positioning and styling in SchemaConfigEditor for improved layout

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

* feat: implement outlined input controls renderers to replace custom fragile CSS

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

* feat: remove margin from last form control inside array items for better spacing

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

* feat: enhance AJV error handling to transform required errors for field-level validation

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

* feat: set default value for User Tokens in manifest.json to improve user experience

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

* format

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

* feat: add margin to outlined input controls for improved spacing

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

* feat: remove redundant margin rule for last form control in array items

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

* feat: adjust font size of label elements in SchemaConfigEditor for improved readability

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-01-19 20:51:00 -05:00
Deluan
4ec6e7c56e perf(taglib): update taglib to use ReadStyleFast for improved performance
Signed-off-by: Deluan <deluan@navidrome.org>
2026-01-18 21:10:06 -05:00