Commit graph

75 commits

Author SHA1 Message Date
Deluan Quintão
f48943c058
fix(plugins): discard buffered scrobbles when a plugin is removed (#5737)
* fix(plugins): discard buffered scrobbles when a plugin is removed

Scrobbles are buffered in the DB per service, keyed by the plugin name.
When a plugin was removed (deleted from the plugins folder and detected by
the sync), its pending buffer entries were left behind forever: the drain
goroutine is stopped on the next scrobbler refresh, so the rows were never
retried nor discarded. Worse, if a plugin with the same name was installed
later, the stale entries would be drained into it - potentially a completely
unrelated plugin that just reuses the name.

Add a Discard(service) method to ScrobbleBufferRepository and call it from
removePluginFromDB, right after the plugin record is deleted. Disabling a
plugin intentionally keeps its buffered scrobbles, consistent with the
buffer's purpose of surviving temporary outages, and transient unload/reload
cycles during config updates are unaffected since they never delete the
plugin record.

* fix(plugins): don't wipe builtin scrobbler queues on plugin removal

Buffer entries are keyed by service name only, and removePluginFromDB runs
for any removed plugin file, so removing a plugin named e.g. lastfm.ndp -
regardless of its capability - would discard the builtin Last.fm retry
queue. Skip the discard when the plugin name is owned by a registered
builtin scrobbler, exposed via a new scrobbler.IsBuiltinScrobbler helper.
Reported by Codex review on the PR.

Also drop the testBroker usage from the new removePluginFromDB spec: it is
defined in manager_test.go which is excluded on Windows, breaking the
Windows test build. sendPluginRefreshEvent is nil-safe, so no broker is
needed.
2026-07-08 12:37:17 -04:00
Deluan Quintão
01b7c86f90
fix(scanner): stop logging expected lyrics sniff misses as warnings (#5702)
Some checks are pending
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (Windows) (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
* fix(scanner): stop logging expected lyrics sniff misses as warnings

During a scan, embedded lyrics are parsed with an empty suffix, which puts
ParseLyrics into content-sniffing mode: it tries the TTML, SRT and Lyricsfile
YAML parsers in turn before falling back to plain text. Every plain-text or LRC
lyric therefore fails the structured probes on its way to the fallback, and each
failure was logged at warning level with no indication of which file triggered
it, flooding the scan log with benign "Error parsing lyrics, falling back to
plain text" messages.

A probe rejecting content it does not own during sniffing is expected control
flow, so it is now logged at trace instead. A parse failure under an explicitly
requested suffix (e.g. a malformed .yaml/.srt/.ttml sidecar) still warns, since
the user declared that format. ParseLyrics gains ctx and path parameters so any
warning names the offending file and carries request context where available;
all call sites are updated accordingly.

Also fixes a test-isolation bug in the new logging spec: the BeforeEach swapped
the process-global default logger via SetDefaultLogger but only restored the log
level on cleanup, leaking the null logger and its hook into later specs in the
shared model suite.

* test: use spec-scoped contexts instead of context.Background in lyrics tests

Replace context.Background() with GinkgoT().Context() (and b.Context() in the
parse benchmarks) across the lyrics-related tests, so contexts are cancelled
when each spec ends. The embeddedLyrics fixture in core/lyrics is now a
hand-written literal like its sibling fixtures, removing the construction-time
ParseLyrics call that could not use a spec-scoped context.

* refactor(model): attach lyrics parse log attribution via context

Narrow ParseLyrics back to (ctx, suffix, lang, contents), dropping the path
parameter added by the previous commit. Attribution now uses the codebase's
existing idiom: callers that know the source attach it with log.NewContext
(e.g. "file" for the media file or sidecar), and the plugin adapter tags both
the plugin name and the track, fixing probe-miss logs that misattributed
plugin-returned content to the file's own tags. This removes three adjacent
string parameters that were easy to swap silently, and the "" placeholder most
call sites had to pass.

Also hardens the logging spec from the previous commit: the null test logger is
now swapped in before raising the level (SetLevel forces the current default
logger to trace, so the old order left the null logger at info and trace
entries never reached the hook), the sniff test now asserts probe misses are
observable at trace with file attribution instead of only asserting the absence
of warnings, and cleanup restores the actual previous logger — via a new return
value on log.SetDefaultLogger — instead of a bare logrus.New() that would
discard hooks configured on the process-wide logger.

* refactor(lyrics): hoist attributed log contexts out of loops

Address review feedback on #5702: build the log-attributed context once per
operation instead of per iteration, and reuse it on the surrounding log calls
so the error/trace lines around ParseLyrics carry the same attribution fields.
In fromExternalFile the sidecar path now rides the context for all log lines
in the function, replacing the repeated explicit "path" field.

* style(model): pass lyrics parse errors as final log arguments

Per the project logging convention, errors go as the last argument (the log
package normalizes them via its error case) instead of a keyed "error" pair,
which stores the raw error value and bypasses that handling. Flagged by review
on #5702; the keyed form was inherited from the original warning line.
2026-07-02 09:46:57 -04:00
dependabot[bot]
b405252f51
chore(deps): bump golang.org/x/net in /plugins/cmd/ndpgen (#5698)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.49.0 to 0.55.0.
- [Commits](https://github.com/golang/net/compare/v0.49.0...v0.55.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.55.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-01 19:24:47 -04:00
Deluan
2d53360f89 docs(plugins): document plugin library scope authorization logic
Signed-off-by: Deluan <deluan@navidrome.org>
2026-07-01 13:20:31 -04:00
Deluan Quintão
f580d3ea76
feat(plugins): expose the song Matcher as a host service (#5643)
* feat(plugins): add public Track and Artist DTOs for host services

* feat(plugins): add Matcher host-service interface and MatchSong DTO

* feat(plugins): generate Matcher host wrappers, PDK clients, and matcher permission

* feat(plugins): implement Matcher host service and MediaFile-to-Track converter

Also fixes an ndpgen bug where ParseDirectory parsed each host-service file
in isolation, so a service method referencing a struct defined in another
file of the same package (host.Track in track.go) could not be resolved.
ParseDirectory now collects package-wide structs in a first pass, mirroring
ParseCapabilities; PDK clients regenerated cleanly via make gen.

* feat(plugins): register Matcher host service in the manager

* test(plugins): add Matcher host service integration test plugin

* refactor(plugins): simplify matcher converter and parser file collection

- toTrack: use gg.V for nil-able field derefs and slice.Map for genres/
  participants, removing the repeated nil-guard blocks and inner loop
- manager_loader: drop the redundant ds==nil guard (loadEnabledPlugins
  already gates a nil DataStore), matching the other service entries
- ndpgen parser: extract collectGoFiles, shared by ParseDirectory and
  ParseCapabilities instead of duplicating the file-filter loop

* fix(plugins): keep nullable Track numerics as pointers

ReplayGain values, BitDepth, and BPM are nullable in model.MediaFile, and 0
is a valid measured ReplayGain value. Flattening them to value types with
omitempty made a real 0 indistinguishable from absent. Model them as *float64
/*int32 so plugins can tell 'no data' from a measured 0. Regenerated PDK
clients; converter passes the model pointers through (RG) or maps *int->*int32
(BitDepth/BPM).

* refactor(plugins): trim redundant pass labels in ndpgen ParseDirectory

The function doc already explains the two-pass approach; the inline labels
restated it. Reduce to bare waypoints.

* fix(plugins): gate Track.Path on library filesystem permission

MatchSongs copied mf.Path into every result unconditionally, letting a plugin
with only the matcher permission enumerate on-disk file paths by matching known
songs. Gate Path behind library.filesystem, matching the Library host service.
toTrack is now a method carrying the permission flag.

* refactor(plugins): align MatchSong JSON casing and parse Go files once

- MatchSong: artistMBID/albumMBID JSON tags -> artistMbid/albumMbid so the Go
  wire format matches the Rust SDK's camelCase serialization (cross-SDK fix)
- MatchSongs doc reworded to language-neutral 'empty (absent)' so generated
  Rust/Python client docs no longer say Go-specific 'nil'
- ndpgen: parse each package file once (parseGoFiles) and reuse the ASTs across
  both passes in ParseDirectory and ParseCapabilities, instead of re-parsing

* refactor(plugins): use shared types for Matcher host service

Move the Matcher host service onto the shared plugins/types package instead
of the host-local MatchSong and Track structs. MatchSongs now takes
[]types.SongRef and returns []*types.Track, dropping host.MatchSong and moving
host.Track (with its host.Artist dependency collapsed onto types.ArtistRef) into
plugins/types. ArtistRef gains SortName and SubRole so it can back a track's
Participants.

SongRef gains a millisecond-precision DurationMs field that supersedes the now
deprecated seconds-based Duration, with DurationInMs() resolving the effective
value and SetDurationMs() keeping both fields in sync when populating a SongRef
to send to a plugin.

The ndpgen host-wrapper template only ever imported context, json and extism, so
a host service referencing the shared types package produced uncompilable code.
Emit the plugins/types import when the service references shared types directly
(gated on the existing Service.ImportsSharedTypes), matching the client template,
and cover it with GenerateHost tests. This removes the need for host-local
re-export aliases. Regenerated the Go/Rust/Python PDK and capability schemas
accordingly.

* test(plugins): cover SongRef duration and artist conversion

Add unit coverage for the new SongRef behavior: SetDurationMs populating both
DurationMs and the deprecated seconds field, and the SongRef-to-agents.Song
conversion preferring DurationMs over Duration and the Artists list over the
scalar Artist/ArtistMBID.

Extract the inline SongRef-to-agents.Song closure in MatchSongs into a named
toAgentSong function so the conversion can be asserted directly rather than only
through the opaque matcher. The end-to-end wire shape of the moved types is
already validated by the existing MatcherService integration test, so no new
WASM-boundary test is needed.

* fix(plugins): harden and unify SongRef-to-agents.Song duration conversion

Address findings from a code review of the matcher host service:

- DurationInMs now clamps a negative deprecated-seconds value to 0 instead of
  converting it through uint32, which previously wrapped a value like -1s into a
  ~49-day duration that corrupted the matcher's duration-proximity tiebreaker.
- Replace the unused SetDurationMs(uint32) with SetDuration(seconds float32),
  which takes the unit callers actually hold (model.MediaFile.Duration is
  float32 seconds) and centralizes the seconds-to-ms conversion. Wire it into
  mediaFileToSongRef so outbound SongRefs carry both duration fields in sync.
- Make the metadata-agent path use DurationInMs() so every consumer of the
  shared SongRef honors the DurationMs-over-Duration precedence contract; a
  plugin sending only DurationMs no longer loses its duration on that path.
- Collapse the matcher's duplicate toAgentSong/agentArtists helpers into the
  existing songRefToAgentSong converter, so there is a single SongRef-to-Song
  mapping. Tests narrowed to the duration cases, with artist precedence still
  covered in metadata_agent_test.go.

* feat(plugins): allow Matcher host service to scope a match to a user

Add an options struct to the Matcher host service so a plugin can run a match as
a specific user. When MatchOptions.Username is set, the match is run in that
user's context: their favourites and ratings inform the matcher's tiebreaker, and
the returned tracks carry that user's per-user annotations (Starred, StarredAt,
Rating, PlayCount, PlayDate, added to types.Track). An empty username preserves
the previous unscoped behaviour.

Cross-user access is gated by the same allowedUsers/allUsers permission the Users
and SubsonicAPI host services use: an unknown username, or one the plugin is not
permitted to act as, returns an error. User-library access applies automatically
once the user is in context (applyLibraryFilter). Independently, results are now
restricted to the libraries the plugin itself may access via the precomputed
libraryAccess set, dropping any matched track outside that set (the input index
stays unmatched) — this applies even without a username and even for an
admin-scoped user.

core/matcher is unchanged: it already loads and uses annotations and applies
user-library filtering from context, so the feature works by deriving the request
context and post-filtering by plugin library access in the host adapter. The new
opts parameter and the Track annotation fields are propagated to all PDK clients
(Go/Rust/Python) by make gen.

* fix(plugins): correct Matcher library scope and unify user-access checks

Address findings from a code review of the user-scoped Matcher host service:

- The plugin-library post-filter previously dropped every match for a plugin that
  holds only the matcher permission, because library config is tied to the Library
  permission and a matcher-only plugin has none (empty allowedLibraries,
  AllLibraries=false). Gate the filter on whether the plugin actually declared the
  Library permission: matcher-only plugins are no longer library-restricted, while
  plugins that opt into a library scope are enforced as before. The per-user
  library filter (applyLibraryFilter) still applies whenever a non-admin user is
  scoped.

- resolveUser collapsed every FindByUsername error (including transient DB
  failures) into a misleading "not found". Extract a shared userAccess type
  (alongside libraryAccess) whose resolve() distinguishes model.ErrNotFound from a
  real backend error and authorizes the user against the allowed set. The Matcher
  service now uses it, and host_subsonicapi shares the same userAccess type for its
  permission check (preserving its existing error messages), removing a third
  divergent copy of the resolve-and-authorize logic.

- Document in the matcher tests that the mock MediaFileRepo returns annotations
  unconditionally, so the unit tests cover the adapter's scoped-flag gating and
  access checks but not the SQL per-user join. Add tests for the library-permission
  gating and for surfacing a backend error instead of masking it as not-found.

* fix(plugins): require a library scope for Matcher, fail closed

Reverse the permissive default introduced when fixing the library post-filter: a
Matcher plugin now must be granted a library scope (all libraries, or at least one
specific library) and MatchSongs rejects the request with "no libraries
configured" when it has none, instead of either silently matching nothing or
defaulting to every library.

This mirrors how the SubsonicAPI host service requires a user scope
(checkPermissions errors with "no users configured" when none is set): the check
is a runtime guard via libraryAccess.configured(), needs no manifest changes, and
keeps the failure loud rather than silent. The per-match library post-filter then
always applies, and the restrictLibraries flag added in the previous commit is
removed.

* fix(plugins): require library permission for matcher; guard nil user

Close the gap where a plugin declaring only the matcher permission loaded
successfully but failed every MatchSongs call with "no libraries configured",
with no way for an admin to grant a library scope (the library-config UI is gated
on the library permission). Add a cross-field manifest rule, mirroring the
existing "subsonicapi requires users" rule, so the matcher permission requires
the library permission to be declared. A matcher plugin therefore also surfaces
the library-config panel and is subject to the existing load/enable-time library
configuration gate, making the fail-closed library check reachable and fixable
rather than a silent dead end. The test plugin manifest now declares the library
permission accordingly.

Also restore a defensive nil-user guard in userAccess.resolve: if a DataStore's
FindByUsername ever returns (nil, nil) instead of model.ErrNotFound, return a
clean "not found" error rather than dereferencing a nil *model.User.

* feat(plugins): expose track AverageRating in Matcher results

Add AverageRating to the Matcher's Track DTO. Unlike the per-user annotations
(Starred, Rating, PlayCount, ...), AverageRating is an aggregate stored on the
track itself and is loaded regardless of the request user, so it is populated
unconditionally rather than gated on a scoped username. Propagated to the PDK
types by make gen.

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

* style(plugins): trim verbose comments in matcher host service

Condense the over-long explanatory comments added across the matcher host
service to one-liners that state the why, and simplify the ptrInt32/unixPtr
helpers to Go 1.26's new(value). No behavior change.

* refactor(plugins): pass userAccess into newSubsonicAPIService

Move newUserAccess construction to the loader call site so the SubsonicAPI service
constructor takes a userAccess value directly, matching newMatcherService. Pure
refactor: the service already stored a userAccess internally, so behavior and error
messages are unchanged.

* fix(plugins): regenerate PDK and drop omitempty from AverageRating

Re-run make gen so the generated PDK doc comments match the source comment
trimmed in an earlier commit (the source was simplified but the PDK was not
regenerated, leaving the committed files stale — a 'generated files up to date'
hazard).

Also drop omitempty from Track.AverageRating: it is always set (0 when unrated),
so it should be present in the payload like the other always-set fields
(BirthTime/CreatedAt/UpdatedAt), not dropped at zero. Tag change propagated to the
PDK by the same regeneration.

* fix(plugins): reject user-scoped match before lookup when plugin has no user scope

A matcher plugin requires the library permission but not the users permission, so
a matcher-only plugin always has an empty user scope (allUsers=false, no allowed
users). MatchSongs still ran FindByUsername for any opts.Username before checking
authorization and returned distinguishable errors ('user X not found' vs 'not
allowed to act as user X'), letting such a plugin enumerate account names from the
error text.

Guard userAccess.resolve to reject with a single fixed error before the lookup when
the plugin has no user scope, mirroring how the SubsonicAPI service short-circuits
with 'no users configured'. The unscoped match path (no username) is unaffected, so
matcher-only plugins still match normally.

* fix(plugins): run unscoped matcher as admin, not the inherited request user

A matcher host call can arrive on a context that already carries a request user
(e.g. a plugin capability invoked while serving that user's request — extism
propagates the call context into host functions). With no opts.Username, MatchSongs
passed that context straight through, so the media-file repository applied the
caller's library filter and per-user annotation ranking to an explicitly unscoped
match.

Set the user context explicitly: a username scopes to that user (overriding any
inherited one), and an unscoped match runs under adminContext so only the plugin's
own library scope constrains results. Adds tests using a context-capturing
DataStore to assert the user the matcher resolves in both cases.

* chore(plugins): drop the generated Python matcher PDK

The Python plugin PDK is no longer supported (ndpgen generates only Go and Rust
clients), so remove the stale generated nd_host_matcher.py rather than leave a
client that drifts from the host interface.

* docs(plugins): deprecate SongRef.Artist/ArtistMBID in favor of Artists

Mark the scalar single-artist fields deprecated; Artists (the ArtistRef list) is
the preferred way to supply artist data and already takes precedence for matching.
Propagated to the PDK and capability schemas by make gen.

* refactor(plugins): flatten Track.Participants and add Role to ArtistRef

Change Track.Participants from map[role][]ArtistRef to a flat []ArtistRef, and give
ArtistRef a Role field (the participation category: artist/composer/performer/...)
alongside SubRole (a specialization within a role, e.g. the instrument for a
performer). In the flat list each entry now self-describes its role rather than
relying on a map key, matching how SongRef.Artists is already a flat list; the
converter tags each entry with its role and emits them in a stable role order.
Propagated to the PDK and capability schemas by make gen.

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-07-01 11:19:15 -04:00
Deluan Quintão
0e5b9e3263
feat(plugins): share plugin DTOs via a types package (#5655)
* refactor(plugins): remove Python PDK generation from ndpgen

* feat(plugins): parse Go type aliases distinctly in ndpgen

* feat(plugins): resolve shared-type aliases against a registry in ndpgen

* fix(plugins): resolve host-service shared aliases package-wide

Mirror the capability approach in ParseDirectoryWithShared: do a first
pass over all package files to build a package-wide alias map, then pass
it into parseServiceFile so that a shared-type alias declared in a sibling
file is visible when resolving types in the service interface file.

Add a focused test that writes the alias in one file and the hostservice
in another, confirming RED before the fix and GREEN after. Also
strengthens the existing Task 3 test with an ArtistRef.Target assertion.

* feat(plugins): add ndpgen -shared-types mode for the Go types package

* feat(plugins): generate the nd-pdk-types Rust crate from -shared-types

* feat(plugins): inject types import and emit deprecated aliases in Go output

* feat(plugins): emit deprecated Rust aliases to the shared types crate

* feat(plugins): inline shared-type shapes into XTP schemas

* feat(plugins): add nd-pdk-types crate and wire dependents

* feat(plugins): move shared capability types to plugins/types with deprecated aliases

* fix(plugins): point Rust deprecated-alias note at the replacement type

* fix(plugins): include shared aliases in KnownStructs so Rust fields keep their type

Capability.KnownStructs() and Service.KnownStructs() previously only
registered names from .Structs. After the shared-types migration, types
like ArtistRef/TrackInfo/SongRef live in .SharedAliases instead, so
ToRustTypeWithStructs could not find them and fell back to serde_json::Value
for every struct field referencing a shared type.

Add the shared-alias names to the knownStructs map in both methods.
Regenerate the Rust capability files; track/song/artist fields now render
as their named types (TrackInfo, SongRef, ArtistRef, etc.).

Add a regression test that verifies a struct field whose type is only in
SharedAliases renders as the named type and not serde_json::Value.

* docs(plugins): remove stale Python references from ndpgen and plugins READMEs

ndpgen no longer has a -python flag; remove it from the usage synopsis,
flags table, and defaults note in ndpgen/README.md. Delete the "Python
Client Library" section that described its output.

plugins/README.md referenced plugins/pdk/python/host/ (deleted) as the
source for Python host-service stubs. Remove that paragraph; Python plugins
still work via the XTP-schema / extism-py path (see examples/*-py).

* refactor(plugins): dedupe ndpgen helpers and tidy shared-type codegen

* docs(plugins): restore Python as a supported XTP schema target

The ndpgen-generated Python PDK was removed, but the XTP YAML schemas are
language-neutral and the XTP CLI still generates Python bindings from them
(as the extism-py examples demonstrate). Only the ndpgen Python output was
dropped, not Python support itself.

* test(plugins): use the shared types package in test plugins

The test fixtures referenced the now-deprecated capability aliases
(sonicsimilarity.SongRef, metadata.ArtistRef/SongRef). Point them at the
canonical types package so our own fixtures don't depend on symbols slated
for removal.

* refactor(plugins): use the shared types package in host adapters

Replace deprecated capabilities.TrackInfo, capabilities.ArtistRef, and
capabilities.SongRef aliases with the canonical types.TrackInfo,
types.ArtistRef, and types.SongRef from plugins/types.

* fix(plugins): reference shared types by canonical path in generated Rust

Previously the generator emitted `pub field: SongRef` (the local deprecated
alias) for struct fields whose type came from SharedAliases. Refactored
ToRustTypeWithStructs into a private toRustType that accepts a shared map,
and added ToRustTypeWithShared which resolves shared-alias names to their
canonical nd_pdk_types::X path before falling through to the knownStructs
check. Both rustCapabilityFuncMap and rustFuncMap now build the shared map
from SharedAliases and use it for fieldRustType, so the generated capability
files reference nd_pdk_types::SongRef / nd_pdk_types::TrackInfo directly.
The deprecated pub type aliases remain in place as the external back-compat
surface. Deprecation warning count from cargo build drops to 0.

* fix(examples): implement missing Scrobbler.playback_report in Rust examples

The Scrobbler trait gained a playback_report method but the two Rust example
plugins (webhook-rs and discord-rich-presence-rs) were not updated, causing
E0046 compile errors. Added the missing fn playback_report to both: webhook-rs
logs and returns Ok(()) mirroring its now_playing handler; discord-rich-presence-rs
is a no-op since Discord presence does not need playback reports. make all-rust
now exits 0.

* refactor(plugins): point Rust deprecation notes at the nd_pdk::types umbrella path

Plugin authors depend on the nd-pdk umbrella crate, which re-exports
nd_pdk_types as 'types', so the migration target they should type is
nd_pdk::types::X. The alias target stays nd_pdk_types::X (the real path
inside nd-pdk-capabilities).

* fix(plugins): error when a shared-type alias can't be resolved against the registry

* refactor(plugins): parse each Go source file once in ndpgen

* fix(plugins): correct ndpgen review nits (flag name, unused dep, docs)

* refactor(plugins): drop the now-unused path param from parseServiceFile

* refactor(plugins): use shared types directly, rename TrackInfo to Track

Capability interfaces now reference the shared `types` package by qualified
name (types.Track, types.SongRef, types.ArtistRef) instead of the package-local
deprecated aliases, and the shared TrackInfo type is renamed to Track to match
its role as the plugin-facing projection of a library media file.

The deprecated bare aliases (scrobbler.TrackInfo, metadata.ArtistRef,
sonicsimilarity.SongRef, etc.) are kept as re-exports so existing plugins keep
compiling, with a deprecation warning steering them to the canonical types.

To support this, ndpgen now resolves qualified types.X references: it collects
them during type discovery, maps each used canonical type back to its declared
deprecated alias for re-export, emits nd_pdk_types::X paths in Rust, and names
the XTP schema components by their canonical type. Regenerated the Go and Rust
PDK and the XTP schemas, and added generator tests covering the qualified-ref
path. Also adds clarifying doc comments to the shared types.

* refactor(plugins): extract shared types selector into a named const

Replace the "types." string literal that detects and strips the shared types
package selector with a single sharedTypesPrefix constant across the ndpgen
generator (parser, types, generator, xtp_schema), giving the package one source
of truth for the selector.

Also restore the single reused scratch map (cleared each iteration) in the
resolveSharedAliases BFS instead of allocating a fresh map per shared-struct
field, matching the prior implementation.

Pure cleanup from a /simplify pass: regeneration produces byte-for-byte
identical Go, Rust, and XTP output.

* refactor(plugins): keep TrackInfo in the capability package for now

Move the track type back out of the shared plugins/types package: it is again
defined inline as TrackInfo in plugins/capabilities/scrobbler.go and referenced
directly by the scrobbler and lyrics capabilities, reverting the rename to
types.Track. The host helper is renamed back to mediaFileToTrackInfo and now
returns capabilities.TrackInfo. SongRef and ArtistRef stay in the shared types
package; TrackInfo keeps using types.ArtistRef for its artist lists.

This type is expected to be reshaped in upcoming work, so leaving it in the
capability package avoids churning the shared types twice. Regenerated the Go
and Rust PDK and the XTP schemas accordingly.

* fix(plugins): emit the Go types import for direct shared-type refs

ndpgen's Capability/Service.ImportsSharedTypes only reported a shared-types
dependency when a deprecated re-export alias (type X = types.X) was declared. A
struct field referencing the canonical form directly (e.g. types.SongRef) with
no such alias produced an empty SharedAliases slice, so the Go templates skipped
the import while still emitting fields/signatures using types.SongRef — leaving
generated PDK code for new shared DTOs uncompilable unless an otherwise
unnecessary alias was added.

ImportsSharedTypes now also returns true when any struct field references the
types. package by qualified name, via a new structsReferenceSharedTypes helper
that reuses collectReferencedTypes (so []types.X and map[...]types.X are covered
too).

* fix(plugins): preserve base64 encoding for shared byte fields in Rust

The Rust shared-types crate template rendered a []byte field as a plain Vec<u8>
without the base64_bytes serde override used by the capability/client templates.
Go's encoding/json serializes []byte as a base64 string, so a Rust plugin using
nd_pdk::types would have serialized an array of numbers instead of the wire
format the Go/server side expects.

GenerateSharedTypesRust now registers the base64_bytes partial and passes a
HasByteFields flag (new anyFieldIsByteSlice helper); types.rs.tmpl emits the
base64_bytes module and a #[serde(with = "base64_bytes")] attribute on []byte
fields, mirroring the capability template.

* fix(plugins): include directly-referenced shared types in XTP schemas

buildSchemas registered shared types into the schema components only by iterating
cap.SharedAliases, which records deprecated re-export aliases. A capability that
referenced a shared DTO solely as types.Foo (no declared alias) therefore never
got Foo into the component set, so the self-contained XTP schema rendered the
field as a generic object (or emitted a dangling $ref), breaking the direct
shared-type use case enabled by -shared.

resolveSharedAliases now also returns the resolved shapes of every used shared
type (alias or not); these are carried on the new Capability.SharedTypes field
and registered by buildSchemas alongside SharedAliases. Validated end-to-end with
the xtp CLI: a direct types.Foo reference now produces a proper component plus a
$ref, so xtp generates a typed struct instead of an untyped serde_json::Map.

* fix(plugins): resolve renamed shared aliases to canonical schema refs

When a deprecated alias renames its canonical type (e.g. type TrackInfo =
types.Track) and a capability field is typed with the alias name (TrackInfo),
buildProperty emitted a $ref to #/components/schemas/TrackInfo. Components are
keyed by the canonical name (Track), so no TrackInfo component was emitted,
leaving a dangling reference that crashes the xtp code generator.

buildSchemas now builds an alias->canonical map; buildProperty (and the slice
item path) resolves $ref targets through it, and a used alias name marks its
canonical component used so it is emitted. Validated with the xtp CLI: the
renamed-alias schema previously crashed xtp and now generates cleanly.

* fix(plugins): detect shared types used directly in method signatures

ImportsSharedTypes only inspected struct fields, so a capability method using a
shared type directly in its signature (e.g. types.SongRef as input/output rather
than inside a local struct) was not detected. The generated Go templates still
rendered the provider/export signatures with types.SongRef, so the capability
package omitted the types import and failed to compile; the same gap applied to
service params/returns.

ImportsSharedTypes now also scans capability method input/output types and
service method params/returns, via a typeReferencesSharedTypes helper that reuses
collectReferencedTypes (covering pointer/slice/map wrappers).

* fix(plugins): add base64 dependency to the shared Rust types crate

When a shared DTO has a []byte field, ndpgen emits the base64_bytes serde helper
and use base64::... imports into nd-pdk-types/src/lib.rs, but the crate manifest
declared only serde. In that case make gen produced a crate that failed to
compile with 'unresolved module base64'.

Add base64 = "0.22" (matching nd-pdk-capabilities) so the generated shared types
crate compiles whenever a []byte field is present. Verified by generating a
shared crate with a []byte field and confirming cargo check fails before and
passes after.

* fix(plugins): translate shared method types in generated Rust

A capability method using a shared DTO directly as input/output (e.g.
types.SongRef) was passed through rustOutputType unchanged, so the Rust template
emitted invalid trait and extism_pdk::Json<$crate::pkg::types.SongRef> signatures
that do not compile.

Method input/output types now resolve through the shared registry: trait
signatures use rustTraitType (shared -> nd_pdk_types::X, locals stay bare) and the
export macros use rustMethodType (fully qualified: shared -> nd_pdk_types::X,
primitives -> Rust, locals -> $crate::<pkg>::X). Verified end-to-end by compiling
a generated capability that takes types.SongRef directly against the real
nd-pdk-types crate.

* fix(plugins): canonicalize XTP export refs for renamed shared aliases

buildSchemas canonicalized alias-to-canonical references for struct-field $ref
targets, but buildExport built export input/output $refs straight from
fieldBaseType. A capability whose export used a renamed deprecated alias
directly (e.g. type TrackInfo = types.Track with NowPlaying(TrackInfo)) emitted
$ref: #/components/schemas/TrackInfo, while the component is emitted under the
canonical name Track — a dangling export reference.

Lift the alias-to-canonical map into GenerateSchema (buildAliasToCanonical) and
apply it to export refs via canonicalRefName, the same resolution already used
for field properties.

* fix(plugins): route shared macro types through $crate for plugin builds

When a capability method used a shared type directly, the generated export macro
named the type as nd_pdk_types::SongRef. The macro expands in the downstream
plugin crate, which depends on the umbrella nd-pdk crate and not on nd-pdk-types
directly, so that path is unresolvable there and the plugin fails to build.

rustMethodType (macro-facing) now emits $crate::types::X, and the generated
nd-pdk-capabilities lib.rs re-exports nd_pdk_types as types so $crate resolves
it. Trait signatures keep nd_pdk_types::X since they live in nd-pdk-capabilities,
which has the direct dependency. Verified end-to-end: a plugin crate depending
only on the umbrella that uses a capability with a direct types.X method now
compiles via the macro.

* fix(plugins): add nd-pdk-types dependency to the Rust host crate

When a host service uses a shared type, ndpgen emits nd_pdk_types::X into the
generated nd-pdk-host client wrappers, but the host crate's manifest did not
depend on nd-pdk-types, so the crate failed to compile with 'unresolved module
nd_pdk_types'. Host client wrappers are plain functions resolved in the host
crate's own context (not macros expanded downstream), so a direct dependency is
the right fix.

Add nd-pdk-types = { path = "../nd-pdk-types" } to nd-pdk-host, mirroring
nd-pdk-capabilities. Found while auditing all Rust paths against the realistic
crate topology after the capability-side $crate fix; verified by generating a
host service with a shared-type return and confirming cargo check fails before
and passes after.

* fix(plugins): resolve shared aliases in Rust host signatures

The Rust host client rendered method params and returns through
RustTypeWithStructs, which only consults KnownStructs. A host service using a
shared alias in a signature (e.g. type Track = types.Track plus
MatchSongs(...) ([]Track, error)) therefore emitted a bare Vec<Track>, but the
client template emits no Track alias or import, so the generated nd-pdk-host
crate did not compile. Only struct fields went through the shared map.

rustType/rustParamType now use the shared map too (RustTypeWithShared /
RustParamTypeWithShared), so an aliased param/return resolves to its canonical
nd_pdk_types::X path, matching field handling. Verified by generating a host
service returning a shared alias and confirming cargo check fails before and
passes after.
2026-06-29 21:20:33 -04:00
Deluan Quintão
5a3ac80a8a
feat(cli): add a 'navidrome plugin' CLI for managing and inspecting plugins (#5682)
Some checks are pending
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (Windows) (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
* feat(plugins): export ReadPackageManifest and ValidatePackage for off-disk inspection

* test(plugins): cover ValidatePackage cross-field validation branch

* feat(cmd): add 'plugin list' command

* refactor(cmd): drop premature pluginManager interface until first use

* feat(cmd): add 'plugin enable' and 'plugin disable' commands

* feat(cmd): add 'plugin edit' for config and permission updates

* test(cmd): cover plugin edit aborting on config validation failure

* feat(cmd): add 'plugin info' and 'plugin validate' (installed id or .ndp file)

* test(cmd): unit-test formatManifestInfo nil-guard and json branches

* feat(cmd): add 'plugin rescan' command

* feat(cmd): enrich plugin info text output and re-validate config on validate

* fix(cmd): omit zero-value timestamps in plugin info text output

* fix(cmd): give plugin list and info separate format flag vars

A shared package-global bound to both commands' --format flag caused the
later init() registration (info, default "text") to clobber the earlier
one (list, default "table"), so 'plugin list' with no -f failed with
'invalid output format "text"'. Split into pluginListFormat /
pluginInfoFormat and add a regression test on the registered defaults.

* fix(plugins): address code-review findings on plugin CLI

- edit: read-modify-write merge so flipping one permission flag no longer
  wipes unspecified fields (matches the native API); reject non-JSON
  --users/--libraries values.
- info: return an error on unknown --format (was silently text); include
  sha256 in off-disk JSON output; align the text columns.
- move declaredPermissions onto plugins.Permissions.DeclaredNames() as the
  single source of truth; drop ValidatePackage's redundant Validate call.
- readConfigFile: pass ctx to log.Fatal; copy flag globals before taking
  their address; trim comments that restated the code.

* refactor(plugins): export ReadManifest/ComputeFileSHA256, drop thin CLI wrappers

Remove the ReadPackageManifest/ComputeFileSHA256/ValidatePackage wrappers in
favor of exporting the underlying readManifest->ReadManifest and
computeFileSHA256->ComputeFileSHA256 directly. ValidatePackage was identical to
ReadPackageManifest (readManifest already validates via ParseManifest), so the
CLI's validate path now calls ReadManifest too.

* test(plugins): consolidate ReadManifest specs and move ComputeFileSHA256 tests

Merge the two ReadManifest Describe blocks into one, and move the
ComputeFileSHA256 tests out of package_test.go into a new manager_sync_test.go
(where the function now lives), deduping the overlapping hash specs.

* test(plugins): use createTestPackage everywhere, drop writeNDP helper

The schema-invalid and cross-field ReadManifest specs now build packages with
createTestPackage (typed Manifest) instead of the raw-JSON writeNDP helper, so
there is a single package-building helper. Also trims the gratuitous 1MB wasm
buffer in the read-only spec to a few bytes.

* test(plugins): drop duplicate missing-manifest spec from ReadManifest

The openPackage block already covers the missing-manifest.json error path
(shared zip-walk code); ReadManifest's identical copy added no distinct
coverage.

* test(plugins): fix misleading ReadManifest spec name and drop unused wasm bytes

The spec was named 'should read only the manifest without loading wasm' but
ReadManifest returns only (*Manifest, error) — it cannot assert whether wasm was
loaded. Rename to what it actually verifies (manifest parses from a package that
also contains a wasm entry) and pass nil wasm bytes, since the contents are
never observed.

* fix(cmd): address PR review feedback on plugin CLI

- edit --users/--libraries now accept both comma-separated and JSON-array input
  (CSV is converted to the JSON the manager stores); help text documents both.
- Permissions.DeclaredNames() derives names by reflecting over the generated
  struct's json tags instead of a hand-maintained list, so new permission types
  are picked up automatically.
- isPackagePath checks only the .ndp suffix, so a mistyped package path yields a
  precise 'no such file' error instead of falling back to a misleading
  'Plugin not found'.
- Restore a ReadManifest test for the missing-manifest.json error path (it has
  its own branch, separate from openPackage).

* docs(cmd): trim verbose comments to the why

* fix(cmd): setting an explicit users/libraries list clears the all-* flag

Per Codex review: with allUsers/allLibraries true, 'plugin edit --users <list>'
preserved the all-* flag so the allow-list was silently ignored, and the
mutually-exclusive flags meant it couldn't be corrected in one command. An
explicit list now implies all-*=false.
2026-06-28 13:26:32 -04:00
Deluan Quintão
bd9fa1c602
feat(listenbrainz): match collaboration top-songs via all credited artist MBIDs (#5670)
Some checks are pending
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (Windows) (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
* refactor(agents): drop single Artist fields from Song, keep only Artists

Song now represents credited artists solely via the Artists slice; the
single Artist/ArtistMBID fields and the ArtistList passthrough are removed.
Equals continues to hash the whole value.

* refactor(lastfm,listenbrainz): build Song.Artists in built-in agents

Last.fm and ListenBrainz now populate the Artists slice directly. For
ListenBrainz top songs, all credited artist MBIDs are mapped (the combined
display name on the first credit plus MBID-only collaborators) instead of
keeping only the first MBID, feeding the matcher's per-MBID specificity.

* refactor(external): set Song.Artists in top-songs enrichment

getMatchingTopSongs now seeds an Artists entry from the known artist when a
song carries none, replacing the single Artist/ArtistMBID writes.

* refactor(plugins): fold single-artist SongRef into Song.Artists

SongRef keeps its single Artist/ArtistMBID fields as part of the plugin wire
contract; songRefToAgentSong now folds them into a one-element Artists list
when a plugin sends no artists array.

* refactor(matcher): read Song.Artists directly

groupQueries consumes s.Artists now that ArtistList is gone; test inputs
build the Artists slice.

* fix(matcher): keep MBID-only artists as identity signals

Review follow-up: an artist credited only by MBID (empty ID and name) was
dropped before resolution in four places, defeating the multi-MBID matching
path this PR adds.

- matcher.groupQueries: treat a non-empty MBID as a usable artist signal
- external.getMatchingTopSongs: backfill the primary credit's name/MBID when
  the agent left them empty
- plugins.songRefToAgentSong: fold a single-artist SongRef when only ArtistMBID
  is set (not just when Artist name is set)
- listenbrainz.topSongArtists: return nil instead of an empty-name placeholder
  when neither name nor MBIDs are present

* fix(external): only backfill top-song artist onto an unnamed credit

Review follow-up (codex P2): the previous backfill stamped the queried
artist's MBID onto Artists[0] whenever it was empty, even when that credit
already named a different (e.g. featured) artist — producing a mismatched
name+MBID pair that could mis-rank matches. Now only an unnamed first credit
is filled (it is, by construction, the queried artist); an already-named
credit is left untouched.

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-06-26 17:06:14 -04:00
Deluan Quintão
7b7721f002
feat(matcher): match similar/top songs by multiple artists (#5668)
* feat(matcher): add Song.Artists (agents.Artist) and field-wise song dedup

* refactor(matcher): make song equality an agents.Song.Equals method via hashstructure

Move the sameSong free function from core/matcher into an Equals method on
agents.Song, following the model.MediaFile/Album.Equals convention. Uses strict
hashstructure hashing (nil opts, no IgnoreZeroValue) to preserve the original
whole-value equality contract. Tests moved to core/agents.

* feat(matcher): match by multiple artists with overlap ranking and artist-ID fast-path

* refactor(matcher): rank artist overlap and specificity above the preferred-track flag

Identity signals (specificityLevel, artistOverlap) now outrank the taste
signal (preferredMatch) in betterThan. A starred/4-star track that is a worse
identity match no longer beats a more specific or higher-overlap track.
PreferStarred still breaks ties when specificity and overlap are equal.

* fix(matcher): score artist-MBID specificity against all credited artists, not just the last

sanitizedTrack.artistMBID (string) replaced with artistMBIDs (map[string]struct{}) so
bucketTracks collects all credited owned MBIDs per query instead of last-write-wins.
computeSpecificityLevel tests set membership, letting each of a collaboration's
MBID-bearing artists reach the proper specificity level (4/5) independently.

* feat(plugins): carry multiple artists (with IDs) through SongRef conversions

* feat(plugins): regenerate schemas and PDK wrappers for multi-artist SongRef

* refactor(matcher): tidy bucketTracks accumulator and artist resolution

Replace bucketTracks' two parallel per-track maps (overlapByQuery/mbidsByQuery)
with a named queryAccum struct (F2). Collapse resolveArtists' four hand-mutated
parallel maps into a pendingArtist slice with derived nameToQueries/mbidToQueries
maps (F1). Replace the own() method on resolvedArtists with a package-level
addToSet helper that drops the method/receiver indirection (F3).

* docs(matcher): trim comments that restate the code

* fix(plugins): use Vec::is_empty for slice fields in generated Rust PDK

* fix(matcher): treat a resolved artist ID as an identity match for specificity

* docs(matcher): reflect artist-ID identity in the specificity ladder
2026-06-26 14:46:09 -04:00
Deluan Quintão
aa5aa731dc
refactor(lyrics): single ParseLyrics entry point + all-format plugin lyrics (#5632)
* refactor(lyrics): read sidecar files via library storage FS

Routes fromExternalFile reads through storage.For(mf.LibraryPath) instead
of os.Open on AbsolutePath, fixing sidecar reads for non-local backends.
UTF-16 LE/BE and BOM handling preserved via ioutils.UTF8Reader.

* refactor(lyrics): address review feedback on sidecar FS read

- Move blank local-storage import from sources.go into lyrics_suite_test.go
  (the test suite already imports the local package for RegisterExtractor,
  so local's init() runs; production binaries get the scheme via normal wiring)
- Fix misleading comment: model.ParseLyrics → model.ParseLyricsFile
- Replace what-comment with why-comment in BeforeSuite explaining the
  log.Fatal guard that requires the no-op extractor registration

* test(lyrics): add subsonic e2e baseline for getLyrics endpoints

Establishes a behavioral baseline for getLyricsBySongId (v2 structured)
and getLyrics (legacy) before the lyrics parser refactor. Covers embedded
formats (LRC synced, plain text, TTML) and sidecar formats (LRC, SRT,
YAML), all isolated under a Lyrics/ fixture folder so the new fixtures
do not perturb existing test behavior beyond fixture counts.

Sidecar files are injected as raw &fstest.MapFile{Data: []byte(...)}
entries; the scanner skips non-audio extensions (.lrc, .srt, .yaml) so
they are invisible to scanning but reachable via the fake FS at request
time through fromExternalFile/storage.For.

Update album/artist/song counts in the album-list, multi-library, and
search3 empty-query tests to reflect the six new tracks (1 new artist,
1 new album, 6 new songs).

* test(lyrics): strengthen e2e lyrics baseline (lang assertions, rename helper)

Rename the local helper `main` to `firstLyric` to avoid collision with the
reserved-feeling built-in name. Add `Lang` assertions to both embedded and
sidecar DescribeTable entries, locking the current observed values: "xxx"
(ISO 639-2 "no language specified") for all embedded and LRC/SRT sidecars,
and "eng" for the YAML sidecar (which explicitly sets `language: eng`).

* feat(lyrics): detect Lyricsfile YAML in content-sniffing

* feat(plugins): content-sniff plugin lyrics for all formats

Replace model.ToLyrics (LRC/plain only) with model.ParseEmbedded so plugin
responses are content-sniffed for TTML, SRT, YAML, LRC, and plain text.
ParseEmbedded returns a LyricList, so the loop now flattens multiple tracks
per response entry.

The test-lyrics WASM plugin gains a "ttml" format mode (configured via
pdk.GetConfig) that returns a minimal TTML document; rebuilt with the
standard Go wasip1 toolchain (GOOS=wasip1 GOARCH=wasm). A new Ginkgo test
asserts Synced==true and the exact cue value, which the old plain-text path
could not produce.

GetLyrics doc comment updated to reflect content-sniffing; a later task will
retarget it to ParseLyrics once that function is introduced.

* test(plugins): validate plugin lyrics auto-detect across all formats

The test-lyrics WASM plugin now supports per-format modes via the
"format" config key: ttml, srt, yaml, lrc, and plain, in addition to
the existing default plain-text response. The plugin is rebuilt with the
standard Go wasip1 compiler.

lyrics_adapter_test.go gains a DescribeTable covering all five formats,
asserting both Synced (the discriminator that proves correct format
detection) and the exact line value. This validates the full
auto-detect chain (TTML → SRT → YAML/Lyricsfile → LRC → plain) end-to-end
through the real plugin → adapter → parser flow.

* refactor(lyrics): consolidate parsers into model.ParseLyrics

* refactor(lyrics): retarget legacy callers to model.ParseLyrics

Pin suffix to ".lrc" to preserve byte-identical output for stored
plain/LRC text that was previously handled by the now-removed ToLyrics.

* test(lyrics): fix lyrics tests after parser consolidation

- Rewrite the YAML-fallback test to assert the correct design: a
  non-Lyricsfile .yaml sidecar returns as plain text and shadows
  lower-priority sources (rather than falling through to .lrc).
- Add LibraryPath + relative Path split to the three subsonic tests
  that read sidecar files via storage.For(), so they resolve against
  the correct fixtures directory.
- Register a no-op extractor in api_suite_test.go BeforeSuite so
  newLocalStorage does not fatal when storage.For is called during
  sidecar-lyrics tests.

* test(lyrics): add per-format ParseLyrics benchmarks

Baseline measurements (count=2 runs) on M2:

BenchmarkParseLyrics_LRC-8           	    5725	    178796 ns/op	  49.78 MB/s	  427877 B/op	     523 allocs/op
BenchmarkParseLyrics_Plain-8         	    5425	    230854 ns/op	  32.44 MB/s	  102508 B/op	      16 allocs/op
BenchmarkParseLyrics_EnhancedLRC-8   	    1942	    605893 ns/op	  17.66 MB/s	  860678 B/op	    4256 allocs/op
BenchmarkParseLyrics_SRT-8           	    3249	    373991 ns/op	  25.91 MB/s	 1113575 B/op	    4407 allocs/op
BenchmarkParseLyrics_TTML-8          	    1483	    813027 ns/op	  13.86 MB/s	 2198052 B/op	    8665 allocs/op
BenchmarkParseLyrics_YAML-8          	    1700	    678250 ns/op	  13.01 MB/s	 1235096 B/op	    8288 allocs/op
BenchmarkParseLyrics_SniffTTML-8     	    1525	    776482 ns/op	  14.51 MB/s	 2225448 B/op	    8681 allocs/op
BenchmarkParseLyrics_SniffSRT-8      	    2528	    451210 ns/op	  21.48 MB/s	 1157000 B/op	    4422 allocs/op
BenchmarkParseLyrics_SniffYAML-8     	    1333	    827152 ns/op	  10.67 MB/s	 1337195 B/op	    8718 allocs/op
BenchmarkParseLyrics_SniffLRC-8      	    2820	    413038 ns/op	  21.55 MB/s	  588934 B/op	    1812 allocs/op
BenchmarkParseLyrics_SniffPlain-8    	    2968	    409091 ns/op	  18.31 MB/s	  254470 B/op	    1491 allocs/op

Content-sniff path overhead: 1.5–15% depending on format.

* test(lyrics): use real public-domain fixtures for parser benchmarks

Replace synthetic benchmark payloads with 'Auld Lang Syne' (Robert Burns,
1788, public domain) rendered into every supported format (LRC, plain,
enhanced LRC, SRT, TTML, Lyricsfile YAML) so the numbers reflect realistic
content. Same song across formats makes per-format cost comparable.

Baseline (Apple M-series, -benchmem, real fixtures):
  LRC          ~28 us/op   42 KB   147 allocs
  Plain        ~23 us/op   18 KB    22 allocs
  EnhancedLRC  ~37 us/op   51 KB   374 allocs
  SRT          ~52 us/op  139 KB   581 allocs
  TTML        ~119 us/op  276 KB  1227 allocs
  YAML        ~142 us/op  193 KB  1732 allocs
  Sniff(LRC)   ~47 us/op   57 KB   237 allocs
  Sniff(TTML) ~122 us/op  282 KB  1250 allocs
  Sniff(YAML) ~186 us/op  218 KB  1847 allocs

Fixtures in tests/fixtures/lyrics/.

* fix(lyrics): preserve [] (not null) for empty lyrics in backfill migration

ParseLyrics returns nil for zero-line input (whitespace-only stored
lyrics). json.Marshal(nil LyricList) produces null, violating the DB
invariant that media_file.lyrics uses [] for empty lyrics, never null.
Initialize to model.LyricList{} when ParseLyrics returns nil so the
marshalled result is always [].

* refactor(lyrics): unify parser dispatch and centralize empty-list invariant

Apply thermo-nuclear review findings (behavior-preserving):

- Replace the suffix switch + three single-use closure adapters
  (parseTTMLKnown/parseSRTKnown + inline YAML closure) with a
  bySuffix map of a single lyricParser(lang, contents) signature.
  Normalize parseTTMLWithDefaultLang/parseSRTWithLanguage to that
  (lang, contents) order so no adapter glue is needed.
- Collapse the parallel sniffLyrics engine into one parseFirstMatch
  primitive shared by both the suffix and content-sniff paths
  (sniffOrder candidate list). TTML stays gated via parseTTMLIfDocument
  in sniff mode to avoid running the XML decoder on plain/LRC text.
- Add LyricList.MarshalJSON so empty/nil always serializes to [] (the
  lyrics column invariant), in one canonical place. Delete the
  migration's nil-guard, which the marshaler now subsumes.

Behavior verified unchanged: full suite + race + e2e green.

* refactor(lyrics): single registry drives both suffix dispatch and sniff order

Collapse the bySuffix map and sniffOrder slice into one ordered registry:
slice order is the content-sniff probe order, each row's suffixes drive
sidecar dispatch, and per-row bySuffix/byContent parsers preserve the
gated-TTML-when-sniffing distinction. One source of truth, no duplicated
parser references.

* refactor(lyrics): self-skipping parsers collapse the format table to one column

Move the TTML <tt>-document gate into parseTTMLWithDefaultLang itself (after
the encoding fixup, so UTF-16-declared docs are still recognized): non-TTML
content returns (nil, nil) to skip; a malformed <tt> document still errors.
SRT and Lyricsfile YAML already self-skip. With every structured parser
self-skipping, the format table drops to one {suffixes, parse} column named
lyricFormats — no bySuffix/byContent split, no separate sniff-only TTML gate.
Both the suffix and content-sniff paths share the same parser per format.

* refactor(lyrics): strip BOM once at ParseLyrics entry for all paths

Previously only the content-sniff path stripped the BOM; the suffix path
relied on its callers (fromExternalFile via UTF8Reader) having already
stripped it. That implicit contract was fragile — a caller passing raw
BOM-prefixed bytes with a suffix would reach the parsers with the BOM intact
(SanitizeText does not strip it). Strip once at entry so every path and
parser sees clean bytes regardless of caller. No-op for already-stripped
input.

* refactor(lyrics): trim verbose comments to essential why

* refactor(lyrics): move LRC parser to its own lyrics_lrc.go

Extract parseLRC, the enhanced-LRC helpers (parseEnhancedLine, adjustGroup,
stripEnhancedMarkers, shiftELRCCues), parseTime, and the LRC regexes from
lyrics.go into lyrics_lrc.go, with the parseLRC tests in lyrics_lrc_test.go.
This makes the layout symmetric — one file per format (lrc/srt/ttml/yaml) —
and leaves lyrics.go holding only shared types and cue normalization. All
moved symbols were already LRC-private; no behavior change.

* refactor(lyrics): collapse ParseLyrics suffix/sniff branches into one loop

Both modes differ only in which formats to try, so select candidates in a
single loop (all formats when sniffing, the suffix's own otherwise) and run
them through parseFirstMatch once. Drops the projected-slice make+index and
the ContainsFunc closure; unmatched suffixes yield no candidates and fall to
the plain-text floor, as before.

* refactor(lyrics): apply simplify-review cleanups

- stripBOM: bytes.TrimPrefix instead of []byte<->string round-trip (no alloc)
- ParseLyrics: pre-size the candidates slice
- move isTTMLDocument to lyrics_ttml.go beside its only caller (the dispatch
  layer should hold no per-format knowledge)

* refactor(lyrics): simplify test descriptions for structured lyrics

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

* refactor(lyrics): fold parseLyricsfile into lyricParser signature and rename file

- parseLyricsfile now matches the lyricParser signature directly (reads via
  bytes.NewReader), removing the parseLyricsfileBytes adapter and the
  string(contents) copy; the lyricFormats table references it directly.
- StructuredLyrics drops the vestigial LyricList{} init (json.Unmarshal
  overwrites; MarshalJSON owns the empty->[] invariant).
- Rename lyricsfile.go -> lyrics_lyricsfile.go (and its test) to match the
  lyrics_<format>.go convention used by lrc/srt/ttml.

* refactor(lyrics): move test-only parseTTML/parseSRT wrappers to test files

These zero-arg wrappers (defaulting lang to "xxx") had no production callers
after the consolidation — only the format tests used them. Move each beside
its tests so the production files carry no test-only code.

* build: exclude generated *_gen.go files from linting

The plugin host *_gen.go files (ndpgen output) were tripping the whitespace
linter despite carrying a generated marker. Exclude them by path so make lint
and the pre-push hook pass on untouched generated code.

* perf(lyrics): drop []byte/string round-trips in parsers

Apply code-review feedback to remove avoidable allocations in the lyrics
parsers. isTTMLDocument now takes []byte directly, so parseTTMLWithDefaultLang
no longer copies its buffer into a string before the TTML probe. parseSRTBlock
splits its block with strings.Split instead of converting to []byte and back
per line. ParseLyrics hoists strings.ToLower(suffix) out of the format loop.

No behavior change; the dropped len(scanner)==0 SRT guard was dead (strings.Split
never returns an empty slice, and the existing len(lines)==0 check still covers
empty input).

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

* refactor(lyrics): colocate and unexport cue-normalization helpers

Move the cue-normalization machinery out of lyrics.go into a dedicated
lyrics_normalize.go (with lyrics_normalize_test.go), leaving lyrics.go to hold
just the shared lyric types and their methods. lyrics.go was mixing the domain
type/contract definitions with format-agnostic post-processing.

Unexport normalizeLyrics, normalizeCueLines, and normalizeLineTiming: they have
no callers outside the model package, so they should not be part of its public
API. NormalizeCueEnds stays exported because the Subsonic enhanced-lyrics
serializer (server/subsonic/lyrics.go) resolves cue ends per agent group while
building the response; that is the only legitimate cross-package caller.

Also includes a small no-op robustness tweak in parseLRC: len(times) == 0
instead of times == nil (equivalent here, more idiomatic).

No behavior change.

* test(lyrics): add direct coverage for NormalizeCueEnds

NormalizeCueEnds is exported and carries the most intricate logic in the
normalization cluster (fill-from-next, fill-from-fallback, both clamps, and the
all-or-none clear), but was only exercised transitively. Add a focused spec
covering each branch plus the empty-input and no-mutation guarantees, bringing
the function to 100% coverage.

* test(lyrics): cover legacy getLyrics across formats and sources

Expand the legacy getLyrics e2e coverage from a single embedded-plain case to a
table over all six fixtures: embedded LRC/plain/TTML and sidecar LRC/SRT/YAML.

Each case asserts the v1 plain-text fallback contract — the structured lyric is
flattened to LRC-style plain text with no timing markup leaking through (no LRC
brackets, SRT arrows, or XML tags), regardless of the source format or whether
it is embedded or a sidecar file. This pins the behavior that synced TTML/SRT/
YAML formats degrade gracefully to plain text on the legacy endpoint.

* test(lyrics): cover songLyrics v1 vs v2 with word-level fixtures

Correct and expand the e2e lyrics coverage to match the OpenSubsonic songLyrics
extension contract:

- v1 (getLyricsBySongId, no enhanced): line-level lyrics with no cueLine, kind,
  or agents — even for word-level formats (ELRC, Lyricsfile YAML).
- v2 (getLyricsBySongId?enhanced=true): word-level cueLine surfaces for ELRC and
  YAML sources; kind="main" is set; a line-level source (SRT) still yields no
  cueLine even when enhanced.
- legacy getLyrics (artist/title): the original Subsonic endpoint, flattening any
  format to plain text. A prior commit mislabeled this as the "v1 contract";
  getLyrics predates OpenSubsonic and is unrelated to the extension versions.

Drive these with the public-domain tests/fixtures/lyrics files (the same set the
parser benchmarks use) so the e2e content stays in sync and actually carries the
word-level timing needed to distinguish v1 from v2. The embedded "synced LRC"
fixture is upgraded to ELRC (word-level); track counts are unchanged, so the
rest of the suite is unaffected.

* test(lyrics): parameterize v2 enhanced coverage across all formats

Convert the v2 (enhanced) e2e block from three ad-hoc cases into a DescribeTable
covering all six formats, matching the v1 and legacy tables. Each entry declares
whether the source carries word-level timing: ELRC, TTML, and Lyricsfile YAML
surface a cueLine; LRC, SRT, and plain text do not. All six get kind="main".

Add word-level <span> timing to the first line of the auld-lang-syne.ttml
fixture so TTML exercises the word-level cueLine path (the parser already
supports <span begin/end>, but the fixture was line-level only). The first line
now yields the same five word cues as the ELRC and YAML fixtures, keeping the
table assertions uniform across formats.

* fix(lyrics): honor caller language when Lyricsfile YAML omits it

parseLyricsfile discarded the caller's language argument, so a Lyricsfile YAML
parsed from an embedded tag or plugin response with no metadata.language was
labeled "xxx" even when ParseLyrics was given a language. The SRT and TTML
parsers already use the caller language as their default; fall back to it here
too, preferring the document's own metadata.language when present.

Also reword a misleading TTML comment: isTTMLDocument still runs an XML decode
(it stops at the first element), so the skip avoids the full TTML parse, not the
XML decoder entirely.

* refactor(lyrics): consolidate lyrics parsing functions names

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

* test(lyrics): drop test-only parse wrappers after parser rename

Commit 48c0173e8 renamed the production parsers to parseTTML/parseSRT, which
collided with the same-named test-only wrappers and broke the model test build
(parseTTML/parseSRT redeclared). Remove the wrappers and call the production
parsers directly with the placeholder language at each test site.

* test(lyrics): complete the truncated enhanced-LRC fixture

The auld-lang-syne.elrc fixture stopped after the first two stanzas (8 lyric
lines) while every other format fixture carries the full 24-line song. Extend it
to all 24 lines with per-word timing so it is a faithful enhanced-LRC sample and
the EnhancedLRC parser benchmark runs on a workload comparable to the others.
The first line's word timings are unchanged, so the e2e cueLine assertions still
hold.

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-06-19 18:25:35 -04:00
Yuuta
3a14faa033
feat(subsonic): add structured sidecar lyrics support with OpenSubsonic v2 karaoke cues and agent layers (#5076)
Some checks are pending
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (Windows) (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
Expand backend lyrics support with richer sidecar formats and upgrade the
OpenSubsonic songLyrics implementation to the version 2 structured karaoke
contract, while preserving version 1 behavior by default.

Sidecar formats and parsing:
- Add a TTML parser (core/lyrics/ttml.go): clock time, offset time, bare
  decimal seconds, nested timing contexts, and token-level <span> timing for
  word/syllable karaoke. Parses Apple Music-style metadata tracks (translation
  and pronunciation/transliteration) and agent metadata into per-track agents[]
  plus per-cue-line agentId. Hydrates missing line timing from cue timing.
- Add an SRT parser (core/lyrics/srt.go).
- Add a LRCLIB Lyricsfile (.yaml/.yml) parser (model/lyricsfile.go): maps
  per-word lines[].words[] to cues with inclusive UTF-8 byte offsets and
  attributes overlapping lines to synthetic voice agents so parallel vocals
  split correctly in the enhanced response.
- Extend LRC parsing for Enhanced LRC inline <mm:ss.xx> word-timing markers.
- Add UTF-8 BOM and UTF-16 LE support for TTML/LRC sidecars.
- Parse the above formats from embedded tags as well as sidecar files.

Source resolution:
- Default lyricspriority is now
  ".ttml,.yaml,.yml,.elrc,.lrc,.srt,.txt,embedded" so the new formats are
  discoverable without manual configuration.
- Preserve configured source priority across duplicate media-file candidates
  instead of only checking the first DB match, so higher-priority sidecar
  lyrics on older duplicates can still win.
- Raise the embedded-lyrics tag maxLength to 1 MB to fit word-timed
  TTML/Enhanced-LRC karaoke for a full song.

OpenSubsonic songLyrics v2:
- Advertise songLyrics versions [1, 2].
- With enhanced=true, getLyricsBySongId may return structuredLyrics.kind
  (main/translation/pronunciation), cueLine[] line-level karaoke groupings,
  cueLine.cue[] timed words/syllables with required UTF-8 byteStart/byteEnd,
  reusable structuredLyrics.agents[], and cueLine.agentId references.
- Without enhanced=true, the response stays v1-compatible: no kind, no cueLine,
  no agents, no non-main tracks; the existing line[] payload is always
  populated so legacy clients keep working.

Contract details:
- cueLine is emitted only for synced lyrics with cue data.
- Within a cueLine, cue.end is normalized all-or-none and overlaps are removed;
  overlaps across separate cueLines remain valid for parallel vocal layers.
- Missing cue end-times are filled from the next cue or the parent line.
- When cueLines share an index, the one whose agent has role "main" is first.
- LyricCue.Value is serialized as XML chardata; cues with nil start are skipped
  rather than serialized as 0.

Refactoring:
- Move pure format parsers into model/ (lyrics.go, lyrics_ttml.go,
  lyrics_srt.go, lyrics_embedded.go, lyricsfile.go) and extract Subsonic
  response building into server/subsonic/lyrics.go.
- Centralize lyric-kind constants and add Lyrics.EffectiveKind/IsMainKind.
- Add gg.Clone helper.

Spec references:
  https://github.com/opensubsonic/open-subsonic-api/discussions/213
  https://github.com/opensubsonic/open-subsonic-api/pull/218 (songLyrics v2)
  https://github.com/opensubsonic/open-subsonic-api/pull/228 (cue byte offsets)
2026-06-19 12:00:58 -04:00
Deluan
2a43c4683e chore: go fix
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (Windows) (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-1 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-2 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-3 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-4 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-5 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-6 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-7 (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-8 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-9 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-10 (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-28 22:13:05 -03:00
Deluan
03ac02d964 refactor: more warnings clean up
Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-20 17:43:12 -03:00
Deluan
efe9291db0 refactor: multiple syntax updates for Go 1.26
Some checks failed
POEditor export / push-translations (push) Has been cancelled
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (Windows) (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-1 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-2 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-3 (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-4 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-5 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-6 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-7 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-8 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-9 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-10 (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-19 18:02:36 -03:00
Deluan Quintão
8f0b4930ff
refactor(conf): replace eager dir creation with lazy Dir type (#5495)
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (Windows) (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-1 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-2 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-3 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-4 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-5 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-6 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-7 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-8 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-9 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-10 (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
* 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
97c01c0614 fix(plugins): add debug logs to syncPlugins for troubleshooting
Some checks are pending
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (Windows) (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Log skipped entries, total entries vs plugins found, and DB state
to help diagnose why sync may not find new plugins.

Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-05 18:58:50 -04:00
Deluan Quintão
ae0e0c89d9
feat(plugins): add PlaybackReport to scrobbler capability (#5452)
Some checks are pending
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (Windows) (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
* feat(plugins): add PlaybackReport to Scrobbler interface and all implementations

* feat(plugins): add PlaybackReport worker and dispatch in PlayTracker

* feat(plugins): add PlaybackReportRequest to plugin scrobbler capability

* chore(plugins): regenerate PDK files with PlaybackReport

* feat(plugins): add PlaybackReport to test scrobbler plugin

* feat(plugins): add PlaybackReport to plugin scrobbler adapter

* refactor(plugins): fix double DB fetch in StateStopped and batch getActiveScrobblers

- Hoist mf from scrobble branch so PlaybackReport reuses it instead of
  fetching again from DB
- Call getActiveScrobblers once per drain batch instead of per-entry

* chore(plugins): include generated scrobbler schema with PlaybackReport

* fix(plugins): skip PlaybackReport for plugins that don't export it

Plugins detected as scrobblers only need to export one scrobbler
function. Older plugins that don't export nd_scrobbler_playback_report
would cause noisy error logs on every reportPlayback call. Now
errFunctionNotFound and errNotImplemented are treated as no-ops.

* refactor: rename NowPlayingInfo to PlaybackReport

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

* refactor: rename stopNowPlayingWorker to stopBackgroundWorkers

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

* refactor: move NowPlaying and PlaybackReport logic to separate worker files

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

* refactor(scrobbler): rename NowPlayingInfo to PlaybackSession and add expired state

Rename NowPlayingInfo struct to PlaybackSession to better reflect its role
as a complete playback session representation. Add UserId field to make
sessions self-contained, removing redundant userId parameters from
PlaybackReport interface method and internal dispatch functions. Introduce
StateExpired internal state that fires when a session cache entry expires
without an explicit stop, ensuring plugins always receive a terminal event
regardless of client behavior.

* fix(scrobbler): update playback state description to include 'expired'

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

* fix(scrobbler): resolve data race in OnExpiration callback

Capture conf.Server.EnableNowPlaying at construction time instead of
reading it from the background ttlcache eviction goroutine. The previous
code raced with test config cleanup that writes to the same field
concurrently.

* fix(scrobbler): return error when media file lookup fails in StateStopped

Simplify the MediaFile population logic in the stopped case to return an
error if the track cannot be found. A stop report with an empty MediaFile
is useless to plugins, and returning the error allows clients to retry
or alert the user when auto-scrobble is enabled.

* refactor(scrobbler): use session data directly in PlaybackReport adapter

Use info.Username from PlaybackSession instead of extracting it from
context in the plugin adapter, since the session is now self-contained.
Add debug/trace logging for session expiration and enqueue the expired
report with a user-enriched context so downstream handlers can identify
the user.

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-02 16:14:53 -04:00
Deluan Quintão
259c1a9484
feat(subsonic): add sonicSimilarity extension as plugin capability (#5419)
* feat(plugins): add sonicSimilarity capability types

Defines the SonicSimilarity plugin capability interface with
GetSonicSimilarTracks and FindSonicPath methods, along with
their request/response types.

* feat(sonic): add core sonic similarity service

Implements the Sonic service with HasProvider, GetSonicSimilarTracks,
and FindSonicPath, delegating to the PluginLoader and using the
Matcher for index-preserving library resolution.

* test(sonic): add sonic service unit tests

Covers HasProvider, GetSonicSimilarTracks, and FindSonicPath with
mock plugin loader and provider, verifying error propagation and
successful match resolution via the library matcher.

* feat(matcher): add MatchSongsToLibraryMap for index-preserving matching

Adds a new method alongside MatchSongsToLibrary that returns a
map[int]MediaFile keyed by input song index rather than a flat slice,
enabling callers to correlate similarity scores back to the original
position in the results.

* fix(sonic): check provider availability before MediaFile lookup

Avoids unnecessary DB call when no plugin is available, and ensures
the correct error path is tested.

* feat(plugins): add sonic similarity adapter

Adds SonicSimilarityAdapter implementing sonic.Provider, bridging the
plugin system to the core sonic service via Extism plugin functions.
Reuses existing songRefsToAgentSongs helper for SongRef conversion.

* feat(plugins): add LoadSonicSimilarity to plugin manager

Adds Manager.LoadSonicSimilarity method following the pattern of
LoadLyricsProvider, enabling the core sonic service to load a
SonicSimilarityAdapter from a named plugin.

* feat(subsonic): add sonicMatch response type

Add SonicMatch struct with Entry and Similarity fields, and a SonicMatches slice to the Subsonic response struct. These types support the OpenSubsonic sonicSimilarity extension for returning similarity-scored track results.

* feat(subsonic): add getSonicSimilarTracks and findSonicPath handlers

Add two new Subsonic API handlers for the sonicSimilarity OpenSubsonic extension: GetSonicSimilarTracks returns similarity-scored tracks similar to a given song, and FindSonicPath returns a path of tracks connecting two songs. Both handlers delegate to the sonic core service and map results to SonicMatch response types.

* feat(subsonic): advertise sonicSimilarity extension when plugin available

Update GetOpenSubsonicExtensions to conditionally include the sonicSimilarity extension only when a sonic similarity plugin provider is available. The nil guard ensures backward compatibility with tests that pass nil for the sonic field. Also update the existing test to pass the new nil parameter.

* feat(subsonic): wire sonic similarity service into router

Add the sonic.Sonic service to the Router struct and New() constructor, register the getSonicSimilarTracks and findSonicPath routes, and wire sonic.New and its PluginLoader binding into the Wire dependency injection graph. Update all existing test call sites to pass the new nil parameter. Regenerate wire_gen.go.

* fix(e2e): add sonic parameter to subsonic.New call in e2e tests

* test(subsonic): add sonicSimilarity extension advertisement tests

Restructures the GetOpenSubsonicExtensions test into two contexts: one verifying the baseline 5 extensions are returned when no sonic similarity plugin is configured, and one verifying that the sonicSimilarity extension is advertised (making 6 total) when a plugin loader reports an available provider. Adds a mockSonicPluginLoader to satisfy the sonic.PluginLoader interface without requiring a real plugin.

* feat(subsonic): add nil guard and e2e tests for sonic similarity endpoints

Handlers return ErrorDataNotFound when no sonic service is configured,
preventing nil panics. E2e tests verify both endpoints return proper
error responses when no plugin is available.

* fix(subsonic): return HTTP 404 when no sonic similarity plugin available

Endpoints are always registered but return 404 when no provider is
available, rather than a subsonic error code 70.

* refactor: clean up sonic similarity code after review

Extract shared helpers to reduce duplication across the sonic similarity
implementation: loadAllMatches in matcher consolidates the 4-phase
matching pipeline, songRefToAgentSong eliminates per-iteration slice
allocation in the adapter, sonicMatchResponse deduplicates response
building in handlers, and a package-level constant replaces raw
capability name strings in core/sonic.

* fix empty response shapes

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

* test(plugins): add testdata plugin and e2e tests for sonic similarity

Add a test-sonic-similarity WASM plugin that implements both
GetSonicSimilarTracks and FindSonicPath via the generated sonicsimilarity
PDK. The plugin returns deterministic test data with decreasing similarity
scores and supports error injection via config. Adapter tests verify the
full round-trip through the WASM plugin including error handling. Also
includes regenerated PDK code from make gen.

* docs: update README to include new capabilities and usage examples for plugins

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

* test(e2e): enhance sonic similarity tests with additional scenarios and mock provider

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

* fix: address PR review feedback for sonic similarity

Fix incorrect field names in README documentation ({from, to} →
{startSong, endSong}) and remove unnecessary XML serialization test
from e2e suite since OpenSubsonic endpoints only use JSON.

* refactor: rename Matcher methods for conciseness

Rename MatchSongsToLibrary to MatchSongs and MatchSongsToLibraryMap to
MatchSongsIndexed. The Matcher receiver already establishes the "to
library" context, making that suffix redundant, and "Indexed" better
describes the intent (preserving input ordering) than "Map" which
describes the data structure.

* refactor: standardize variable naming for media files in sonic path methods

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

* refactor: simplify plugin loading by introducing adapter constructors

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-27 17:50:09 -04:00
Deluan
fd930eefd7 feat(plugins): add LibraryID to TrackInfo
Add LibraryID field to TrackInfo so plugins with library filesystem access
can determine which library a track belongs to. This lets plugins resolve
the full filesystem path by combining the library's root path with the
track's relative path. LibraryID is gated behind the same filesystem
access permission check as Path.
2026-04-26 16:36:57 -04:00
Deluan Quintão
64c8d3f4c5
ci: run Go tests on Windows (#5380)
Some checks are pending
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (Windows) (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
* 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
Jorge Pardo Pardo
85e9982b43
feat(plugins): add path to Scrobbler and Lyrics plugin TrackInfo (#5339)
* feat: add Path to TrackInfo struct

* refactor: improve naming to follow the rest of the code

* test: add tests

* fix: actually check for filesystem permission

* refactor: remove library logic from specific plugins

* refactor: move hasFilesystemPermission to a Manifest method

* test(plugins): add unit tests for hasLibraryFilesystemAccess method

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

* refactor(plugins): remove hasFilesystemPerm field and use manifest for filesystem permission checks

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

* refactor(plugins): streamline library filesystem access checks in lyrics and scrobbler adapters

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan <deluan@navidrome.org>
2026-04-12 10:27:58 -04:00
Deluan
478845bc5d fix(plugins): fix race between KVStore cleanup goroutine and Close (navidrome/apple-music-plugin#7)
Some checks are pending
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
The cleanupLoop goroutine could execute cleanupExpired against a closed
database because Close() did not wait for the goroutine to exit before
calling db.Close(). This caused 'sql: database is closed' errors during
plugin unload or shutdown.

Close() now cancels the cleanup goroutine's context and waits for it to
finish via a sync.WaitGroup before running the final cleanup and closing
the database.

Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-06 22:31:30 -04:00
Deluan
4cca7bce4e test: increase FlakeAttempts for library directory tests and remove flaky job test 2026-03-23 11:59:11 -04:00
Deluan
03844a9a36 feat(plugins): add NoFollowRedirects option to HTTPRequest
Some checks are pending
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
Allow plugins to opt out of automatic redirect following on a per-request
basis. When set to true, the response returns the redirect status code and
Location header directly instead of following to the final destination.
2026-03-20 18:16:07 -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
3cd5d16b0a chore: upgrade golangci-lint to 2.11 and fix lint issues
Some checks are pending
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-06 19:23:47 -05:00
Deluan Quintão
f03ca44a8e
feat(plugins): add lyrics provider plugin capability (#5126)
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-1 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-2 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-3 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-4 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-5 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-6 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-7 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-8 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-9 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-10 (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
* feat(plugins): add lyrics provider plugin capability

Refactor the lyrics system from a static function to an interface-based
service that supports WASM plugin providers. Plugins listed in the
LyricsPriority config (alongside "embedded" and file extensions) are
now resolved through the plugin system.

Includes capability definition, Go/Rust PDK, adapter, Wire integration,
and tests for plugin fallback behavior.

* test(plugins): add lyrics capability integration test with test plugin

* fix(plugins): default lyrics language to 'xxx' when plugin omits it

Per the OpenSubsonic spec, the server must return 'und' or 'xxx' when
the lyrics language is unknown. The lyrics plugin adapter was passing
an empty string through when a plugin didn't provide a language value.
This defaults the language to 'xxx', consistent with all other callers
of model.ToLyrics() in the codebase.

* refactor(plugins): rename lyrics import to improve clarity

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

* refactor(lyrics): update TrackInfo description for clarity

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

* fix(lyrics): enhance lyrics plugin handling and case sensitivity

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

* fix(plugins): update payload type to string with byte format for task data

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-03 15:48:39 -05:00
Deluan
eeb1bd5f41 fix(plugins): update payload type to string with byte format for task data
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-03 13:54:43 -05:00
Deluan Quintão
668869b6c7
feat(plugins): add TaskQueue host service for persistent background task queues (#5116)
* feat(plugins): define TaskQueue host service interface

Add the TaskQueueService interface with CreateQueue, Enqueue,
GetTaskStatus, and CancelTask methods plus QueueConfig struct.

* feat(plugins): define TaskWorker capability for task execution callbacks

* feat(plugins): add taskqueue permission to manifest schema

Add TaskQueuePermission with maxConcurrency option.

* feat(plugins): implement TaskQueue service with SQLite persistence and workers

Per-plugin SQLite database with queues and tasks tables. Worker goroutines
dequeue tasks and invoke nd_task_execute callback. Exponential backoff
retries, rate limiting via delayMs, automatic cleanup of terminal tasks.

* feat(plugins): require TaskWorker capability for taskqueue permission

* feat(plugins): register TaskQueue host service in manager

* feat(plugins): add test-taskqueue plugin for integration testing

* feat(plugins): add integration tests for TaskQueue host service

* docs: document TaskQueue module for persistent task queues

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

* fix(plugins): harden TaskQueue host service with validation and safety improvements

Add input validation (queue name length, payload size limits), extract
status string constants to eliminate raw SQL literals, make CreateQueue
idempotent via upsert for crash recovery, fix RetentionMs default check
for negative values, cap exponential backoff at 1 hour to prevent
overflow, and replace manual mutex-based delay enforcement with
rate.Limiter from golang.org/x/time/rate for correct concurrent worker
serialization.

* refactor(plugins): remove capability check for TaskWorker in TaskQueue host service

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

* fix(plugins): use context-aware database execution in TaskQueue host service

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

* refactor(plugins): streamline task queue configuration and error handling

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

* feat(plugins): increase maxConcurrency for task queue and handle budget exhaustion

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

* refactor(plugins): simplify goroutine management in task queue service

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

* feat(plugins): update TaskWorker interface to return status messages and refactor task queue service

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

* feat(plugins): add ClearQueue function to remove pending tasks from a specified queue

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

* refactor(plugins): use migrateDB for task queue schema and fix constant name collision

Replaced the raw db.Exec call in createTaskQueueSchema with migrateDB,
matching the pattern used by createKVStoreSchema. This enables version-tracked
schema migrations via SQLite's PRAGMA user_version, allowing future schema
changes to be appended incrementally. Also renamed cleanupInterval to
taskCleanupInterval to resolve a redeclaration conflict with host_kvstore.go.

* regenerate PDKs

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-03 13:48:49 -05:00
Deluan
6fd044fb09 feat(plugins): change websockets Data field type to []byte for binary support
Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-02 16:38:00 -05:00
Deluan Quintão
30df004d4d
test(plugins): speed up integration tests (~45% improvement) (#5137)
* test(plugins): speed up integration tests with shared wazero cache

Reduce plugin test suite runtime from ~22s to ~12s by:

- Creating a shared wazero compilation cache directory in TestPlugins()
  and setting conf.Server.CacheFolder globally so all test Manager
  instances reuse compiled WASM binaries from disk cache
- Moving 6 createTestManager* calls from inside It blocks to BeforeAll
  blocks in scrobbler_adapter_test.go and manager_call_test.go
- Replacing time.Sleep(2s) in KVStore TTL test with Eventually polling
- Reducing WebSocket callback sleeps from 100ms to 10ms

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

* test(plugins): enhance websocket tests by storing server messages for verification

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-02 16:18:30 -05:00
Deluan
82f9f88c0f refactor(auth): replace untyped JWT claims with typed Claims struct
Some checks are pending
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
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
c4fd8e3125 fix(plugins): resolve kvstore TTL flaky test due to second-boundary race
Changed the TTL expiration check from strict greater-than to greater-or-equal
in the notExpiredFilter SQL condition. SQLite's datetime has second-level
precision, so a 1-second TTL set late in a second could appear expired
immediately when read at the next second boundary (e.g. expires_at of T+1
fails the check 'T+1 > T+1'). Updated the cleanup query consistently to use
strict less-than, so rows are only deleted after their expiration second has
fully passed.
2026-03-02 11:20:25 -05:00
Deluan
27a83547f7 fix(plugins): clear plugin errors on startup to allow retrying
Plugins that entered an error state (e.g., incompatible with the
Navidrome version) would remain in that state across restarts, blocking
the user from retrying. This adds a ClearErrors method to
PluginRepository that resets the last_error field on all plugins, and
calls it during plugin manager startup before syncing and loading.

Signed-off-by: Deluan <deluan@navidrome.org>
2026-03-02 08:56:56 -05:00
Deluan Quintão
2471bb9cf6
feat(plugins): add TTL support, batch operations, and hardening to kvstore (#5127)
Some checks are pending
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
* feat(plugins): add expires_at column to kvstore schema

* feat(plugins): filter expired keys in kvstore Get, Has, List

* feat(plugins): add periodic cleanup of expired kvstore keys

* feat(plugins): add SetWithTTL, DeleteByPrefix, and GetMany to kvstore

Add three new methods to the KVStore host service:

- SetWithTTL: store key-value pairs with automatic expiration
- DeleteByPrefix: remove all keys matching a prefix in one operation
- GetMany: retrieve multiple values in a single call

All methods include comprehensive unit tests covering edge cases,
expiration behavior, size tracking, and LIKE-special characters.

* feat(plugins): regenerate code and update test plugin for new kvstore methods

Regenerate host function wrappers and PDK bindings for Go, Python,
and Rust. Update the test-kvstore plugin to exercise SetWithTTL,
DeleteByPrefix, and GetMany.

* feat(plugins): add integration tests for new kvstore methods

Add WASM integration tests for SetWithTTL, DeleteByPrefix, and GetMany
operations through the plugin boundary, verifying end-to-end behavior
including TTL expiration, prefix deletion, and batch retrieval.

* fix(plugins): address lint issues in kvstore implementation

Handle tx.Rollback error return and suppress gosec false positive
for parameterized SQL query construction in GetMany.

* fix(plugins): Set clears expires_at when overwriting a TTL'd key

Previously, calling Set() on a key that was stored with SetWithTTL()
would leave the expires_at value intact, causing the key to silently
expire even though Set implies permanent storage.

Also excludes expired keys from currentSize calculation at startup.

* refactor(plugins): simplify kvstore by removing in-memory size cache

Replaced the in-memory currentSize cache (atomic.Int64), periodic cleanup
timer, and mutex with direct database queries for storage accounting.
This eliminates race conditions and cache drift issues at negligible
performance cost for plugin-sized datasets. Also unified Set and
SetWithTTL into a shared setValue method, simplified DeleteByPrefix to
use RowsAffected instead of a transaction, and added an index on
expires_at for efficient expiration filtering.

* feat(plugins): add generic SQLite migration helper and refactor kvstore schema

Add a reusable migrateDB helper that tracks schema versions via SQLite's
PRAGMA user_version and applies pending migrations transactionally. Replace
the ad-hoc createKVStoreSchema function in kvstore with a declarative
migrations slice, making it easy to add future schema changes. Remove the
now-redundant schema migration test since migrateDB has its own test suite
and every kvstore test exercises the migrations implicitly.

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

* fix(plugins): harden kvstore with explicit NULL handling, prefix validation, and cleanup timeout

- Use sql.NullString for expires_at to explicitly send NULL instead of
  relying on datetime('now', '') returning NULL by accident
- Reject empty prefix in DeleteByPrefix to prevent accidental data wipe
- Add 5s timeout context to cleanupExpired on Close
- Replace time.Sleep in unit tests with pre-expired timestamps

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

* refactor(plugins): use batch processing in GetMany

Process keys in chunks of 200 using slice.CollectChunks to avoid
hitting SQLite's SQLITE_MAX_VARIABLE_NUMBER limit with large key sets.

* feat(plugins): add periodic cleanup goroutine for expired kvstore keys

Use the manager's context to control a background goroutine that purges
expired keys every hour, stopping naturally on shutdown when the context
is cancelled.

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-28 23:12:17 -05:00
Deluan Quintão
d9a215e1e3
feat(plugins): allow mounting library directories as read-write (#5122)
Some checks are pending
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
POEditor export / push-translations (push) Waiting to run
* feat(plugins): mount library directories as read-only by default

Add an AllowWriteAccess boolean to the plugin model, defaulting to
false. When off, library directories are mounted with the extism "ro:"
prefix (read-only). Admins can explicitly grant write access via a new
toggle in the Library Permission card.

* test: add tests to buildAllowedPaths

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

* chore: improve allowed paths logging for library access

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-28 10:59:13 -05:00
Deluan Quintão
bd8032b327
fix(plugins): add base64 handling for []byte and remove raw=true (#5121)
Some checks are pending
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
* fix(plugins): add base64 handling for []byte and remove raw=true

Go's json.Marshal automatically base64-encodes []byte fields, but Rust's
serde_json serializes Vec<u8> as a JSON array and Python's json.dumps
raises TypeError on bytes. This fixes both directions of plugin
communication by adding proper base64 encoding/decoding in generated
client code.

For Rust templates (client and capability): adds a base64_bytes serde
helper module with #[serde(with = "base64_bytes")] on all Vec<u8> fields,
and adds base64 as a dependency. For Python templates: wraps bytes params
with base64.b64encode() and responses with base64.b64decode().

Also removes the raw=true binary framing protocol from all templates,
the parser, and the Method type. The raw mechanism added complexity that
is no longer needed once []byte works properly over JSON.

* fix(plugins): update production code and tests for base64 migration

Remove raw=true annotation from SubsonicAPI.CallRaw, delete all raw
test fixtures, remove raw-related test cases from parser, generator, and
integration tests, and add new test cases validating base64 handling
for Rust and Python templates.

* fix(plugins): update golden files and regenerate production code

Update golden test fixtures for codec and comprehensive services to
include base64 handling for []byte fields. Regenerate all production
PDK code (Go, Rust, Python) and host wrappers to use standard JSON
with base64-encoded byte fields instead of binary framing protocol.

* refactor: remove base64 helper duplication from rust template

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

* fix(plugins): add base64 dependency to capabilities' Cargo.toml

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-27 19:00:19 -05:00
Deluan
582d1b3cd9 refactor(plugins): validate scheduler capability at load time
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-1 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-2 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-3 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-4 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-5 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-6 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-7 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-8 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-9 (push) Has been cancelled
Pipeline: Test, Lint, Build / Build-10 (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Move scheduler capability check from runtime (when callback fires) to
load-time validation in ValidateWithCapabilities. This ensures plugins
declaring the scheduler permission must export the nd_scheduler_callback
function, failing fast with a clear error instead of silently skipping
callbacks at runtime.
2026-02-26 16:30:50 -05:00
Deluan
cdd3432788 refactor(http): rename HTTP client files and update struct names for consistency
Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-26 16:19:37 -05:00
Deluan Quintão
652c27690b
feat(plugins): add HTTP host service (#5095)
Some checks are pending
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
* feat(httpclient): implement HttpClient service for outbound HTTP requests in plugins

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

* feat(httpclient): enhance SSRF protection by validating host requests against private IPs

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

* feat(httpclient): support DELETE requests with body in HttpClient service

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

* feat(httpclient): refactor HTTP client initialization and enhance redirect handling

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

* refactor(http): standardize naming conventions for HTTP types and methods

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

* refactor example plugin to use host.HTTPSend for improved error management

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

* fix(plugins): fix IPv6 SSRF bypass and wildcard host matching

Fix two bugs in the plugin HTTP/WebSocket host validation:

1. extractHostname now strips IPv6 brackets when no port is present
(e.g. "[::1]" → "::1"). Previously, net.SplitHostPort failed for
bracketed IPv6 without a port, leaving brackets intact. This caused
net.ParseIP to return nil, bypassing the private/loopback SSRF guard.

2. matchHostPattern now treats "*" as an allow-all pattern. Previously,
a bare "*" only matched via exact equality, so plugins declaring
requiredHosts: ["*"] (like webhook-rs) had all requests rejected.

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-24 14:28:36 -05:00
Deluan
c280dd67a4 refactor: run Go modernize
Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-09 08:44:44 -05:00
Maximilian
a704e86ac1
refactor: run Go modernize (#5002) 2026-02-08 09:57:30 -05:00
Deluan Quintão
e8863ed147
feat(plugins): add SubsonicAPI CallRaw, with support for raw=true binary response for host functions (#4982)
Some checks are pending
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-10 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
* feat: implement raw binary framing for host function responses

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

* feat: add CallRaw method for Subsonic API to handle binary responses

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

* test: add tests for raw=true methods and binary framing generation

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

* fix: improve error message for malformed raw responses to indicate incomplete header

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

* fix: add wasm_import_module attribute for raw methods and improve content-type handling

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-04 15:48:08 -05:00
dependabot[bot]
338853468f
chore(deps): bump bytes in /plugins/pdk/rust/nd-pdk-host (#4973)
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.11.0 to 1.11.1.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.11.0...v1.11.1)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.11.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-04 10:11:37 -05:00
Deluan
c3a4585c83 chore(plugins): move Discord Rich Presence plugin to its own repository: https://github.com/navidrome/discord-rich-presence-plugin 2026-02-03 11:41:49 -05:00
Deluan
2068e7d413 fix(plugins): don't recording metrics for not implemented plugin calls
Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-03 10:15:12 -05:00
Deluan
7b709899a1 refactor(plugins): simplify websocket callback invocation by creating a generic helper function
Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-02 08:59:54 +01:00
Deluan
7d5e13672d refactor(plugins): remove unnecessary configuration permissions from manifest files
Some checks are pending
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
Signed-off-by: Deluan <deluan@navidrome.org>
2026-01-29 17:27:16 -05:00
Deluan Quintão
1afcf7775b
feat: add ISRC matching for similar songs (#4946)
Some checks are pending
Pipeline: Test, Lint, Build / Push to GHCR (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build Windows installers (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Package/Release (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Get version info (push) Waiting to run
Pipeline: Test, Lint, Build / Lint Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test Go code (push) Waiting to run
Pipeline: Test, Lint, Build / Test JS code (push) Waiting to run
Pipeline: Test, Lint, Build / Lint i18n files (push) Waiting to run
Pipeline: Test, Lint, Build / Check Docker configuration (push) Waiting to run
Pipeline: Test, Lint, Build / Build (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-1 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-2 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-3 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-4 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-5 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-6 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-7 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-8 (push) Blocked by required conditions
Pipeline: Test, Lint, Build / Build-9 (push) Blocked by required conditions
* feat: add ISRC support to similar songs matching and plugin interface

Add ISRC (International Standard Recording Code) as a high-priority
identifier in the provider matching algorithm, alongside MBID. The
matching pipeline now uses four strategies in priority order:
ID > MBID > ISRC > Title+Artist fuzzy match.

- Add ISRC field to agents.Song struct
- Add ISRC field to plugin capability SongRef (Go, Rust PDKs)
- Add loadTracksByISRC using json_tree query on tags column
- Integrate ISRC into matchSongsToLibrary, selectBestMatchingSongs,
  and buildTitleQueries

https://claude.ai/code/session_01Dd4mTq1VQZag4RNjCVusiF

* chore: regenerate plugin schema after ISRC addition

Run `make gen` to update the generated YAML schema for the
metadata agent capability with the new ISRC field on SongRef.

https://claude.ai/code/session_01Dd4mTq1VQZag4RNjCVusiF

* feat(mediafile): add GetAllByTags method to MediaFileRepository for tag-based retrieval

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

* feat(provider): speed up track matching by incorporating prior matches in ISRC and MBID lookups

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-27 14:54:29 -05:00
Deluan
a55c4f0410 fix(plugins): log plugin function not implemented and record successful request metrics
Signed-off-by: Deluan <deluan@navidrome.org>
2026-01-27 14:32:57 -05:00