navidrome/core
Deluan Quintão 1f3034f022
fix(playlist): block track edits on synced playlists across all APIs (#5984)
* fix(playlist): block track edits on synced playlists across all APIs

A synced playlist's tracks come from its source file, so any track edit made
through the UI or an API was silently reverted on the next scan. Track mutations
funnel through two service guards, checkTracksEditable (incremental edits) and
Create (wholesale replace, used by Subsonic createPlaylist and Jellyfin's
replace path), which each duplicated the smart-playlist check. Both now consult
a shared model.Playlist.TracksEditable() predicate, so the native, Subsonic, and
Jellyfin paths are all locked: track edits return ErrNotAuthorized (403, or
Subsonic error 50) instead of being accepted and lost. Metadata-only edits
(name, comment, public, the sync flag itself) still go through checkWritable and
are unaffected. In the UI, a synced playlist's track list becomes read-only,
mirroring how smart playlists already behave.

* fix(playlist): return 409 Conflict for non-editable playlist track edits

The previous commit rejected track edits on smart and synced playlists with
ErrNotAuthorized (403). That conflates two different things: a 403 says the
caller lacks permission, but a synced or smart playlist's tracks are immutable
for everyone, including the owner and admins. It is a property of the resource,
not the caller.

Introduce ErrPlaylistNotEditable and return it from both track-edit guards. The
Native and Jellyfin APIs now map it to 409 Conflict; Subsonic maps it to error
50, the closest code it has (it has no read-only concept). The Native track
handlers previously mapped this rejection inconsistently (400 on add, 500 on
remove, 403 on reorder) through a new shared writePlaylistError helper. Genuine
authorization failures (non-owner, non-admin) still return ErrNotAuthorized.

* fix(playlist): surface synced read-only state in picker, Jellyfin, and OpenSubsonic

Follow-up to the track-edit lock: the read-only state was enforced but not
advertised consistently, so clients still offered edits that the server rejects.

- UI: the Add to Playlist picker filtered targets by isWritable only, offering
  synced playlists that then 409 on add. It now filters with canChangeTracks.
- Jellyfin: addToPlaylist/removeFromPlaylist hard-coded every error to 404, so a
  locked playlist reported "not found" instead of 409. They now return 409 for
  ErrPlaylistNotEditable while keeping the deliberate anti-probing 404 for every
  other error (a non-owner never reaches ErrPlaylistNotEditable, so 409 leaks
  nothing).
- OpenSubsonic: buildOSPlaylist marked only smart playlists readonly; owned
  synced playlists advertised readonly=false. Readonly now also covers
  !TracksEditable(), matching the existing smart-playlist treatment.

* fix(jellyfin): report CanEdit from playlist editability in permission probes

getPlaylistUsers and getPlaylistUser returned CanEdit: true unconditionally, so
Finamp (which probes this before showing edit controls) offered track editing on
synced/smart playlists whose add/remove requests now return 409. Both handlers
now fetch the playlist and set CanEdit from TracksEditable(), keeping the
deliberate non-owner looseness (CanEdit stays true for a normal playlist a
non-owner views) and mapping any lookup error to 404 like the sibling probes.

* fix(playlist): check ownership before editability when replacing tracks

Create checked TracksEditable() before ownership, so a non-owner replacing
another user's public smart/synced playlist (Jellyfin updatePlaylist with a
non-empty Ids list) received a 409 read-only conflict instead of a 403
authorization failure. The incremental guards check ownership first via
checkWritable; Create now matches that order. Subsonic is unaffected (both errors
map to code 50). Owners of their own smart/synced playlists still get the
read-only conflict.

* fix(jellyfin): return 403 for locked playlists, matching Jellyfin

Jellyfin itself refuses edits on its file-backed playlists with Forbid() (403):
PlaylistsController gates every mutation on OwnerUserId == caller or a share with
CanEdit, and playlists imported from .m3u files satisfy neither. Its CanEdit is
an ACL field, not a read-only marker, and Jellyfin core has no server-managed
playlist type at all.

Our Jellyfin routes exist to imitate that API, so ErrPlaylistNotEditable now maps
to 403 there instead of 409. The native API keeps 409 (a resource-state conflict
is the accurate REST answer where we define the contract) and Subsonic keeps
error 50, its closest code.

* chore(playlist): trim comments added by this branch

Several comments ran to three or four lines and carried rationale that belongs in
the commit history rather than the code: what Jellyfin does with its own
file-backed playlists, and restatements of the expressions directly below them.
Each block is now one or two lines covering only the non-obvious why.
2026-08-19 08:47:53 -04:00
..
agents fix(plugins): stop reporting plugin call failures as not-found (#5953) 2026-08-13 22:56:09 -04:00
artwork fix(artwork): re-resolve artwork when image files change on disk (#5965) 2026-08-16 13:24:07 -04:00
auth refactor(db): migrate all ids to a uniform canonical 128-bit base62 encoding (#5824) 2026-08-02 12:58:53 -04:00
external fix(instant-mix): top short mixes up instead of returning what the first source found (#5951) 2026-08-13 18:50:34 -04:00
ffmpeg fix(streaming): surface why a transcode decision failed (#5820) 2026-07-19 18:51:32 -04:00
lyrics fix(scanner): stop logging expected lyrics sniff misses as warnings (#5702) 2026-07-02 09:46:57 -04:00
matcher feat(listenbrainz): match collaboration top-songs via all credited artist MBIDs (#5670) 2026-06-26 17:06:14 -04:00
metrics feat(insights): report the app store or hosting platform via ND_PLATFORM (#5956) 2026-08-14 13:46:52 -04:00
playback refactor: more warnings clean up 2026-05-20 17:43:12 -03:00
playlists fix(playlist): block track edits on synced playlists across all APIs (#5984) 2026-08-19 08:47:53 -04:00
publicurl refactor(db): migrate all ids to a uniform canonical 128-bit base62 encoding (#5824) 2026-08-02 12:58:53 -04:00
scrobbler feat(scrobbler): add per-user scrobble filter (#5964) 2026-08-15 16:10:53 -04:00
sonic feat(jellyfin): AudioMuse-AI compatible sonic endpoints (#5782) 2026-07-15 20:44:56 -04:00
storage fix(scanner): resolve file symlinks with the production local storage FS (#5755) 2026-07-10 10:52:05 -04:00
stream fix(subsonic): omit bit depth for lossy targets in transcode decision (#5768) 2026-07-12 13:27:01 -04:00
archiver.go feat(artist): add Share and Download actions to the Artist detail page (#5944) 2026-08-12 11:59:56 -04:00
archiver_test.go feat(artist): add Share and Download actions to the Artist detail page (#5944) 2026-08-12 11:59:56 -04:00
common.go feat(bfr): Big Refactor: new scanner, lots of new fields and tags, improvements and DB schema changes (#2709) 2025-02-19 20:35:17 -05:00
common_test.go ci: run Go tests on Windows (#5380) 2026-04-19 13:16:47 -04:00
core_suite_test.go Rename log.LevelCritical to log.LevelFatal 2022-12-21 14:53:36 -05:00
inspect.go refactor: multiple syntax updates for Go 1.26 2026-05-19 18:02:36 -03:00
library.go perf(subsonic): speed up artist search3 deep-offset pagination (#5620) 2026-06-16 21:47:15 -04:00
library_test.go feat: add go-taglib pure Go metadata extractor (#4902) 2026-01-18 14:42:53 -05:00
maintenance.go refactor: run Go modernize (#5002) 2026-02-08 09:57:30 -05:00
maintenance_test.go feat(scanner): implement selective folder scanning and file system watcher improvements (#4674) 2025-11-14 22:15:43 -05:00
players.go feat(server): group Subsonic config options together 2025-03-05 12:29:30 -08:00
players_test.go Use userId in player, other fixes (#3182) 2024-08-03 13:37:21 -04:00
share.go chore: go fix 2026-05-28 22:13:05 -03:00
share_test.go refactor: extract TruncateRunes function for safe string truncation with suffix 2025-11-06 14:27:38 -05:00
user.go refactor: run Go modernize (#5002) 2026-02-08 09:57:30 -05:00
user_test.go feat(plugins): New Plugin System with multi-language PDK support (#4833) 2026-01-14 19:22:48 -05:00
wire_providers.go feat(artwork): new artwork pipeline with background resolution and Low Quality Image Placeholders (#5847) 2026-08-09 15:03:27 -04:00