Commit graph

5,225 commits

Author SHA1 Message Date
navidrome-bot
e567b0d8c3 fix(ui): update German, Greek, Estonian, Finnish, Galician, Hungarian, Dutch, Slovak translations from POEditor 2026-11-14 10:08:50 +00:00
Junker der Provinz
2ce7ef53ca
test(scanner): run multi-library and selective scan specs on Windows (#5381) (#6307)
* 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>
2026-11-13 22:58:10 -05:00
Kris Anderson
88adc02f74
fix(ui): skip the login form when already signed in (#6459)
* 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>
2026-11-13 20:16:40 -05:00
Deluan Quintão
3a40f29aa5
feat(scanner): add option to skip folders containing a .nomedia file (#6139) 2026-11-13 19:38:14 -05:00
Deluan Quintão
55e52b7ce4
fix(server): skip the startup music folder write when the path is unchanged (#6457) 2026-11-13 17:13:15 -05:00
Deluan Quintão
6177c92e32
test(scanner): restore DbPath after Ordered containers and reset runPhase counter (#6456) 2026-11-13 15:21:14 -05:00
Deluan Quintão
4ee78867c9
refactor(conf): make the global config copy-on-write to stop config data races in tests (#6455) 2026-11-13 11:42:14 -05:00
Deluan Quintão
94a6e59fee
fix(stream): match the estimated Content-Length on uncached transcodes, and return 200 on HEAD (#6449) 2026-11-12 16:53:15 -05:00
Deluan Quintão
a7f19c7c7a
fix(transcoding): declare the duration of a piped mp3 transcode (#6218) 2026-11-11 19:03:55 -05:00
Kendall Garner
ff58ca5d1f
fix(play tracker): record local play count even if external scrobbling is disabled on the player (#6387) 2026-11-11 14:15:15 -05:00
Deluan
2fecb718c0 ci: raise golangci-lint timeout to 3m
Package loading alone hit the 2m limit on a slow runner.
2026-11-11 08:16:03 -05:00
Junker der Provinz
514207faf1
fix(server): classify library path validation errors instead of a catch-all (#4595) (#5919) 2026-11-10 21:56:14 -05:00
Deluan Quintão
d22a31aee0
fix(subsonic): honour explicit download format over player transcoding (#6446) 2026-11-10 19:53:12 -05:00
Adrián Sánchez Zapico
1efd5c467b
fix(jellyfin): list the user's libraries at the /Items user root (#6214)
* 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>
2026-11-10 17:44:00 -05:00
Santhi Prakash
64cf86fae9
fix(jellyfin): clamp negative StartIndex to zero - #6341 (#6366) 2026-11-10 16:12:19 -05:00
ump45nose
27d041486c
fix(jellyfin): honour every value of repeated list params (#6414)
* 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>
2026-11-10 12:41:46 -05:00
Deluan Quintão
9f0441352c
fix(transcoding): keep the DSD cap unless the client declares an upper rate limit (#6445)
* 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.
2026-11-10 10:59:28 -05:00
Junker der Provinz
ecbd4b7d55
fix(transcoding): default DSD transcoded to lossless PCM to 88.2kHz
* 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>
2026-11-10 10:14:37 -05:00
Alen Požun
818603436c
fix(smartplaylists): preserve empty nested groups in smart playlist criteria marshaling (#6438)
* 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>
2026-11-10 09:26:16 -05:00
Deluan Quintão
93cead9b16
fix(search): scope artist search counts to the requested libraries (#6434) 2026-11-09 12:15:16 -05:00
ump45nose
e40d54fd40
fix(search): apply the artist library scope to every search path - #6340 (#6415)
* 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>
2026-11-09 11:35:05 -05:00
Deluan Quintão
93e01f390e
fix(ui): remember the player play mode across reloads (#6433)
* 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
2026-11-09 08:07:00 -05:00
Coffeeicus
f5b6805b31
fix(ui): update Turkish localisation (#6412)
* fix(ui): update Turkish localisation

* Update tr.json

* Update tr.json

* Update tr.json

---------

Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2026-11-08 18:22:44 -05:00
Deluan
97ae17cab3 docs: ask before working on an issue someone offered to fix
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>
2026-11-08 18:01:18 -05:00
Deluan Quintão
a4f75cf6c4
docs: update contribution policy and PR/Issues templates (#6417)
* 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>
2026-11-08 13:20:09 -05:00
Deluan Quintão
6a38dc988b
fix(ui): bump navidrome-music-player to 4.25.5 (#6416)
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.
2026-11-07 16:59:42 -05:00
ump45nose
7c1e68a1e4
fix(subsonic): reject setRating values outside the 0-5 range - #6411 (#6413) 2026-11-03 11:26:14 -05:00
Tiago Carrondo
755da3ea59
feat(ui): add Cave theme (#6374)
* 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>
2026-11-03 10:58:48 -05:00
Pujitha Paladugu
c9c70c952b
fix(scanner): map TagLib's normalized "initialkey" tag to the key tag (#6384)
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>
2026-11-02 21:54:46 -05:00
DawidKrynski
563c5c72a1
fix(db): refuse to restore a backup file that is not a Navidrome database - #6295 (#6296) 2026-11-02 19:43:23 -05:00
Xabi
2a57570e90
fix(ui): update Basque localisation (#6133)
* Update Basque localisation

Added strings:
refresh
scrobbleFilter
scrobbleFilter
invalidScrobbleFilter
choices {noDefaultBitRate}
starred
idHelp
metadataRefreshStarted
onlyFavourites

* Update eu.json

some syntax issue

---------

Co-authored-by: Deluan Quintão <deluan@navidrome.org>
2026-11-01 14:35:11 -05:00
Deluan Quintão
ec2e0a9ea9
fix(ui): stop the playlist list from overflowing on mobile (#6376)
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.
2026-10-23 16:25:10 +01:00
Deluan Quintão
56cc32aede
fix(scanner): map movement number and count from ID3 MVIN and MP4 tags (#6357)
TagLib reports ID3 MVIN and MP4 ©mvi as movementnumber, and MP4 ©mvc as
movementcount. Neither name was aliased in mappings.yaml, so getSong
returned movements with the name only. Add both aliases.

ID3 MVIN stores the value as n/N (e.g. 1/4), which parsed as 0. Split it
at scan time, like track and disc numbers, so the stored movement tag holds
the number and movementtotal gets the count when the file has no explicit
total. Storing the clean value also lets smart playlist rules on the
movement tag match. Existing files need a full scan to pick this up.

Fixes #6356
2026-10-21 13:26:26 +01:00
Deluan Quintão
df77c462e8
feat(ui): add a Date Format setting to choose language or browser dates (#6263)
* 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
2026-10-18 01:00:24 -07:00
Deluan Quintão
a45adbbb2e
chore: move README badge and dev image tag to navidrome/navidrome 2026-10-16 14:54:33 -07:00
Deluan Quintão
be201a2d79
ci: also publish Docker images to navidrome/navidrome on Docker Hub (#6327)
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>
2026-10-14 07:23:26 -07:00
Deluan Quintão
30663f7d4f
fix(jellyfin): honor likes on /Rating and add POST UserData endpoints
* 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.
2026-10-12 11:49:07 -07:00
Rucolastico
8e306b1f61
feat(ui): add Solarized Light and Dark themes (#6280)
* 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>
2026-10-11 13:33:34 -04:00
Deluan Quintão
4ae97ab59e
Merge pull request #6313 from navidrome/investigate-6310-20261010
fix(playlists): drop unknown track ids when adding to a playlist
2026-10-11 10:05:15 -07:00
Deluan
803b2da8ee refactor(playlists): simplify #6310 tests and update check comments
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.
2026-10-10 11:35:43 -04:00
Deluan
74e96a2bb7 fix(playlists): drop unknown track ids when adding to a playlist
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
2026-10-10 11:28:58 -04:00
Deluan Quintão
497dfe2586
Merge pull request #6300 from navidrome/docs/hosting-badges-6258
docs: render managed hosting links as proportionate badges
2026-10-09 05:45:55 -07:00
deluan
b70ba3a49e docs: use equal-size local hosting badges without stretching artwork 2026-10-08 22:19:34 -04:00
deluan
d5a2e1e813 docs: match README deployment badges to native PikaPods height 2026-10-08 22:02:28 -04:00
deluan
b4d2c3e4af docs: render hosting providers as proportionate deployment badges 2026-10-08 21:59:52 -04:00
raphmim
22ce85e5d5 Minor fixes based on AI suggestions 2026-10-08 21:59:04 -04:00
raphmim
aea60c6e99 Badges for new cloud providers 2026-10-08 21:59:04 -04:00
Deluan Quintão
b78c963931
fix(scanner): respect FollowSymlinks in watcher-triggered scans (#6293)
* 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.
2026-10-08 17:34:42 -04:00
Deluan Quintão
52135913d4
fix(artwork): don't crash the server when a playlist's tracks can't be loaded (#6267)
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
2026-10-06 07:57:07 -07:00
Deluan Quintão
caa2f8a0c0
fix(ui): make playlist toggle switches visible in all themes (#6277)
* 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.
2026-10-06 09:50:11 -04:00