4799 Commits

Author SHA1 Message Date
Deluan Quintão
a00152397e
fix(artwork): prefer album-root images over disc-subfolder images for multi-disc albums (#5451)
Fixed two bugs in album cover art resolution for multi-disc layouts:

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

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

Closes #5376
2026-05-02 19:48:44 -04:00
Deluan Quintão
ae0e0c89d9
feat(plugins): add PlaybackReport to scrobbler capability (#5452)
* 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
13c48b38a0
fix(smartplaylists): coerce string booleans in smart playlist rules (#5450)
* fix(criteria): coerce string booleans in smart playlist rules - #4826

When clients (e.g. Feishin) send boolean values as strings ("true"/"false")
in smart playlist JSON rules, the SQL comparison fails because SQLite stores
booleans as 0/1 integers. For example, `COALESCE(annotation.starred, false) = 'true'`
never matches.

This adds a `boolean` flag to mapped fields and coerces string values to
native Go bools in `mapFields`, so squirrel generates correct SQL parameters.

Signed-off-by: mango766 <mango766@users.noreply.github.com>
Signed-off-by: easonysliu <easonysliu@tencent.com>

* fix(criteria): implement boolean string coercion for smart playlist rules

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

---------

Signed-off-by: mango766 <mango766@users.noreply.github.com>
Signed-off-by: easonysliu <easonysliu@tencent.com>
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: easonysliu <easonysliu@tencent.com>
2026-05-01 19:21:48 -04:00
Deluan Quintão
7e16b6acb5
feat(ui): replace UI scrobble with reportPlayback and redesign NowPlaying panel (#5448)
* feat(config): add UIPlaybackReportInterval setting

* feat(server): expose playbackReportIntervalMs to UI config

* feat(ui): add playbackReportIntervalMs config default

* feat(ui): replace scrobble/nowPlaying with reportPlayback in subsonic API layer

* feat(ui): replace scrobble logic with reportPlayback state machine in Player

* refactor(ui): simplify Player heartbeat using useInterval hook

- Replace manual setInterval/clearInterval with existing useInterval hook
- Extract shared reportPlaybackUrl helper to deduplicate URL construction
- Use ref for currentTrackId in beforeunload to stabilize effect deps
- Have heartbeat read lastPositionMsRef instead of audioInstance.currentTime

* feat(ui): redesign NowPlaying panel with Discord-style layout

Show album art with play/pause overlay icon, track title, artist,
album name, progress bar with position/duration, and username.

* fix(ui): adjust NowPlaying panel height to fit 3 entries

* fix(ui): send stopped on player close and tab close while paused

- onBeforeDestroy now sends reportPlayback stopped before clearing queue
- beforeunload sends stopped beacon regardless of pause state

* feat(ui): animate NowPlaying progress bar with 1s client-side tick

* fix(ui): account for playbackRate in NowPlaying progress interpolation

* fix(ui): use timestamp-based interpolation for smooth NowPlaying progress

Replace tick counter with fetchedAt timestamp so the progress bar
advances smoothly without resetting on each server poll.

* fix(ui): fix NowPlaying progress bar not animating

Pass `now` (Date.now()) as a prop that changes every tick, so
React.memo'd components actually re-render each second.

* fix(ui): prevent progress bar reset on NowPlaying poll

Set fetchedAt and now atomically on fetch so the elapsed offset
starts at zero and the server's already-estimated positionMs
is used as the base without a visible jump.

* fix(ui): stamp entries with fetch time to prevent progress bar reset

Embed _fetchedAt timestamp directly into each entry object so the
position and its reference timestamp are always in the same state
update, eliminating the React 17 multi-setState batching race.

* fix(server): estimate position for starting state in GetNowPlaying

GetNowPlaying was only estimating elapsed position for the "playing"
state, returning raw positionMs=0 for "starting". Since the UI
player sends "starting" once and then doesn't update until the
60s heartbeat, NowPlaying polls returned 0 for up to a minute,
causing the progress bar to reset on every poll.

* fix(ui): send playing immediately after starting to enable position estimation

The server only estimates elapsed position for "playing" state in
GetNowPlaying. The Player was sending "starting" once and then not
updating until the 60s heartbeat, leaving the server state as
"starting" with positionMs=0 for up to a minute.

Now the Player follows up "starting" with an immediate "playing"
call, transitioning the server state so position estimation works
from the first poll.

* fix(subsonic): fix getNowPlaying returning same playerId for all entries

PlayerId was never incremented in the map callback, so every entry
got playerId=1. This caused the UI to use duplicate React keys,
mixing up rendered entries between players. Also use a stable
composite key in the UI instead of the sequential playerId.

* fix(ui): only send stopped beacon when tab is actually closing

Move the reportPlaybackBeacon call from beforeunload to pagehide.
beforeunload fires before the confirmation dialog, so cancelling
the close would still send stopped. pagehide only fires when the
page is actually being unloaded.

* fix(ui): revert to beforeunload for stopped beacon

pagehide does not fire reliably in Chrome when closing tabs.
Use beforeunload instead — if the user cancels the close dialog,
the heartbeat will re-register the NowPlaying entry on its next tick.

* fix(ui): use synchronous XHR for stopped report on tab close

Replace sendBeacon with synchronous XMLHttpRequest in beforeunload.
This blocks the page from closing until the server acknowledges
the stopped state, ensuring the NowPlaying entry is always removed.

* fix(ui): fix confirmation dialog and use fetch keepalive for tab close

- Move e.preventDefault() before the stopped report so the dialog
  always shows regardless of XHR errors
- Use fetch with keepalive:true instead of sync XHR (more reliable,
  non-blocking, survives page teardown)
- Fall back to sendBeacon if fetch throws

* fix(ui): prevent heartbeat from re-adding entry after stopped on tab close

Set a stoppedRef flag in beforeunload so the heartbeat interval
skips sending playing reports after stopped has been sent.
Without this, the heartbeat could re-register the NowPlaying
entry after the stopped event removed it.

* fix(ui): include client unique ID header in stopped report on tab close

Root cause: reportPlaybackSync (fetch keepalive) did not include the
X-ND-Client-Unique-Id header. Regular reportPlayback calls via
httpClient include this header, and the server uses it as the playMap
key. Without the header, the stopped call fell back to player.ID
as the key, which didn't match the entry added with the UUID key.
The playMap.Remove targeted the wrong key, so the entry persisted.

Fix: export clientUniqueId from httpClient and include it as a header
in the fetch keepalive request.

* fix(ui): use pagehide for stopped report to avoid premature send

beforeunload fires before the confirmation dialog, so the stopped
event was sent even when the user cancelled closing. Move the
stopped report to pagehide, which only fires when the page is
actually being unloaded (after confirmation).

* feat(server): broadcast NowPlaying SSE on every state change

Previously, the SSE broadcast only fired when the NowPlaying count
changed. Now it fires on every reportPlayback call (starting,
playing, paused, stopped), so the NowPlaying panel gets instant
updates for state transitions and position changes.

The UI reducer stores a nowPlayingLastUpdate timestamp alongside
the count, ensuring every SSE event triggers a re-fetch even when
the count is unchanged (e.g., pause/resume).

* fix(ui): clamp NowPlaying position to prevent negative time display

* fix(ui): debounce NowPlaying fetches to prevent progress bar trembling

During track changes, rapid SSE events (stopped, starting, playing)
triggered multiple refetches within milliseconds, each resetting the
interpolation base and causing the progress bar to oscillate. Skip
fetches within 1 second of the previous fetch.

* feat(ui): report playback position on seek

Send a reportPlayback(playing) call when the user seeks/scrubs in
the player, so the NowPlaying panel and server position stay in
sync immediately instead of waiting for the next 60s heartbeat.

* refactor: code review cleanup

- Export clientUniqueIdHeader from httpClient, use in subsonic layer
- Fix variable shadowing (now → fetchNow) in NowPlayingPanel fetchList
- Fix onBeforeDestroy nested dep (read isRadio from ref instead)
- Only broadcast SSE on state transitions, not heartbeat position updates
- Only enqueue NowPlaying to external scrobblers on state transitions

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

* fix(ui): use trailing-edge debounce for NowPlaying fetch

Replace the leading-edge throttle (which fetched on the first event
and blocked subsequent ones) with a trailing-edge debounce (300ms).
During track transitions, the burst of events (stopped → starting →
playing) now collapses into a single fetch after the burst settles,
showing the new track immediately instead of briefly showing empty.

* fix(ui): only show overlay on NowPlaying artwork when paused

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

* refactor(ui): remove unnecessary sendBeacon fallback from reportPlaybackSync

* refactor(ui): rename reportPlaybackSync to reportPlaybackKeepalive

The function was never synchronous — it uses fetch with keepalive:true,
which is fire-and-forget. The name now reflects the actual behavior.

* style: format code with prettier

* test: add tests for reportPlayback SSE broadcast and UI changes

- play_tracker: verify SSE broadcast on every state transition and
  that broadcasts are skipped when EnableNowPlaying is false
- activityReducer: verify nowPlayingLastUpdate timestamp is set
- subsonic/index: verify reportPlayback URL construction

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

* fix(ui): prevent NowPlaying from fetching every second when panel is open

fetchList had unstable identity because it depended on doFetch
(which depended on notify/dispatch). Each 1s setNow re-render
recreated the callback chain, re-triggering the useEffect that
calls fetchList. Use a ref for the fetch logic so fetchList has
a stable identity with empty deps.

* fix(ui): break fetch→dispatch→effect→fetch loop in NowPlaying panel

The fetch dispatched nowPlayingCountUpdate on every result, which
updated nowPlayingLastUpdate in Redux, which triggered the SSE
effect to call fetchList again — creating a fetch loop.

Fix: remove dispatch from fetch results. The badge count uses
entries.length (from local state) when entries are loaded, falling
back to Redux count (from SSE) when they aren't. SSE events remain
the only trigger for nowPlayingLastUpdate, breaking the loop.

* fix(ui): clear NowPlaying entries on panel close so badge uses SSE count

* style: format code with prettier

* fix: address code review feedback

- Fix currentTime truthiness check to handle position 0 correctly
- Report actual player state (playing/paused) on seek instead of
  always sending 'playing'
- Remove idx from React key to avoid reorder issues
- Add debounce timer cleanup on unmount
- Keep entries on panel close so badge stays accurate from polling
- Fix test description to match actual assertion

* fix(ui): keep NowPlaying badge count accurate from polling

Add a separate nowPlayingCountSync action that updates the Redux
count without setting nowPlayingLastUpdate (which would trigger
the SSE effect and cause a fetch loop). Polling results now sync
the badge count via this action, so the badge stays accurate even
when SSE is unavailable.

* style: format code with prettier

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-01 15:27:32 -04:00
Deluan
556f345a10 chore(lint): upgrade golangci-lint, fix/ignore new errors
Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-01 14:22:51 -04:00
Deluan Quintão
94eb6c522b
feat(subsonic): implement playbackReport OpenSubsonic extension (#5442)
* feat(req): add Float64Or helper for parsing float query params

* feat(scrobbler): extend NowPlayingInfo with state/position/rate fields

* feat(scrobbler): implement ReportPlayback with state machine and auto-scrobble

* feat(responses): add state/positionMs/playbackRate to NowPlayingEntry

* feat(subsonic): add reportPlayback endpoint handler

* feat(subsonic): include state/positionMs/playbackRate in getNowPlaying response

* feat(subsonic): register playbackReport OpenSubsonic extension

* test(e2e): add reportPlayback endpoint e2e tests

* refactor(scrobbler): simplify ReportPlayback — extract helpers, remove duplication

- Add state constants and exported ValidStates map
- Extract remainingTTL() helper (was duplicated 3x)
- Merge playing/paused switch cases into single branch
- Use Get instead of GetWithParticipants for non-stopped states
- Guard NowPlayingCount broadcast with count-change detection
- Use cache entry for NowPlaying dispatch instead of extra DB query
- Remove redundant Position field from NowPlayingInfo

* refactor(scrobbler): skip DB query in playing/paused when playMap has entry

* fix(play_tracker): handle errors when adding/updating NowPlayingInfo in cache

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

* refactor(play_tracker): replace sort with slices.SortFunc for NowPlayingInfo

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

* fix(play_tracker): check all ReportPlayback errors in tests

Replace _ = with explicit error assertions to avoid masking
failures in intermediate calls.

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

* test(e2e): use real PlayTracker and assert getNowPlaying after reportPlayback

Replace noopPlayTracker with a real PlayTracker backed by the E2E
database. E2E tests now verify the full round-trip: reportPlayback
creates/updates/removes entries visible via getNowPlaying, including
state, positionMs, and playbackRate fields.

Export NewPlayTracker constructor for use outside the scrobbler package.

* fix(play_tracker): account for playback rate in TTL and detect track switches

The remainingTTL function now divides remaining time by the playback rate,
so cache entries expire correctly at non-1x speeds (e.g., 2x playback halves
the TTL). Zero/negative rates default to 1.0. The playing/paused case now
checks if the cached MediaFile ID matches the reported mediaId, falling back
to a DB fetch when the client switches tracks without sending stopped/starting.
Adds parameterized tests for remainingTTL covering rate variations and edge cases.

* fix(subsonic): validate positionMs and playbackRate in reportPlayback

Reject negative positionMs values and invalid playbackRate values (NaN,
Inf, zero, negative) at the API boundary before they reach TTL and
position estimation math. Returns clear error messages for each case.

* feat(play_tracker): add ClientId and ClientName to ReportPlayback parameters

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

* refactor(play_tracker): replace NowPlaying method with ReportPlayback calls

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

* refactor(play_tracker_test): remove redundant TTL behavior tests and clean up mockPluginLoader

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-30 23:04:05 -04:00
Deluan
2b9f326993 chore(docker): add app directory to PATH in Dockerfile 2026-04-30 16:55:10 -04:00
Deluan Quintão
2307a64da7
fix(ui): start new album from track 1 after closing player (#5441)
When a user played an album, advanced a few tracks, closed the player,
then played a different album, playback started mid-album at the
previous track index instead of track 1.

The root cause was in reduceSyncQueue: the hasPendingSwitch check
compared playIndex against savedPlayIndex, but after clearQueue both
reset to 0, making the check falsely conclude no switch was pending.
This caused PLAYER_SYNC_QUEUE to prematurely clear the playIndex and
clear flags before the music player library could act on them.

Fix: also treat clear=true as a signal that a track switch is pending,
since it means a new queue was just loaded.
2026-04-29 15:36:39 -04:00
Daniel Barrientos Anariba
bdea9ed6a1
fix(ui): show album tile actions on keyboard focus (#5434)
* fix(ui): show album tile actions on keyboard focus - #4836

The album grid tile bar (containing play/heart/context-menu buttons) had
opacity:0 by default and only became visible on mouse :hover. Keyboard
users tabbing through the album grid never saw which tile was focused
and could not discover the available actions.

Mirrors the existing :hover rule with :focus-within, which matches when
the link itself or any descendant (e.g. the play button) has focus.

Signed-off-by: Daniel Banariba <banaribad@gmail.com>

* fix(ui): also disable pointer events on hidden album tile bar - #4836

Per review feedback (@gemini-code-assist): the tileBar's buttons remained
clickable even at opacity:0, causing accidental Play/Menu triggers when a
user clicked what looked like the album cover.

Set 'pointerEvents: none' on the base tileBar and restore 'auto' on the
same selector that turns it visible.

Signed-off-by: Daniel Banariba <banaribad@gmail.com>

---------

Signed-off-by: Daniel Banariba <banaribad@gmail.com>
2026-04-28 22:48:59 -04:00
Deluan
57fc85f434 refactor(smartplaylist): remove unused 'value' field and clarify 'random' usage
Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-28 20:44:54 -04:00
Deluan
0fd9c6df2e refactor(smartplaylist): clarify FieldInfo naming in criteria package
Rename FieldInfo.Name to Alias (only meaningful for backward-compat
entries like albumtype→releasetype) and FieldInfo.alias to tagAlias
(tag names from mappings.yml that resolve to existing fields). Add a
Name() method that derives the canonical name from the map key,
eliminating 60+ redundant Name declarations where the value matched
the key. LookupField now populates the private name field automatically.

Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-28 20:24:04 -04:00
Deluan
d9dac44456 feat(smartplaylist): add ReplayGain fields to criteria system
Expose the four ReplayGain database columns (rg_album_gain, rg_album_peak,
rg_track_gain, rg_track_peak) as first-class numeric criteria fields for
smart playlists. This allows users to filter tracks by ReplayGain values,
e.g. finding tracks with album gain below a threshold.
2026-04-28 19:46:12 -04:00
Deluan Quintão
46b4dcd5f6
feat(smartplaylist): add isMissing and isPresent operators (#5436)
* feat(smartplaylist): add IsMissing and IsPresent operator types

Add two new Expression types for detecting absent/present tags and
roles in smart playlist criteria. Includes JSON marshal/unmarshal
support and Walk visitor registration.

* test(smartplaylist): add JSON marshal/unmarshal tests for isMissing/isPresent

* feat(smartplaylist): add SQL generation for isMissing/isPresent operators

Tags check json_tree(media_file.tags) for key existence.
Roles check json_tree(media_file.participants) for key existence.
Regular DB column fields are rejected with an error.

* test(smartplaylist): add e2e tests for isMissing/isPresent operators

Tests cover tag presence/absence with selective matching (grouping),
universal absence (lyricist role), universal presence (composer role),
and combined operator usage.

* refactor(smartplaylist): use strconv.ParseBool in IsTruthy

Replace hand-rolled string truthiness check with strconv.ParseBool,
which correctly handles standard boolean strings and rejects
unrecognized values as false.

* refactor(smartplaylist): clarify missingExpr parameter naming

Rename defaultNegate to checkAbsence and extract truthy local for
readability. The XNOR logic (checkAbsence == truthy) is now easier
to follow: isMissing passes true, isPresent passes false.

* refactor(smartplaylist): reuse jsonExpr in missingExpr, improve errors

- tagCond/roleCond now handle nil cond (existence-only check)
- missingExpr delegates to jsonExpr(info, nil, negate) instead of
  building SQL manually
- Better error messages: unknown fields now report the field name
2026-04-28 19:40:08 -04:00
Deluan Quintão
d5ba61adf8
fix(ui): prevent autoplay when clearing the play queue (#5430)
When clearing the queue, the reducer resets to initialState which lacks
an autoPlay field (undefined). The autoPlay option was computed as true
because undefined !== false, causing the music player library to attempt
playback of the first song before internal state was fully cleared.

Added a queue.length > 0 guard to the autoPlay calculation so it is
never true when the queue is empty, regardless of other state flags.

Fixes #5331
2026-04-27 21:45:13 -04:00
Deluan
a4c1fa6378 chore(deps): update dependencies to latest versions
Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-27 20:24:01 -04:00
Deluan Quintão
3e25ca3868
test: enable Subsonic response snapshot tests on Windows (#5427)
* fix(test): enable Subsonic response snapshot tests on Windows

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

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

Restore the ability to update snapshots via `make snapshots`
(UPDATE_SNAPSHOTS=true), which was lost when replacing cupaloy
with the custom matcher.
2026-04-27 20:19:28 -04:00
Deluan Quintão
5c4f0298a6
fix(sharing): validate JWT expiration and share existence on stream endpoint (#5426)
* fix(sharing): validate JWT expiration and share existence on stream endpoint

The public stream endpoint (/public/s/{token}) was using
TokenAuth.Decode() which only verifies the JWT signature but skips
exp claim validation. This allowed expired share stream URLs to remain
functional indefinitely. Additionally, deleting a share did not revoke
previously issued stream tokens since the handler never performed a
server-side share lookup.

Fixed by switching decodeStreamInfo() to use auth.Validate() which
properly checks the exp claim, and by embedding the share ID ("sid")
in stream tokens so the handler can verify the share still exists.
Old tokens without the sid claim remain backward compatible but still
benefit from expiration validation.

* fix(sharing): check share expiration on stream requests

Replace the lightweight Exists() check with Get() + expiration
validation, so that shares whose ExpiresAt was updated to an earlier
time after token issuance are also rejected (410 Gone). Reuses the
existing checkShareError handler for consistent error responses.
2026-04-27 19:36:57 -04:00
Daniel Barrientos Anariba
0fe08bfa74
feat(ui): add Not Starred filter option (#5362)
* feat(ui): add Not Starred filter option - #5108

Signed-off-by: Daniel Banariba <banaribad@gmail.com>

* fix(ui): apply notStarred translation to playlistTrack resource - #5108

Signed-off-by: Daniel Banariba <banaribad@gmail.com>

* refactor(ui): use NullableBooleanInput for starred filter - #5108

Replace QuickFilter approach with NullableBooleanInput per maintainer
review feedback. Single tri-state filter (Yes/No/Any) instead of two
separate buttons + dataProvider translation. Matches the existing pattern
used by the 'missing' filter.

Signed-off-by: Daniel Banariba <banaribad@gmail.com>

---------

Signed-off-by: Daniel Banariba <banaribad@gmail.com>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2026-04-27 18:21:32 -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 Quintão
e6680c904b
fix(playlists): allow toggling auto-import and avoid unnecessary artwork reloads (#5421)
* fix(playlists): allow toggling auto-import (sync) via REST API

The updatePlaylistEntity handler was not applying the sync field from
incoming requests, causing the auto-import toggle in the UI to have no
effect. Apply the sync value for file-backed playlists only.

* fix(playlists): enhance update logic for playlist metadata and sync toggle

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

* fix(playlists): address code review feedback

- Add pointer equality short-circuit in rulesEqual before reflect.DeepEqual
- Guard against empty ID in Put's partial-update path
- Only apply Sync when it actually differs from current value, preventing
  zero-value overwrites from partial payloads

* fix(playlists): remove unused parameters from Update method

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-27 12:20:27 -04:00
Deluan Quintão
5d1c1157b5
refactor(artwork): migrate readers to storage.MusicFS and add e2e suite (#5379)
* test(artwork): add e2e suite documenting album/disc resolution

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

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

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

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

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

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

* refactor(artwork): make fromExternalFileAbs a thin delegator

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

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

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

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

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

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

* refactor(artwork): migrate album reader to MusicFS

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

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

* refactor(artwork): migrate disc reader to MusicFS

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

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

* refactor(artwork): migrate mediafile reader to MusicFS

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Add high-value scenarios uncovered by the existing specs:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: escape artist folder names in artwork glob

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

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

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

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

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

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

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

---------

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

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

* test: add comment explaining forward-slash paths in test fixtures
2026-04-26 17:34:39 -04:00
Deluan
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
1bd736dae9
refactor: centralize criteria sort parsing and extract smart playlist logic (#5415)
* test: add tests for recordingdate alias resolution in smart playlists

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

* refactor: update FieldInfo structure and simplify fieldMap initialization

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

* refactor: move sort parsing logic from persistence to criteria package

Extracted sort field parsing, validation, and direction handling from
persistence/criteria_sql.go into model/criteria/sort.go. The new
OrderByFields method on Criteria parses the Sort/Order strings into
validated SortField structs (field name + direction), resolving aliases
and handling +/- prefixes and order inversion. The persistence layer now
consumes these parsed fields and only handles SQL expression mapping.
This centralizes sort parsing to enforce consistent implementations.

* refactor: standardize field access in smartPlaylistCriteria structure

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

* refactor: add ResolveLimit method to Criteria

Moved the percentage-limit resolution logic from playlist_repository
into Criteria.ResolveLimit, replacing the 3-line mutate-after-query
pattern with a single method call. The method preserves LimitPercent
rather than zeroing it, since IsPercentageLimit already returns false
once Limit is set, making the clear redundant and lossy.

* refactor: improve child playlist loading and error handling in refresh logic

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

* refactor: extract smart playlist logic to dedicated files

Moved refreshSmartPlaylist, addSmartPlaylistAnnotationJoins, and
addCriteria methods from playlist_repository.go to a new
smart_playlist_repository.go file. Extracted all smart playlist tests
to smart_playlist_repository_test.go. Added DeferCleanup to the
"valid rules" test to fix ordering flakiness when Ginkgo randomizes
test execution across files.

* refactor: break refreshSmartPlaylist into smaller focused methods

Split the monolithic refreshSmartPlaylist method into discrete helpers
for readability: shouldRefreshSmartPlaylist for guard checks,
refreshChildPlaylists for recursive dependency refresh,
resolvePercentageLimit for count-based limit resolution,
buildSmartPlaylistQuery for assembling the SELECT with joins, and
addMediaFileAnnotationJoin to DRY up the repeated annotation join clause.

* refactor: deduplicate child playlist IDs in Criteria

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

* refactor: simplify withSmartPlaylistOwner to accept model.User

Replaced separate ownerID string and ownerIsAdmin bool parameters with a
single model.User struct, reducing the field count in smartPlaylistCriteria
and making the option function signature clearer. Updated all call sites
and tests accordingly.

* fix: handle empty sort fields and propagate child playlist load errors

OrderByFields now falls back to [{title, asc}] when all user-supplied
sort fields are invalid, preventing empty ORDER BY clauses that would
produce invalid SQL in row_number() window functions. Also restored the
original behavior where a DB error loading child playlists aborts the
parent smart playlist refresh, by making refreshChildPlaylists return a
bool.

* refactor: log warning when no valid sort fields are found

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-26 14:49:59 -04:00
Deluan
0ab10e819f refactor: simplify criteria Expression interface
Replaced the Fields() type switch with a fields() method on the
Expression interface, eliminating the need to update a central switch
when adding new expression types. Removed the now-redundant
criteriaExpression() marker method since fields() alone suffices to
restrict the interface. Extracted a conjunction interface for the
ChildPlaylistIds() lookup used by All and Any.
2026-04-26 10:58:57 -04:00
Deluan Quintão
5d1c9530ab
feat(cli): add pls export/import subcommands for bulk playlist management (#5412)
* refactor: rename ImportFile to ImportFromFolder in playlists service

* feat: add ImportFile method with library/folder resolution

* feat: allow sync flag upgrade on re-import of non-synced playlists

* feat: add pls export subcommand with bulk and single export

Add `navidrome pls export` command that supports:
- Single playlist export to stdout (-p flag only)
- Single playlist export to directory (-p and -o flags)
- Bulk export of all playlists to a directory (-o flag only)
- Filtering by user (-u flag)
- Automatic filename sanitization and collision detection

Also extracts findPlaylist helper from runExporter for reuse.

* feat: add pls import subcommand with sync flag support

* fix: improve error message for export without output directory

* test: add tests for ImportFile sync flag and sync upgrade behavior

* refactor: streamline export and import logic by removing redundant comments and improving library path matching

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

* feat: update ImportFile method to include sync flag for playlist imports

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

* feat: implement fetchPlaylists function to streamline playlist retrieval

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

* feat: replace inline filename sanitization with centralized utility function

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

* feat: refactor playlist import logic to consolidate sync handling and improve method signatures

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

* fix: address code review feedback on playlist import/export

- Fix duplicate playlist creation on non-sync re-import: only reconcile
  sync flag when the playlist was actually persisted (has an ID)
- Distinguish "not in any library" from real errors in resolveFolder
  using a sentinel error, so DB/folder errors propagate instead of
  falling back to ImportM3U
- Use bufio.Scanner in countM3UTrackLines instead of reading entire file

* feat: replace bufio.Scanner with UTF8Reader and LinesFrom utility for improved file reading

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

* fix: record path for outside-library imports to prevent duplicates

Files outside all libraries now go through updatePlaylist with the
absolute path recorded, so re-importing the same file updates the
existing playlist instead of creating a duplicate.

* refactor: name guard condition in updatePlaylist for readability

Extracted the compound boolean expression into a named local variable
`alreadyImportedAndNotSynced` to make the intent of the early-return
guard clearer at a glance.

* add godocs

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-25 20:54:02 -04:00
Deluan Quintão
81a17f6bbb
fix(search): normalization for non-NFKD Unicode letters (ø, æ, œ, ß) (#5413)
* fix(search): transliterate non-ASCII letters symmetrically in FTS5 path

Songs and artists with letters like ø, æ, œ, ß were unsearchable. The
query path in server/subsonic/searching.go transliterates with
sanitize.Accents (Øystein → Oystein), but the FTS5 tokenizer's
remove_diacritics 2 only strips NFKD-decomposable marks — atomic
letters with built-in strokes/ligatures survive tokenization, so the
query side and index side disagreed.

Apply sanitize.Accents on both sides:

- normalizeForFTS now also emits an ASCII-transliterated form for each
  word, so search_normalized contains the variant the query produces.
- buildFTS5Query transliterates the unquoted portion of the input so
  every caller (Subsonic, REST fullTextFilter) gets the same handling.
  Quoted phrases stay as typed, preserving phrase matches against the
  original title/artist columns.

Existing libraries pick up the fix as records are re-scanned; users
can trigger a manual full rescan to refresh older entries.

* fix(search): cache transliteration and add ß/quoted-phrase test coverage

Address review feedback: call sanitize.Accents once per word and reuse
the result for both the punct-stripped and accent-only paths. Add missing
test entries for ß→ss transliteration and quoted Unicode phrase preservation.

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-25 20:27:38 -04:00
Daniele Massa
9824102efb
fix(ui): completed Italian translation (#5407)
Co-authored-by: Daniele Massa <x@danielemassa.org>
2026-04-25 16:18:51 -04:00
Deluan Quintão
ca09070a6c
feat(smartplaylists): relax playlist visibility in inPlaylist/notInPlaylist rules (#5411)
* test(e2e): add end-to-end tests for smart playlists functionality

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

* fix: enforce playlist visibility in smart playlist InPlaylist/NotInPlaylist rules

Previously, the InPlaylist/NotInPlaylist smart playlist criteria only
allowed referencing public playlists, regardless of who owned the smart
playlist. This was too restrictive for owners referencing their own
private playlists and for admins who should have unrestricted access.

The fix passes the smart playlist owner's identity and admin status into
the criteria SQL builder, so that: admins can reference any playlist,
regular users can reference public playlists plus their own private ones,
and inaccessible referenced playlists produce a warning instead of a hard
error. Also prevents recursive refresh of child playlists the owner
cannot access.

* test(e2e): clarify user roles and fix playlist visibility tests

Renamed testUser/otherUser to adminUser/regularUser to make the admin
vs regular user distinction explicit in test code. Fixed three playlist
visibility tests that were evaluating as admin (bypassing all access
checks) instead of as a regular user, so the public playlist path is
now actually exercised. All playlist operator tests now use explicit
evaluateRuleAs calls with the appropriate user role.

* fix: sync rulesSQL criteria after limitPercent resolution

The rulesSQL struct captures a copy of rules at creation time. When
limitPercent is resolved later, rules.Limit is updated but rulesSQL
still holds the stale value. This caused percentage-based smart playlist
limits to be silently ignored. Fix by updating rulesSQL.criteria after
the resolution.

* refactor: convert inList to a method on smartPlaylistCriteria

The inList function already receives ownerID and ownerIsAdmin from the
smartPlaylistCriteria caller. Making it a method lets it access those
fields directly from the receiver, simplifying the signature and staying
consistent with exprSQL which was already converted to a method.

* refactor: simplify function signatures by removing type parameters in criteria_sql.go

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-25 14:59:06 -04:00
Deluan Quintão
251cc71e2d
refactor: move smart playlist criteria SQL to persistence (#5408)
* refactor: move criteria SQL generation to persistence

Keep model/criteria as a domain DSL with JSON parsing, field metadata, expression traversal, and child playlist extraction only. Move smart playlist SQL translation, sort SQL, and join planning into persistence behind smartPlaylistCriteria so repository code uses a small query-building API.

* refactor: simplify criteria translator metadata

Use generic helper functions for criteria operator maps so the SQL translator can pass named criteria map types directly. Remove unused pseudo-field metadata from the criteria field API while preserving special field name lookup.

* test: add coverage check for criteria-to-SQL field mappings

Add a test that iterates all fields registered in the criteria package and
verifies that every non-tag/non-role field has a corresponding entry in
the persistence layer's smartPlaylistFields map. This prevents silent
drift between the domain field registry and the SQL translation layer.

Also adds an AllFieldNames() function to the criteria package to support
field enumeration from outside the package.
2026-04-24 23:18:20 -04:00
Deluan Quintão
3b3b9a62ca
test(smartplaylists): add smart playlist e2e test suite (#5409)
* test: add smart playlist e2e suite infrastructure

* test: add string field smart playlist e2e tests

* test: add numeric, boolean, tag, participant, annotation, logic, sorting smart playlist e2e tests

* test: add playlist operator smart playlist e2e tests

* test: add isNot and endsWith string field e2e tests

* test: add date/time field smart playlist e2e tests

* fix: add gosec nolint directives for safe SQL concatenation in e2e restore

* refactor: address code review feedback for smart playlist e2e tests

- Deduplicate evaluateRule by delegating to evaluateRuleOrdered
- Cache table list in BeforeSuite instead of querying sqlite_master per test
- Wrap restoreDB in a transaction with defer cleanup for DETACH/foreign_keys
- Use JSON numbers for numeric criteria values to match canonical JSON shape

* refactor: simplify e2e test infrastructure

- Remove unused return value from buildTestFS
- Add deferred ROLLBACK as safety net in restoreDB transaction
- Cache Come Together ID to avoid repeated lookups in BeforeSuite
- Use range-over-int for play count loop

* test: add missing operator coverage to smart playlist e2e tests

Add 4 tests for operators/paths that had no e2e coverage:
- notContains on string fields (LIKE negation path)
- before on date fields (Lt for dates, only after was tested)
- startsWith on tag fields (json_tree + LIKE subquery)
- endsWith on participant/role fields (json_tree + LIKE subquery)
2026-04-24 23:03:10 -04:00
Deluan Quintão
7e083e0795
fix: split html sanitization from plaintext handling (#5403)
* fix: split html sanitization from plaintext handling

Add a dedicated SanitizeHTML helper for HTML-rendered values so entity-encoded markup is decoded before bluemonday sanitization. Use the new helper for the login welcome message and artist biographies while preserving SanitizeText semantics for lyrics and other plaintext callers. Add regression coverage for both helpers and the serveIndex welcomeMessage path.

* docs: add SanitizeText and SanitizeHTML godoc

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

* fix: preserve plain text in artist biographies

Revert artist biography storage to SanitizeText so entity-encoded plain text remains decoded for Subsonic consumers. This avoids double-escaping values like R&B in XML responses while keeping the new welcomeMessage HTML sanitization in place, and adds a regression test covering the biography storage behavior.

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-23 17:53:28 -04:00
Deluan
4488349a3a fix(makefile): adjust PATH order for golangci-lint installation and linting
Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-22 20:11:20 -04:00
Aengus Walton
44e63596a0
feat(server): add EnforceNonRootUser config option to exit early if started as root (#5373)
* feat(config): Add EnforceNonRootUser config option to exit early if started as root

Signed-off-by: Aengus Walton <ventolin@gmail.com>

* Move validateEnforceNonRootUser check to directly after parsing the config

* Ensure the data directory hasn't been created in test

---------

Signed-off-by: Aengus Walton <ventolin@gmail.com>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2026-04-21 21:27:54 -04:00
Deluan
2954c052f5 fix(tests): update media file paths in tests to be relative
Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-19 20:07:23 -04:00
Deluan Quintão
64c8d3f4c5
ci: run Go tests on Windows (#5380)
* ci(windows): add skeleton go-windows job (compile-only smoke test)

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

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

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

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

* test(lyrics): skip Windows-incompatible tests

* test(utils): skip Windows-incompatible tests

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

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

* test(storage): skip Windows-incompatible tests

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

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

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

* test(playlists): skip Windows-incompatible tests

* test(model): skip Windows-incompatible tests

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

* test(core): skip Windows-incompatible tests

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

* test(artwork): skip Windows-incompatible tests

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

* test(persistence): skip Windows-incompatible tests

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

* test(scanner): skip Windows-incompatible tests

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

* test(plugins): skip Windows-incompatible tests

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

* test(server): skip Windows-incompatible tests

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

* test(nativeapi): skip Windows-incompatible tests

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

* test(e2e): skip Windows-incompatible tests

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

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

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

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

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

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

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

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

* ci: retrigger for Windows soak run 2/3

* ci: retrigger for Windows soak run 3/3

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

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

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

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

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

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

Resolves Copilot review comments on #5380.

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

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

* test(adapters): use tests.SkipOnWindows helper

* test(core): use tests.SkipOnWindows helper

* test(model): use tests.SkipOnWindows helper

* test(persistence): use tests.SkipOnWindows helper

* test(scanner): use tests.SkipOnWindows helper

* test(server): use tests.SkipOnWindows helper

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

config_validation_test, manager_loader_test, and migrate_test have no
WASM/exec dependencies and don't rely on the make-built test plugins
from plugins_suite_test.go. Let them run on Windows too.
2026-04-19 13:16:47 -04:00
Deluan Quintão
3b7d3f4383
feat(matcher): add Matcher.PreferStarred option to bias fuzzy matcher toward starred/high-rated tracks (#5387)
* matcher: update godoc for matcher config scoring order

* conf: log deprecated SimilarSongsMatchThreshold option

* conf: enable matcher prefer-starred by default
2026-04-19 12:54:41 -04:00
bobo-xxx
28eba567a7
fix(artwork): return correct timestamp when disc or album coverart changes (#5378)
* fix(artwork): return imagesUpdatedAt in LastUpdated when cover art changes

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

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

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

Fixes navidrome/navidrome#5377

* refactor tests

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

* fix(artwork): return imagesUpdatedAt in disc LastUpdated

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

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

---------

Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: Deluan <deluan@navidrome.org>
2026-04-17 21:35:33 -04:00
Deluan Quintão
155e293f4d
chore(deps): upgrade Go to 1.26 (#5361)
Bump the main module, Dockerfile build stages, and devcontainer to Go
1.26.0. Plugin sub-modules under plugins/ remain on go 1.25 intentionally
(independent modules, untouched in this change).

Also add an explicit actions/setup-go@v6 step (with go-version-file:
go.mod) to the go-lint and go jobs in the CI pipeline. This matches the
golangci-lint-action v4+ requirement that setup-go run before the linter,
and pins the runner Go version to go.mod so CI does not depend on the
ubuntu-latest tools cache picking up Go 1.26.
2026-04-14 19:31:01 -04:00
Deluan Quintão
e86d3266c4
Add context7.json with URL and public key 2026-04-14 19:19:42 -04:00
dependabot[bot]
15e011bd49
chore(deps-dev): bump flatted from 3.3.3 to 3.4.2 in /ui (#5236)
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 20:38:03 -04:00
Deluan
02c9fc3359 chore(deps): update go-sqlite3 and other dependencies to latest versions
Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-13 20:32:42 -04:00
Deluan Quintão
e53e60d39d
feat(artwork): enable native libwebp encoding in Docker image (#5350)
* feat(docker): add musl build stage for native libwebp support

Add a new build-alpine stage using Alpine/musl with xx cross-compilation,
producing a dynamically-linked musl binary for the Docker image. The
runtime image now installs libwebp, libwebpdemux, and libwebpmux and
creates .so symlinks so gen2brain/webp can detect native libwebp via
purego/dlopen at startup and use it automatically.

The existing Debian/glibc 'build' stage is kept for standalone binary
distribution (darwin, windows, and glibc linux binaries); the Docker
image now ships the musl build from build-alpine instead.

* fix(docker): use dynamic symlinks for libwebp libraries

Avoid hardcoding SONAME versions (.so.7, .so.2, .so.3) which break
on Alpine version bumps. Also fix misleading comment: the musl build
is dynamic (required for purego dlopen), not static.

* feat(docker): enable WebP encoding in Docker environment

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

* fix(docker): pin build-alpine stage to Go 1.25 to match base stage

Align the new build-alpine stage with the existing glibc 'base' stage,
both pinned to Go 1.25. Bumping build-alpine independently would create
a version skew between the Docker image binary and the standalone
binaries, which should be avoided unless there is a specific reason.

* fix(docker): harden build-alpine stage (musl pin, -latomic, dynamic-link check)

Address review feedback on the build-alpine stage:
- Pin Go builder to golang:1.25-alpine3.20 so the musl version used at
  build time matches the alpine:3.20 runtime image, eliminating any
  potential musl ABI skew between builder and runtime.
- Add -extldflags '-latomic' so SQLite's 64-bit atomics resolve when
  cross-compiling for 32-bit arm targets (arm/v6, arm/v7).
- Add a build-time check that the produced binary is dynamically
  linked (using 'file' from Alpine), failing the build if it is not.
  A fully-static binary cannot dlopen libwebp and would silently fall
  back to the WASM encoder, defeating the whole point of this stage.

* fix(docker): revert to unpinned golang:1.25-alpine builder

The golang:1.25-alpine3.20 tag suggested during review does not exist on
public.ecr.aws (only 3.21, 3.22, 3.23, and unpinned 'alpine' are
published). Revert to the unpinned 'golang:1.25-alpine' tag so the
Docker build can resolve the base image.

This means the builder's Alpine version can drift relative to the
alpine:3.20 runtime, but in practice musl's backward compatibility
covers this for Navidrome's small dlopen surface (a few libwebp
symbols, no direct libc calls from the dlopen path). If a skew ever
manifests, we can pin both builder and runtime to the same specific
Alpine release in a follow-up.

---------

Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-13 13:30:05 -04:00
Deluan Quintão
0a6b5519cc
refactor(scanner): remove C++ taglib adapter (#5349)
* refactor(build): remove CPP taglib adapter

Remove the CGO-based TagLib adapter (adapters/taglib/) and all
cross-taglib build infrastructure. The WASM-based go-taglib adapter
(adapters/gotaglib/) is now the sole metadata extractor.

- Delete adapters/taglib/ (CPP/CGO wrapper)
- Delete .github/actions/download-taglib/
- Remove CROSS_TAGLIB_VERSION, CGO_CFLAGS_ALLOW, and all taglib-related
  references from Dockerfile, Makefile, CI pipeline, and devcontainer

* fix(scanner): gracefully fallback to default extractor instead of crashing

Replace log.Fatal with a graceful fallback when the configured scanner
extractor is not found. Instead of terminating the process, the code now
warns and falls back to the default taglib extractor using the existing
consts.DefaultScannerExtractor constant. A fatal log is retained only for
the case where the default extractor itself is not registered, which
indicates a broken build.

* test(scanner): cover default extractor fallback and suppress redundant warn

Address review feedback on the extractor fallback in newLocalStorage:
- Only log the "using default" warning when the configured extractor
  differs from the default, so a broken build (default extractor itself
  missing) logs only the fatal — not a misleading "falling back" warn
  followed immediately by the fatal.
- Add a unit test that registers a mock under consts.DefaultScannerExtractor,
  sets the configured extractor to an unknown name, and asserts the local
  storage is constructed using the default extractor's constructor.
2026-04-12 21:52:29 -04:00
Deluan Quintão
52e47b896a
refactor: extract song-to-library matcher to core/matcher package (#5348)
* refactor: extract matchSongsToLibrary to core/matcher package

Move the song-to-library matching algorithm from core/external into its own
core/matcher package. The Matcher struct exposes a single public method
MatchSongsToLibrary that implements a multi-phase matching algorithm
(ID > MBID > ISRC > fuzzy title+artist). Includes pre-sanitization
optimization for the fuzzy matching loop.

No behavioral changes — the algorithm is identical to the version in
core/external/provider_matching.go.

* refactor: inject matcher.Matcher via Wire instead of creating it inline

Add *matcher.Matcher as a dependency of external.NewProvider, wired via
Google Wire. Update all provider test files to pass matcher.New(ds).
This eliminates tight coupling so future consumers can reuse the matcher
without depending on the external package.

* refactor: remove old provider_matching files

Delete core/external/provider_matching.go and its tests. All matching
logic now lives in core/matcher/.

* test(matcher): restore test coverage lost in extraction

Port back 23 specs that existed in the old provider_matching_test.go
but were dropped during the extraction. Covers specificity levels,
fuzzy matching thresholds, fuzzy album matching, duration matching,
and deduplication edge cases.

* test(matcher): extract matchFieldInAnd/matchFieldInEq helpers

The four inline mock.MatchedBy closures in setupAllPhaseExpectations
all followed the same squirrel.And -> squirrel.Eq -> field-name-check
pattern. Extract into two small helpers to reduce duplication and
make the setup functions read as a concise list of phase expectations.

* refactor(matcher): address PR #5348 review feedback

- sanitizedTrack now holds *model.MediaFile instead of a value copy. Since
  MediaFile is a large struct (~74 fields), this avoids the per-track copy
  into sanitized[] and a second copy when findBestMatch assigns the winner.
  loadTracksByTitleAndArtist updated to iterate by index and pass &tracks[i].

- loadTracksByISRC now sorts results (starred desc, rating desc, year asc,
  compilation asc) so that when multiple library tracks share an ISRC the
  most relevant one is picked deterministically, matching the sort order
  already used by loadTracksByTitleAndArtist.

- Restored the four worked examples (MBID Priority, ISRC Priority,
  Specificity Ranking, Fuzzy Title Matching) in the MatchSongsToLibrary
  godoc that were dropped during the extraction.

- matcher_test.go: tests now enforce expectations via AssertExpectations in
  a DeferCleanup. The old setupAllPhaseExpectations helper was replaced
  with per-phase helpers (expectIDPhase/expectMBIDPhase/expectISRCPhase +
  allowOtherPhases) so each test deterministically verifies which matching
  phases fire. This surfaced (and fixes) a latent issue copilot flagged:
  the old .Once() expectations were not actually asserted, so tests would
  silently pass even when phases short-circuited unexpectedly.
2026-04-12 16:47:22 -04:00
Deluan
aa84e645ba fix(ui): add albumGain and trackGain translations in Brazilian Portuguese
Signed-off-by: Deluan <deluan@navidrome.org>
v0.61.2
2026-04-12 13:22:56 -04:00
Alexander Makeenkov
9dfd9ac849 fix(ui): update Russian translations and add missing gain keys (#5329)
* feat(i18n): add album and track gain translation strings

* chore(i18n): update Russian translations

---------

Co-authored-by: Alexander Makeenkov <amakeenk@altlinux.org>
Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-12 13:17:46 -04:00
Deluan
1988a4162e refactor(configuration): improve error handling in configuration validation
Signed-off-by: Deluan <deluan@navidrome.org>
2026-04-12 12:18:13 -04:00
m8tec
c49e5855b9
feat(artwork): make max image upload size configurable (#5335)
* feat(config): make max image upload size configurable

Let max image upload size be set from config or environment instead of a fixed 10 MB cap. The upload handler still falls back to 10 MB when MaxImageUploadSize is not set.

Signed-off-by: M8te <38794725+m8tec@users.noreply.github.com>

* feat(config): support human-readable MaxImageUploadSize values

Max image upload size can now be configured as a readable string like 10MB or 1GB instead of raw bytes. The config load validates it at startup, and the upload handler parses it before applying request limits (10MB fallback if it fails).

+ MaxImageUploadSize as human-readable string
+ removed redundant max(1, ...) to address code review
+ cap memory usage of ParseMultipartForm to 10MB (address code review)

Signed-off-by: M8te <38794725+m8tec@users.noreply.github.com>

* refactor(config): consolidate MaxImageUploadSize default and add tests

Move the "10MB" default constant to consts.DefaultMaxImageUploadSize so
both the viper default and the runtime fallback share a single source of
truth. Improve the validator error message with fmt.Errorf wrapping to
match the project convention (e.g. validatePurgeMissingOption). Add unit
tests for validateMaxImageUploadSize (valid/invalid inputs) and
maxImageUploadSize (configured, empty, invalid, raw bytes). Compute
maxImageSize once at handler creation rather than per request.

---------

Signed-off-by: M8te <38794725+m8tec@users.noreply.github.com>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2026-04-12 11:16:00 -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