* test(scanner): run multi-library and selective scan specs on Windows
Both suites put their DB in Ginkgo's TempDir, but the shared db.Db() pool stays open until TestScanner returns, so on Windows the TempDir cleanup failed with a sharing violation. Use the same os.MkdirTemp plus best-effort RemoveAll setup as scanner_test.go and drop the skips.
Signed-off-by: junkerderprovinz <jdp@braethoria.com>
* test(scanner): say why the DB may stay open in the temp dir
Signed-off-by: junkerderprovinz <jdp@braethoria.com>
---------
Signed-off-by: junkerderprovinz <jdp@braethoria.com>
Co-authored-by: junkerderprovinz <jdp@braethoria.com>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
* fix(ui): skip the login form when already signed in
Opening #/login while signed in always showed the login form, so a
bookmarked or browser-autocompleted login URL asked for the password on
every visit although the stored session was still valid. Redirect to
the app when checkAuth succeeds, as react-admin's default login page
does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kris Anderson <1562427+kris-anderson@users.noreply.github.com>
* fix(ui): replace the login history entry on redirect
Pushing / left #/login one step back in history, so pressing Back
landed on the login page, which redirected forward again and kept the
user from returning to the previous page. Replace the entry instead.
Raised in review by Codex.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kris Anderson <1562427+kris-anderson@users.noreply.github.com>
* chore(ui): shorten the login redirect comment
Background moved here from the code comment: react-admin's default login
page sends an already signed-in user to the app, and Navidrome's custom
Login.jsx had dropped that check. Opening #/login from a bookmark or the
browser's autocomplete therefore showed the password form although the
stored session was still valid. The redirect uses replace so Back
returns to the previous page instead of landing on #/login again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Kris Anderson <1562427+kris-anderson@users.noreply.github.com>
---------
Signed-off-by: Kris Anderson <1562427+kris-anderson@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
* fix(jellyfin): list the user's libraries at the /Items user root
An unfiltered, non-recursive /Items query with no ParentId is answered by
Jellyfin with the user's libraries (the user root folder's children), not
their contents. Navidrome defaulted to albums there, so Symfonium found no
music library and synced nothing.
Fixes#6199
Signed-off-by: zapisanchez <zapisanchez@gmail.com>
* fix(jellyfin): page the user-root /Items libraries by StartIndex and Limit
* fix(jellyfin): sort the user-root /Items libraries by SortBy and SortOrder before paging
* refactor(jellyfin): simplify user-root sorting and filter detection
Share the first-SortOrder rule between applySort and sortViews through a new
isDescending helper, and make sortViews compare string keys instead of building
comparator closures. hasFilters now compares itemFilters to its zero value, so
a filter added there later also counts at the user root. Read the Recursive
param once, fix the paginate comment that said it only served the multi-type
path, and cut comments that restated the code. The user-root specs share one
library fixture and one request helper.
---------
Signed-off-by: zapisanchez <zapisanchez@gmail.com>
Co-authored-by: Deluan <deluan@navidrome.org>
* fix(jellyfin): honour every value of repeated list params - #6389
Jellyfin's model binder accepts an array query param both as one
comma-separated value and as repeated keys, and @jellyfin/sdk clients
(Jellify) send the latter. parseItemsQuery read IncludeItemTypes with
StringOr, so IncludeItemTypes=MusicArtist&IncludeItemTypes=Audio&...
behaved as an artist-only query and a mixed search silently lost songs,
albums and playlists. Filters had the same single-value read.
knownItemKinds now takes all values of the param, and parseItemFilters
walks them too, keeping the comma splitting each already did. Absent or
all-unknown lists still mean "no type filter", so the inferred-type and
album-default paths are unchanged.
Signed-off-by: yuwk <1729065730@qq.com>
* fix(jellyfin): join repeated query params so every value is parsed
Jellyfin binds a list param as one comma-separated value or as repeated
keys. joinedStrings feeds every value into the parsers that already
split on commas, so IncludeItemTypes, Filters, SortBy, SortOrder, the
artist id lists and Container all accept both spellings.
ArtistIds, AlbumArtistIds and contributingArtistIds now match every
artist in the list instead of only the first. SortOrder is paired with
each SortBy key, and a key past the end of the list reuses the first
direction, which is what Jellyfin's GetOrderBy does. Playlist SearchTerm
is left for a follow-up.
Signed-off-by: Vincent <52391318+ump45nose@users.noreply.github.com>
Co-authored-by: ump45nose <ump45nose@users.noreply.github.com>
* fix(jellyfin): apply multi-id artist filters as a single slice
ORing one contributing-artist filter per id let an album whose album
artist is one of the ids still match as Featured On via another id in
the list. ParticipantIDFilter and NotParticipantIDFilter already accept
a slice, so contributingArtistIds now uses one IN/NOT IN pair over the
whole list. Album and song artist filters do the same, which also avoids
one subquery per id.
Signed-off-by: Vincent <52391318+ump45nose@users.noreply.github.com>
Co-authored-by: ump45nose <ump45nose@users.noreply.github.com>
* refactor(jellyfin): trim comments in repeated list params change
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: yuwk <1729065730@qq.com>
Signed-off-by: Vincent <52391318+ump45nose@users.noreply.github.com>
Signed-off-by: Deluan <deluan@navidrome.org>
Co-authored-by: ump45nose <ump45nose@users.noreply.github.com>
Co-authored-by: Deluan <deluan@navidrome.org>
* fix(transcoding): keep the DSD cap unless the client declares an upper rate limit
Any audioSamplerate limitation on the target codec turned off the 88.2kHz
DSD cap, including GreaterThanEqual, NotEquals, empty or non-numeric limits.
None of those lower the rate, so DSD64 still went out at 352.8kHz. Only
LessThanEqual with a number, or Equals with at least one number, now replaces
the cap.
Follow-up to #6367, addressing
https://github.com/navidrome/navidrome/pull/6367#discussion_r4526602338
* refactor(stream): share upper-bound parsing between the DSD cap check and limits
applyIntLimitation and the DSD cap check now read LessThanEqual and Equals values through one upperBounds helper, so the two can't disagree on which limits lower a rate.
* fix(stream): cap DSD transcoded to lossless PCM at 88.2kHz
DSD64 was sent as 352.8kHz FLAC/WAV, which many clients cannot play.
Client and codec profile limits below 88.2kHz still apply.
ffprobe already reports DSD at rate/8 with 8 bits per sample, so only
rates read from tags are divided, and any DSD depth maps to 24-bit PCM.
Probed files used to come out at 44.1kHz with a bit depth of 8.
Fixes#6288
Co-authored-by: Olli Sulopuisto <1051255+ollisulopuisto@users.noreply.github.com>
Signed-off-by: Junker der Provinz <133605895+junkerderprovinz@users.noreply.github.com>
* fix(stream): let a client's sample rate limit replace the DSD cap
The 88.2kHz cap on DSD transcoded to lossless PCM is meant for clients that
say nothing about sample rates, like the legacy stream endpoint. Applied
unconditionally, it also overrode clients that declared their own
audioSamplerate limit: a client accepting up to 192kHz got 88.2kHz, and one
accepting only 96kHz or 192kHz got no compatible profile at all.
The cap now applies only when the client has no audioSamplerate limitation
for the target codec. Otherwise the client's limit decides the rate.
* refactor(stream): share the audio codec profile matcher
Direct play checks, transcode limitations and the new DSD cap check each
looped over the client's codec profiles with the same audio-type and codec
alias filter. They now use one audioCodecProfiles iterator, so the DSD cap
decision cannot drift from the limitations that are actually applied.
targetSampleRate takes a single capDSD flag instead of two adjacent bools.
* test(stream): table-drive the DSD to FLAC sample rate specs
Six specs repeated the same DSD source and FLAC transcoding profile and
differed only in the source rate, the client's codec profiles and the
expected rate. A DescribeTable lists the cap policy case by case, and
every entry now also checks the 24-bit depth.
---------
Signed-off-by: Junker der Provinz <133605895+junkerderprovinz@users.noreply.github.com>
Co-authored-by: Olli Sulopuisto <1051255+ollisulopuisto@users.noreply.github.com>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
* fix: preserve empty nested groups in smart playlist criteria marshaling
Fixes#6316
Empty nested groups were serialized incorrectly because of omitempty.
Now only the active conjunction key is emitted, so empty groups become [].
* docs: add changelog entry for #6316
* fix: recover playlists previously saved with empty nested groups
Treats the old broken {} form as an empty All on unmarshal so existing
playlists load again.
* docs: mention recovery of previously broken playlists in changelog
* style: run gofmt
* fix: safer recovery of empty groups and nil slices
- only treat {} as empty All when the group has no other rules
- marshal nil slices as [] instead of null
- drop CHANGELOG.md (release notes are generated from commits)
* test: remove hand-built criteria test
* test: add Ginkgo round-trip specs for empty nested groups
* chore: remove CHANGELOG.md
* docs: add docstrings to criteria json helpers
* Revert "docs: add docstrings to criteria json helpers"
This reverts commit 9f74c604a1.
* docs: add docstrings to criteria json helpers
* test: fix criteria round-trip specs to use Criteria and gomega
* fix(criteria): keep only the marshal fix for empty nested groups
Remove the recovery that turned a group made only of {} elements into an
empty All. It lived in the parser shared by POST bodies, .nsp files and
scrobble filters, so it accepted inputs such as {"all":[{}]} that were
rejected before, and turned them into rules that match the whole library.
It also had to guess whether a saved {} was an empty all or an empty any.
Rows already saved broken are a one-time cleanup, not something the parser
should carry.
Also remove the {}/null short-circuit (null was already handled, and {}
only widened what the parser accepts), drop the doc comments that restated
function names, and fold the round-trip specs into one table that checks
the exact marshaled output for empty nested all and any groups.
---------
Co-authored-by: kukirin-ui <kukirin-ui@users.noreply.github.com>
Co-authored-by: Deluan <deluan@navidrome.org>
* fix(persistence): apply the artist library scope to every search path - #6340
Signed-off-by: yuwk <1729065730@qq.com>
* test(persistence): expect a scoped artist search to find the artist
The library-scope specs only showed that searching another library
returns nothing. Also search with library_id = [lib2.ID] and expect
the artist that belongs to that library.
Signed-off-by: Vincent <52391318+ump45nose@users.noreply.github.com>
Co-authored-by: ump45nose <ump45nose@users.noreply.github.com>
* refactor(search): streamline artist search configuration and improve library filtering logic
---------
Signed-off-by: yuwk <1729065730@qq.com>
Signed-off-by: Vincent <52391318+ump45nose@users.noreply.github.com>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
Co-authored-by: ump45nose <ump45nose@users.noreply.github.com>
* fix(ui): remember the player play mode across reloads - #6361
The play mode was kept only in memory, so a page reload always reset it to
"In order". It is now saved to localStorage with the rest of the player
state, and clearing the queue (closing the player) keeps it too.
* test(ui): wait for the throttled save before reloading the store
When an issue author or another contributor offers to submit a fix, others
should ask in the issue before starting their own PR. This avoids duplicate
work and gives the person who did the investigation the first chance to
contribute the fix.
Signed-off-by: Deluan <deluan@navidrome.org>
* docs(contributing): propose updated contribution policy
Draft proposal for maintainer review. Rewrite the contribution guide around prior scope agreement, personal review, AI disclosure and maintainability, while keeping the commit conventions and DCO sign-off. Add AI use and review items to the PR template, keeping its headings.
Simplify the bug form so only what happened, the version and the Code of Conduct are required, and it no longer asks for an upgrade or a duplicate-search attestation. Update the selector text and point README feature requests to Discussions.
Keep the pull-requests anchor because the stale workflow links to it.
Signed-off-by: Deluan <deluan@navidrome.org>
* docs(contributing): allow AI-assisted PR text and review replies
Let contributors use AI tools to draft, edit, or translate PR explanations and review replies, as long as they review the final text, agree with it, and take responsibility for it. The PR template description hint now says the same instead of asking for the contributor's own words.
Signed-off-by: Deluan <deluan@navidrome.org>
* docs(contributing): route closed-issue regressions and no-agreement PRs
The bug form told reporters to add details to a matching report, but the lock workflow locks inactive issues and asks for a new issue instead. It now says to use a matching open report, or open a new one that links a closed issue.
The PR template and guide only offered the small-change exception when prior agreement isn't needed, which doesn't fit translation updates. Both now ask contributors to explain why agreement isn't needed.
Signed-off-by: Deluan <deluan@navidrome.org>
* docs(contributing): ask contributors to be mindful of the maintainer's time
Add the request to the end of the communication paragraph in the guide and to the PR template's description hint.
Signed-off-by: Deluan <deluan@navidrome.org>
* docs(issues): require reproducing bugs on the latest release
Restore a required checkbox in the bug form, placed before the version field: "I'm using the latest Navidrome release and can reproduce the issue on it." Drop the version help sentence that said upgrading isn't required, since it now contradicts the checkbox.
Signed-off-by: Deluan <deluan@navidrome.org>
* docs(issues): turn the bug form search guidance into a checkbox
The long intro paragraph was easy to skip. It now only thanks the reporter and links private security reporting. A required checkbox, grouped with the latest-release one at the top, confirms the reporter searched open and closed issues. The "What happened" help asks reporters to link a matching closed issue.
The steps help now says "If it happens only sometimes" instead of mentioning problems that can't be reproduced, so it no longer conflicts with the required reproduction checkbox.
Signed-off-by: Deluan <deluan@navidrome.org>
* docs(pr-template): ask for AI use with Yes and No checkboxes
Replace the free-text "Yes / No" line with two checkboxes. Contributors tick exactly one, so empty boxes clearly mean the question wasn't answered.
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
Picks up the progress slider focus fix from navidrome/react-music-player#11. On 4.25.4, focus changes on the progress handle started and committed a synthetic rc-slider drag, which seeked the audio. 4.25.5 keeps native focus but does not start a drag on focus.
* feat(themes): add Cave theme
Near-black background with a single orange accent, based on the colors
of https://acave.pt.
Signed-off-by: Tiago Carrondo <tiago@carrondo.pt>
* fix(themes): raise Cave button hover contrast
Dark text on the hover color was 4.27:1; #e84518 brings it to 4.96:1.
Signed-off-by: Tiago Carrondo <tiago@carrondo.pt>
* fix(themes): keep Cave in alphabetical order in the themes list
---------
Signed-off-by: Tiago Carrondo <tiago@carrondo.pt>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
Motivation: The `key` tag was never populated from MP3 (TKEY), M4A
(iTunes initialkey) or FLAC (INITIALKEY) files. TagLib exposes all three
as the raw tag "initialkey", which was not among the aliases for `key`
in mappings.yaml, so the value showed up in rawTags but not in mappedTags.
Approach: Add the lowercase alias `initialkey` to the `key` entry in
resources/mappings.yaml, plus a unit test.
Validation: Added a test in model/metadata/metadata_test.go that feeds
{"initialkey": ["Ebm"]} to metadata.New and expects `key` == "Ebm". It
fails without the alias and passes with it (go test ./model/metadata/).
`go test ./model/...` passes and gofmt is clean. I did not scan real
audio files end to end; the raw tag name comes from the issue's
`navidrome inspect` output.
Report: https://github.com/navidrome/navidrome/issues/6369
Assisted-by: claude-sonnet-5-5 (via Claude Code)
Signed-off-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com>
Co-authored-by: Pujitha Paladugu <10557236+pujitha24@users.noreply.github.com>
On phone-width screens the playlist table needs about 473px (cover, name, duration, favourite and edit columns), so even a short playlist name made the whole page wider than a 390px viewport and pushed the app bar icons off screen. react-admin's layout root uses min-width: fit-content, so the page grows to fit its widest content. On xs screens the list card now has a max-width of 100vw and scrolls horizontally, keeping the page at screen width while the favourite and edit buttons stay reachable by swiping the table. Long playlist names without spaces can also wrap there. Desktop and tablet layouts are unchanged.
* feat(ui): add a Date Format setting to choose language or browser dates
Since #6160 dates follow the selected UI language, which is wrong for users who run the UI in one language but want their own region's date format. The Personal page now has a Date Format select with "Match language" (default, current behavior) and "Match browser". The choice is kept in localStorage and read by useDateLocale, which returns no locale in browser mode so Intl uses the browser's own.
Six screens imported react-admin's DateField directly and so ignored the language setting (playlist, radio and missing files lists, album table, song and album info). They now use the common DateField wrapper, which also reads the record from the record context.
* feat(ui): add pt-BR translations for the Date Format setting
Run the Docker Hub manifest job as a matrix over the target repositories,
so the existing DOCKER_HUB_REPO (deluan/navidrome) push is unchanged and
the upstream repository also pushes the same tags to navidrome/navidrome.
Each leg fails independently, so a problem with one repository does not
block the other.
Signed-off-by: Deluan <deluan@navidrome.org>
* fix(jellyfin): honor likes on /Rating and add POST UserData endpoints
POST .../Rating only read a numeric `rating` param that upstream Jellyfin
does not define, so a request using Jellyfin's `likes` flag (or no value
at all, or a malformed one) silently cleared the user's rating. The
handler now reads `likes` like Jellyfin (true stores 5 stars, false 1
star, matching Jellyfin's 10 and 1), keeps the numeric `rating` param for
existing clients, and answers 400 without writing when the value is
missing, malformed or outside 0-10. Clearing is left to DELETE. The
current /UserItems/{itemId}/Rating spelling is now routed as well.
Adds POST /UserItems/{itemId}/UserData and the legacy
/Users/{userId}/Items/{itemId}/UserData. They write Rating, Likes and
IsFavorite with Jellyfin's precedence (Rating wins over Likes), leave
omitted or null fields untouched, and ignore play-state fields. The body
must be a single JSON object: trailing data, null, or out-of-range values
are rejected before anything is written.
Rating and UserData writes whose userId names a different user now get
403 instead of silently writing the caller's own data. Annotation
responses return the item's full UserData, including Jellyfin's derived
Likes flag (rating >= 6.5). The README compatibility notes are updated.
* refactor(jellyfin): simplify rating and UserData handlers
Moves the Jellyfin 0-10 to stars conversions (StarsFromRating,
StarsFromLikes) into the dto package next to the reverse mapping and the
Likes threshold, so both directions live together. The userId ownership
check is folded into a single applyCallerUserData helper instead of being
called by hand in three handlers, the setFavorite passthrough is dropped,
and decodeUserDataBody now writes its own 400 like the other (value, ok)
helpers.
Tests: withChiURLParam is now variadic, replacing the duplicate
withChiParams helper; the three other-user 403 specs and the
empty-object/no-event spec become table entries; and redundant specs are
removed. Behavior is unchanged.
* feat(ui): add Solarized Light and Dark themes
Add Light and Dark variants of Ethan Schoonover's Solarized palette, built
on the Tokyo Night theme structure so they cover the app bar, sidebar,
tables, album grid, album/playlist details, login and the player.
Text colors are one step stronger than canonical Solarized body text
(base01 on light, base1 on dark) so table text keeps WCAG AA contrast.
The Dark variant uses cyan instead of violet as the secondary accent,
since violet is too dark on base03.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: rucolastico <diegoricomartell@gmail.com>
* fix(ui): improve Solarized contrast and table row hover
Address review feedback on the Solarized themes:
- Use text-specific shades for links and the login system name, keeping
them above 4.5:1 on table surfaces and hover rows (darkened blue on
Light, lightened cyan on Dark). Canonical blue stays for controls.
- Darken the Light theme's violet accent, which is also used as text
(album artist), to keep it above 4.5:1.
- Give secondary buttons readable text: base3 on violet (Light) and
base03 on cyan (Dark), instead of the body text color.
- Apply the row hover color to table cells, whose opaque background was
hiding the row hover.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: rucolastico <diegoricomartell@gmail.com>
* fix(ui): keep Solarized app bar title and icons readable
The app bar uses the secondary color, whose contrastText matched the
app bar background (1:1 in Light, 1.15:1 in Dark). Override the app bar's
colorSecondary text with the theme foreground (4.99:1 Light, 4.86:1 Dark),
keeping secondary.contrastText for buttons on the accent color.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: rucolastico <diegoricomartell@gmail.com>
---------
Signed-off-by: rucolastico <diegoricomartell@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Deluan Quintão <deluan@navidrome.org>
Builds the removeOrphans fixture with a single INSERT instead of shifting positions in two UPDATE passes, and has the Insert test keep the playlist id instead of reading a private repository field. Two comments now say that addTracks owns the existence check as well as the library check, and that Insert closes holes left by any id keepAccessible drops. No behavior change.
Adding tracks to a playlist skipped the media_file lookup for admins and for users who can see every library, so ids matching no track were stored as orphan playlist_tracks rows and reported as added. The native API returned an inflated "added" count, and the hidden rows left gaps in the positions, so Subsonic songIndexToRemove could delete a hidden row instead of the song at that index.
keepAccessible now always checks the ids against media_file. applyLibraryFilter is already a no-op for those users, so the check only drops unknown ids. Rows stored before this fix are still removed by the existing GC removeOrphans step on the next full scan, or on any scan that finds changes.
Fixes#6310
* fix(scanner): respect FollowSymlinks in watcher-triggered scans
With FollowSymlinks disabled, creating a folder symlink inside the library
made the watcher schedule a selective scan with the link itself as the
target. walkDirTree only checked FollowSymlinks for child entries, so it
walked the link and imported its files as duplicates (or, for links that
point outside the library, files that should never be scanned).
walkDirTree now skips any target folder whose path, or any parent folder,
is a symlink when FollowSymlinks is disabled. The skipped target stays in
lastUpdates, so rows previously imported through it are marked missing,
matching what a full scan does. localFS now implements fs.ReadLinkFS so
fs.Lstat can see symlinks instead of following them.
Fixes#6292
* test(scanner): run the #6292 symlinked target tests on Windows
Remove the SkipOnWindows guard from the symlinked target folder tests, so
the go-windows CI job covers the FollowSymlinks fix for selective scans.
Playlist().Tracks returns nil when its internal Get fails (for example when the
context is canceled at shutdown), and resolvePlaylist called GetAlbumIDs on it,
panicking with a nil pointer dereference. The artwork drain runs on a bare
goroutine, so the panic killed the whole server.
resolvePlaylist now returns an error when Tracks is nil, and the worker recovers
panics per item: it logs the panic with the item details and stack, and marks
the item as a failed attempt so the rest of the batch still runs.
Fixes#6266
* fix(ui): make playlist toggle switches visible in all themes
The Public and Auto-import switches in the playlist list did not set a
color, so Material-UI used the theme's secondary color. Many themes use
secondary as a surface color close to the table background, which made
checked switches nearly invisible (Catppuccin, Rosé Pine, Monokai,
Moonbase and others).
Set color="primary" on the playlist switch, like every other switch in
the app, and make primary the default MuiSwitch color in useCurrentTheme
so future switches cannot regress. Fixes#6272.
* refactor(ui): drop secondary switch overrides from themes
Dracula, Gruvbox Dark, Tokyo Night and Tokyo Night Light styled checked
MuiSwitch colorSecondary to work around the same invisible-switch problem
(Gruvbox in #5064). With primary as the default switch color and every
switch in the app using it, no switch renders with colorSecondary anymore,
so these overrides are dead code.