Add DevEnableMediaFileProbe (default true) to allow disabling ffprobe-
based media file probing as a safety fallback. When disabled, the
decider uses tag-based metadata from the scanner instead.
Add ensureProbed to probe media files on first transcode decision,
caching results in probe_data. Build SourceStream from probe data
with fallback to tag-based metadata.
Refactor decision logic to pass StreamDetails instead of MediaFile,
enabling codec profile limitations (e.g., audioProfile) to use
probe data. Add normalizeProbeCodec to map ffprobe codec names
(dsd_lsbf_planar, pcm_s16le) to internal names (dsd, pcm).
NewDecider now accepts ffmpeg.FFmpeg; wire_gen.go regenerated.
Add ProbeAudioStream to FFmpeg interface, using ffprobe to extract
codec, profile, bitrate, sample rate, bit depth, and channels.
Parse bits_per_raw_sample as fallback for FLAC/ALAC bit depth.
Normalize "unknown" profile to empty string.
All parseProbeOutput tests use real ffprobe JSON from actual files.
Add probe_data TEXT column to media_file for caching ffprobe results.
Add UpdateProbeData to MediaFileRepository interface and implementations.
Use hash:"ignore" tag so probe data doesn't affect MediaFile fingerprints.
Updated transcode token handling to use the struct-based auth.Claims
introduced on master, replacing the previous map[string]any approach.
Extended auth.Claims with transcoding-specific fields (MediaID, DirectPlay,
UpdatedAt, Channels, SampleRate, BitDepth) and added float64 fallback in
ClaimsFromToken for numeric claims that lose their Go type during JWT
string serialization. Also added the missing lyrics parameter to all
subsonic.New() calls in test files.
Updated mock AAC transcoding command to use the new default (ipod with
fragmented MP4) matching the migration, ensuring tests exercise the same
buildDynamicArgs code path as production. Improved archiver test mock to
match on the whole StreamRequest struct instead of decomposing fields,
making it resilient to future field additions. Added named constants for
JWT claim keys in the transcode token and wrapped ParseTranscodeParams
errors with ErrTokenInvalid for consistency. Documented the IsLossless
BitDepth fallback heuristic as temporary until Codec column is populated.
Signed-off-by: Deluan <deluan@navidrome.org>
Fix multiple issues identified during code review of the transcoding
extension: add missing return after error in shared stream handler
preventing nil pointer panic, replace dead r.Body nil check with
MaxBytesReader size limit, distinguish not-found from other DB errors,
fix bpsToKbps integer truncation with rounding, add "pcm" to
isLosslessFormat for consistency with model.IsLossless(), add
sampleRate/bitDepth/channels to streaming log, fix outdated test
comment, and add tests for conversion functions and GetTranscodeStream
parameter passing.
* feat(playlists): add percentage-based limits to smart playlists
Add a new `limitPercent` JSON field to Criteria that allows smart playlist
limits to be expressed as a percentage of matching tracks rather than a
fixed number. For example, a playlist matching 450 songs with a 10% limit
returns 45 songs, scaling dynamically as the library grows.
When `limitPercent` is set, refreshSmartPlaylist runs a COUNT query first
to determine the total matching tracks, then resolves the percentage to an
absolute LIMIT before executing the main query. The fixed `limit` field
takes precedence when both are set. Values are clamped to [0, 100] during
JSON unmarshaling.
No database migration is needed since rules are stored as a JSON string.
* fix(criteria): validate percentage limit range in IsPercentageLimit method
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(criteria): ensure idempotency of ToSql method for expressions
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
* 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>
* 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>
* fix(persistence): add nil guards to cursor wrapping in folder and mediafile repos
Prevent SIGSEGV panic when queryWithStableResults yields a zero-value
struct on the rows.Err() path (e.g., "database is locked" during
concurrent scanning). Extract cursor wrapping into wrapFolderCursor and
wrapMediaFileCursor with nil checks matching the existing pattern in
album_repository.go.
Fixes#5138
* fix(persistence): wrap original cursor error in nil guard messages
Use %w to preserve the underlying error (e.g., "database is locked")
so callers can use errors.Is/As for root cause analysis. Tests now
verify the original error is accessible via errors.Is.
* fix(persistence): add nil guards and error wrapping in album, folder, and mediafile cursor functions
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Allow administrators to disable playlist cover art upload/removal for
non-admin users via the new EnableCoverArtUpload config option (default: true).
- Guard uploadPlaylistImage and deletePlaylistImage endpoints (403 for non-admin when disabled)
- Set CoverArtRole in Subsonic GetUser/GetUsers responses based on config and admin status
- Pass config to frontend and conditionally hide upload/remove UI controls
- Admins always retain upload capability regardless of setting
* 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>
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>
* feat(playlist): add migration for playlist image field rename and external URL
* refactor(playlist): rename ImageFile to UploadedImage and ArtworkPath to UploadedImagePath
Rename playlist model fields and methods for clarity in preparation for
adding external image URL and sidecar image support. Add the new
ExternalImageURL field to the Playlist model.
* feat(playlist): parse #EXTALBUMARTURL directive in M3U imports
* feat(playlist): always sync ExternalImageURL on re-scan, preserve UploadedImage
* feat(artwork): add sidecar image discovery and cache invalidation for playlists
Add playlist sidecar image support to the artwork reader fallback chain.
A sidecar image (e.g., MyPlaylist.jpg next to MyPlaylist.m3u) is discovered
via case-insensitive base name matching using model.IsImageFile(). Cache
invalidation uses max(playlist.UpdatedAt, imageFile.ModTime()) to bust
stale artwork when sidecar or ExternalImageURL local files change.
* feat(artwork): add external image URL source to playlist artwork reader
Add fromPlaylistExternalImage source function that resolves playlist
cover art from ExternalImageURL, supporting both HTTP(S) URLs (via
the existing fromURL helper) and local file paths (via os.Open).
Insert it in the Reader() fallback chain between sidecar and tiled cover.
* refactor(artwork): simplify playlist artwork source functions
Extract shared fromLocalFile helper, use url.Parse for scheme check,
and collapse sidecar directory scan conditions.
* test(artwork): remove redundant fromPlaylistSidecar tests
These tests duplicated scenarios already covered by findPlaylistSidecarPath
tests combined with fromLocalFile (tested via fromPlaylistExternalImage).
After refactoring fromPlaylistSidecar to a one-liner composing those two
functions, the wrapper tests add no value.
* fix(playlist): address security review comments from PR #5131:
- Use url.PathUnescape instead of url.QueryUnescape for file:// URLs so
that '+' in filenames is preserved (not decoded as space).
- Validate all local image paths (file://, absolute, relative) against
known library boundaries via libraryMatcher, rejecting paths outside
any configured library.
- Harden #EXTALBUMARTURL against path traversal and SSRF by adding EnableM3UExternalAlbumArt config flag (default false, also
disabled by EnableExternalServices=false) to gate HTTP(S) URL storage
at parse time and fetching at read time (defense in depth).
- Log a warning when os.ReadDir fails in findPlaylistSidecarPath for
diagnosability.
- Extract resolveLocalPath helper to simplify resolveImageURL.
Signed-off-by: Deluan <deluan@navidrome.org>
* feat(playlist): implement human-friendly filename generation for uploaded playlist cover images
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
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.