2020-01-19 14:21:44 -09:00
|
|
|
package subsonic
|
2020-01-07 10:56:26 -09:00
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"encoding/xml"
|
2022-09-30 14:54:25 -08:00
|
|
|
"errors"
|
2020-01-07 10:56:26 -09:00
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
2026-02-08 06:33:46 -09:00
|
|
|
"regexp"
|
2020-01-07 10:56:26 -09:00
|
|
|
|
2021-05-11 13:21:18 -08:00
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
|
"github.com/go-chi/chi/v5/middleware"
|
2023-01-13 14:10:32 -09:00
|
|
|
"github.com/navidrome/navidrome/conf"
|
2020-07-10 09:11:02 -08:00
|
|
|
"github.com/navidrome/navidrome/core"
|
2022-12-25 12:07:28 -09:00
|
|
|
"github.com/navidrome/navidrome/core/artwork"
|
2025-04-08 17:11:09 -08:00
|
|
|
"github.com/navidrome/navidrome/core/external"
|
2025-06-27 18:13:57 -08:00
|
|
|
"github.com/navidrome/navidrome/core/metrics"
|
2024-05-08 18:21:38 -08:00
|
|
|
"github.com/navidrome/navidrome/core/playback"
|
refactor: move playlist business logic from repositories to service layer (#5027)
* refactor: move playlist business logic from repositories to core.Playlists service
Move authorization, permission checks, and orchestration logic from
playlist repositories to the core.Playlists service, following the
existing pattern used by core.Share and core.Library.
Changes:
- Expand core.Playlists interface with read, mutation, track management,
and REST adapter methods
- Add playlistRepositoryWrapper for REST Save/Update/Delete with
permission checks (follows Share/Library pattern)
- Simplify persistence/playlist_repository.go: remove isWritable(),
auth checks from Delete()/Put()/updatePlaylist()
- Simplify persistence/playlist_track_repository.go: remove
isTracksEditable() and permission checks from Add/Delete/Reorder
- Update Subsonic API handlers to route through service
- Update Native API handlers to accept core.Playlists instead of
model.DataStore
* test: add coverage for playlist service methods and REST wrapper
Add 30 new tests covering the service methods added during the playlist
refactoring:
- Delete: owner, admin, denied, not found
- Create: new playlist, replace tracks, admin bypass, denied, not found
- AddTracks: owner, admin, denied, smart playlist, not found
- RemoveTracks: owner, smart playlist denied, non-owner denied
- ReorderTrack: owner, smart playlist denied
- NewRepository wrapper: Save (owner assignment, ID clearing),
Update (owner, admin, denied, ownership change, not found),
Delete (delegation with permission checks)
Expand mockedPlaylistRepo with Get, Delete, Tracks, GetWithTracks, and
rest.Persistable methods. Add mockedPlaylistTrackRepo for track
operation verification.
* fix: add authorization check to playlist Update method
Added ownership verification to the Subsonic Update endpoint in the
playlist service layer. The authorization check was present in the old
repository code but was not carried over during the refactoring to the
service layer, allowing any authenticated user to modify playlists they
don't own via the Subsonic API. Also added corresponding tests for the
Update method's permission logic.
* refactor: improve playlist permission checks and error handling, add e2e tests
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: rename core.Playlists to playlists package and update references
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: rename playlists_internal_test.go to parse_m3u_test.go and update tests; add new parse_nsp.go and rest_adapter.go files
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: block track mutations on smart playlists in Create and Update
Create now rejects replacing tracks on smart playlists (pre-existing
gap). Update now uses checkTracksEditable instead of checkWritable
when track changes are requested, restoring the protection that was
removed from the repository layer during the refactoring. Metadata-only
updates on smart playlists remain allowed.
* test: add smart playlist protection tests to ensure readonly behavior and mutation restrictions
* refactor: optimize track removal and renumbering in playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: implement track reordering in playlists with SQL updates
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: wrap track deletion and reordering in transactions for consistency
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: remove unused getTracks method from playlistTrackRepository
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: optimize playlist track renumbering with CTE-based UPDATE
Replace the DELETE + re-INSERT renumbering strategy with a two-step
UPDATE approach using a materialized CTE and ROW_NUMBER() window
function. The previous approach (SELECT all IDs, DELETE all tracks,
re-INSERT in chunks of 200) required 13 SQL operations for a 2000-track
playlist. The new approach uses just 2 UPDATEs: first negating all IDs
to clear the positive space, then assigning sequential positions via
UPDATE...FROM with a CTE. This avoids the UNIQUE constraint violations
that affected the original correlated subquery while reducing per-delete
request time from ~110ms to ~12ms on a 2000-track playlist.
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: rename New function to NewPlaylists for clarity
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: update mock playlist repository and tests for consistency
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-21 15:57:13 -09:00
|
|
|
playlistsvc "github.com/navidrome/navidrome/core/playlists"
|
2021-06-19 16:56:56 -08:00
|
|
|
"github.com/navidrome/navidrome/core/scrobbler"
|
2020-02-01 16:07:15 -09:00
|
|
|
"github.com/navidrome/navidrome/log"
|
2020-07-31 09:07:39 -08:00
|
|
|
"github.com/navidrome/navidrome/model"
|
2024-09-30 16:46:10 -08:00
|
|
|
"github.com/navidrome/navidrome/server"
|
2021-06-10 08:20:52 -08:00
|
|
|
"github.com/navidrome/navidrome/server/events"
|
2020-01-23 15:44:08 -09:00
|
|
|
"github.com/navidrome/navidrome/server/subsonic/responses"
|
2023-12-21 12:32:37 -09:00
|
|
|
"github.com/navidrome/navidrome/utils/req"
|
2020-01-07 10:56:26 -09:00
|
|
|
)
|
|
|
|
|
|
2020-11-01 13:04:53 -09:00
|
|
|
const Version = "1.16.1"
|
2020-01-07 10:56:26 -09:00
|
|
|
|
2026-02-08 06:33:46 -09:00
|
|
|
var validJSIdentifier = regexp.MustCompile(`^[a-zA-Z_$][a-zA-Z0-9_$.]*$`)
|
|
|
|
|
|
2022-11-21 08:57:56 -09:00
|
|
|
type handler = func(*http.Request) (*responses.Subsonic, error)
|
|
|
|
|
type handlerRaw = func(http.ResponseWriter, *http.Request) (*responses.Subsonic, error)
|
2020-01-07 10:56:26 -09:00
|
|
|
|
2020-01-11 08:37:05 -09:00
|
|
|
type Router struct {
|
2021-06-13 08:46:36 -08:00
|
|
|
http.Handler
|
2025-04-08 17:11:09 -08:00
|
|
|
ds model.DataStore
|
|
|
|
|
artwork artwork.Artwork
|
|
|
|
|
streamer core.MediaStreamer
|
|
|
|
|
archiver core.Archiver
|
|
|
|
|
players core.Players
|
|
|
|
|
provider external.Provider
|
refactor: move playlist business logic from repositories to service layer (#5027)
* refactor: move playlist business logic from repositories to core.Playlists service
Move authorization, permission checks, and orchestration logic from
playlist repositories to the core.Playlists service, following the
existing pattern used by core.Share and core.Library.
Changes:
- Expand core.Playlists interface with read, mutation, track management,
and REST adapter methods
- Add playlistRepositoryWrapper for REST Save/Update/Delete with
permission checks (follows Share/Library pattern)
- Simplify persistence/playlist_repository.go: remove isWritable(),
auth checks from Delete()/Put()/updatePlaylist()
- Simplify persistence/playlist_track_repository.go: remove
isTracksEditable() and permission checks from Add/Delete/Reorder
- Update Subsonic API handlers to route through service
- Update Native API handlers to accept core.Playlists instead of
model.DataStore
* test: add coverage for playlist service methods and REST wrapper
Add 30 new tests covering the service methods added during the playlist
refactoring:
- Delete: owner, admin, denied, not found
- Create: new playlist, replace tracks, admin bypass, denied, not found
- AddTracks: owner, admin, denied, smart playlist, not found
- RemoveTracks: owner, smart playlist denied, non-owner denied
- ReorderTrack: owner, smart playlist denied
- NewRepository wrapper: Save (owner assignment, ID clearing),
Update (owner, admin, denied, ownership change, not found),
Delete (delegation with permission checks)
Expand mockedPlaylistRepo with Get, Delete, Tracks, GetWithTracks, and
rest.Persistable methods. Add mockedPlaylistTrackRepo for track
operation verification.
* fix: add authorization check to playlist Update method
Added ownership verification to the Subsonic Update endpoint in the
playlist service layer. The authorization check was present in the old
repository code but was not carried over during the refactoring to the
service layer, allowing any authenticated user to modify playlists they
don't own via the Subsonic API. Also added corresponding tests for the
Update method's permission logic.
* refactor: improve playlist permission checks and error handling, add e2e tests
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: rename core.Playlists to playlists package and update references
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: rename playlists_internal_test.go to parse_m3u_test.go and update tests; add new parse_nsp.go and rest_adapter.go files
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: block track mutations on smart playlists in Create and Update
Create now rejects replacing tracks on smart playlists (pre-existing
gap). Update now uses checkTracksEditable instead of checkWritable
when track changes are requested, restoring the protection that was
removed from the repository layer during the refactoring. Metadata-only
updates on smart playlists remain allowed.
* test: add smart playlist protection tests to ensure readonly behavior and mutation restrictions
* refactor: optimize track removal and renumbering in playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: implement track reordering in playlists with SQL updates
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: wrap track deletion and reordering in transactions for consistency
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: remove unused getTracks method from playlistTrackRepository
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: optimize playlist track renumbering with CTE-based UPDATE
Replace the DELETE + re-INSERT renumbering strategy with a two-step
UPDATE approach using a materialized CTE and ROW_NUMBER() window
function. The previous approach (SELECT all IDs, DELETE all tracks,
re-INSERT in chunks of 200) required 13 SQL operations for a 2000-track
playlist. The new approach uses just 2 UPDATEs: first negating all IDs
to clear the positive space, then assigning sequential positions via
UPDATE...FROM with a CTE. This avoids the UNIQUE constraint violations
that affected the original correlated subquery while reducing per-delete
request time from ~110ms to ~12ms on a 2000-track playlist.
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: rename New function to NewPlaylists for clarity
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: update mock playlist repository and tests for consistency
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-21 15:57:13 -09:00
|
|
|
playlists playlistsvc.Playlists
|
2025-11-14 18:15:43 -09:00
|
|
|
scanner model.Scanner
|
2025-04-08 17:11:09 -08:00
|
|
|
broker events.Broker
|
|
|
|
|
scrobbler scrobbler.PlayTracker
|
|
|
|
|
share core.Share
|
|
|
|
|
playback playback.PlaybackServer
|
2025-06-27 18:13:57 -08:00
|
|
|
metrics metrics.Metrics
|
2020-01-11 08:37:05 -09:00
|
|
|
}
|
|
|
|
|
|
2022-12-25 12:07:28 -09:00
|
|
|
func New(ds model.DataStore, artwork artwork.Artwork, streamer core.MediaStreamer, archiver core.Archiver,
|
2025-11-14 18:15:43 -09:00
|
|
|
players core.Players, provider external.Provider, scanner model.Scanner, broker events.Broker,
|
refactor: move playlist business logic from repositories to service layer (#5027)
* refactor: move playlist business logic from repositories to core.Playlists service
Move authorization, permission checks, and orchestration logic from
playlist repositories to the core.Playlists service, following the
existing pattern used by core.Share and core.Library.
Changes:
- Expand core.Playlists interface with read, mutation, track management,
and REST adapter methods
- Add playlistRepositoryWrapper for REST Save/Update/Delete with
permission checks (follows Share/Library pattern)
- Simplify persistence/playlist_repository.go: remove isWritable(),
auth checks from Delete()/Put()/updatePlaylist()
- Simplify persistence/playlist_track_repository.go: remove
isTracksEditable() and permission checks from Add/Delete/Reorder
- Update Subsonic API handlers to route through service
- Update Native API handlers to accept core.Playlists instead of
model.DataStore
* test: add coverage for playlist service methods and REST wrapper
Add 30 new tests covering the service methods added during the playlist
refactoring:
- Delete: owner, admin, denied, not found
- Create: new playlist, replace tracks, admin bypass, denied, not found
- AddTracks: owner, admin, denied, smart playlist, not found
- RemoveTracks: owner, smart playlist denied, non-owner denied
- ReorderTrack: owner, smart playlist denied
- NewRepository wrapper: Save (owner assignment, ID clearing),
Update (owner, admin, denied, ownership change, not found),
Delete (delegation with permission checks)
Expand mockedPlaylistRepo with Get, Delete, Tracks, GetWithTracks, and
rest.Persistable methods. Add mockedPlaylistTrackRepo for track
operation verification.
* fix: add authorization check to playlist Update method
Added ownership verification to the Subsonic Update endpoint in the
playlist service layer. The authorization check was present in the old
repository code but was not carried over during the refactoring to the
service layer, allowing any authenticated user to modify playlists they
don't own via the Subsonic API. Also added corresponding tests for the
Update method's permission logic.
* refactor: improve playlist permission checks and error handling, add e2e tests
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: rename core.Playlists to playlists package and update references
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: rename playlists_internal_test.go to parse_m3u_test.go and update tests; add new parse_nsp.go and rest_adapter.go files
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: block track mutations on smart playlists in Create and Update
Create now rejects replacing tracks on smart playlists (pre-existing
gap). Update now uses checkTracksEditable instead of checkWritable
when track changes are requested, restoring the protection that was
removed from the repository layer during the refactoring. Metadata-only
updates on smart playlists remain allowed.
* test: add smart playlist protection tests to ensure readonly behavior and mutation restrictions
* refactor: optimize track removal and renumbering in playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: implement track reordering in playlists with SQL updates
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: wrap track deletion and reordering in transactions for consistency
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: remove unused getTracks method from playlistTrackRepository
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: optimize playlist track renumbering with CTE-based UPDATE
Replace the DELETE + re-INSERT renumbering strategy with a two-step
UPDATE approach using a materialized CTE and ROW_NUMBER() window
function. The previous approach (SELECT all IDs, DELETE all tracks,
re-INSERT in chunks of 200) required 13 SQL operations for a 2000-track
playlist. The new approach uses just 2 UPDATEs: first negating all IDs
to clear the positive space, then assigning sequential positions via
UPDATE...FROM with a CTE. This avoids the UNIQUE constraint violations
that affected the original correlated subquery while reducing per-delete
request time from ~110ms to ~12ms on a 2000-track playlist.
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: rename New function to NewPlaylists for clarity
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: update mock playlist repository and tests for consistency
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-21 15:57:13 -09:00
|
|
|
playlists playlistsvc.Playlists, scrobbler scrobbler.PlayTracker, share core.Share, playback playback.PlaybackServer,
|
2025-06-27 18:13:57 -08:00
|
|
|
metrics metrics.Metrics,
|
2024-05-08 18:21:38 -08:00
|
|
|
) *Router {
|
2020-10-27 09:52:01 -08:00
|
|
|
r := &Router{
|
2025-04-08 17:11:09 -08:00
|
|
|
ds: ds,
|
|
|
|
|
artwork: artwork,
|
|
|
|
|
streamer: streamer,
|
|
|
|
|
archiver: archiver,
|
|
|
|
|
players: players,
|
|
|
|
|
provider: provider,
|
|
|
|
|
playlists: playlists,
|
|
|
|
|
scanner: scanner,
|
|
|
|
|
broker: broker,
|
|
|
|
|
scrobbler: scrobbler,
|
|
|
|
|
share: share,
|
|
|
|
|
playback: playback,
|
2025-06-27 18:13:57 -08:00
|
|
|
metrics: metrics,
|
2020-10-27 09:52:01 -08:00
|
|
|
}
|
2021-06-13 08:46:36 -08:00
|
|
|
r.Handler = r.routes()
|
2020-01-11 09:21:43 -09:00
|
|
|
return r
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (api *Router) routes() http.Handler {
|
2020-01-07 10:56:26 -09:00
|
|
|
r := chi.NewRouter()
|
2025-06-27 18:13:57 -08:00
|
|
|
|
|
|
|
|
if conf.Server.Prometheus.Enabled {
|
|
|
|
|
r.Use(recordStats(api.metrics))
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-27 11:10:46 -09:00
|
|
|
r.Use(postFormToQueryParams)
|
2020-01-07 10:56:26 -09:00
|
|
|
|
2024-10-21 12:31:56 -08:00
|
|
|
// Public
|
|
|
|
|
h(r, "getOpenSubsonicExtensions", api.GetOpenSubsonicExtensions)
|
|
|
|
|
|
|
|
|
|
// Protected
|
2023-01-15 11:11:37 -09:00
|
|
|
r.Group(func(r chi.Router) {
|
2024-10-21 12:31:56 -08:00
|
|
|
r.Use(checkRequiredParameters)
|
|
|
|
|
r.Use(authenticate(api.ds))
|
|
|
|
|
r.Use(server.UpdateLastAccessMiddleware(api.ds))
|
|
|
|
|
|
|
|
|
|
// Subsonic endpoints, grouped by controller
|
2023-01-24 14:31:40 -09:00
|
|
|
r.Group(func(r chi.Router) {
|
2024-10-21 12:31:56 -08:00
|
|
|
r.Use(getPlayer(api.players))
|
|
|
|
|
h(r, "ping", api.Ping)
|
|
|
|
|
h(r, "getLicense", api.GetLicense)
|
2023-01-24 14:31:40 -09:00
|
|
|
})
|
Jukebox mode (#2289)
* Adding cache directory to ignore-list
* Adding jukebox-related config options
* Adding DevEnableJukebox config option pls. dummy server
* Adding types and routers
* Now without panic
* First draft on parsing the action
* Some cleanups
* Adding playback server
* Verify audio device configuration
* Adding debug-build target to have full symbol support
* Adding beep sound library pls some example code. Not working yet
* Play a fixed mp3 on any interface access for testing purposes
* Put action code into separate file, adding stringer, more debug output, prepare structs, validation
* Put action parameter parser code where it belongs
* Have a single Action transporting all information
* User fmt.Errorf for error-generation
* Adding wide playback interface
* Use action map for parsing, stringer instead switch stmt.
* Use but only one switch case and direct dispatch, refactoring
* Add error handling and pushing to client
* send decent errormessage, no internal server error
* Adding playback devices slice and load it from config
* Combine config-verification and structure init
* Return user-specific device
* Separate playback server from device
* Use dataStore to retrieve mediafile by id
* WIP: Playlist and start/stop handling. Doing start/stop the hard way as of now
* WIP: set, start and stop work on one single song. More to come
* Dont need to wait for the end
* Merge jukebox_action.go into jukebox.go
* Remove getParameterAsInt64(). Use existing requiredParamInt() instead
* Dont need to call newFailure() explicitly
* Remove int64, use int instead.
* Add and set action now accept multiple ids
* Kickout copy of childFromMediaFile(). It is not needed here.
* Refactoring devices and playbackServer
* Turn (internal) playback.DeviceStatus into subsonic JukeboxStatus when rendering output. Indexes int64 -> int
* Now we have a position and playing status
* Switching gain to float32, xs:float is defined as 32 bit. Fixing nasty copy/pointer bug
* Now with volume control
* Start working the queue
* Remove user from device interface
* Rename function GetDevice -> GetDeviceForUser to make intention clearer
* Have a nice stringer for the queue
* User Prepared boolean for now to allow pause/unpause
* Skipping works, but without offsets
* Make ChildFromMediaFile public to be used in jukebox get() implementation
* Return position in seconds and implement offset-skip in seconds
* Default offset to 0
* Adding a simple setGain implementation
* Prepare for transcoding AAC
* WIP: transcode to WAV to use beeps wav decoder. Not done yet.
* WIP: out of sheer desparation: convert to MP3 (which works) rather than WAV to troubleshoot issue.
* Use FLAC as intermediate format to play Apple AAC
* A bit of cleanup
* Catching the end-of-stream event for further reactions
* Have a trackSwitching goroutine waiting on channel when track ends
* Move decoder code into own file. Restructure code a bit
* Now with going on to play the next song in the playlist
* Adding shuffle feature
* Implementing remove action
* Cleanup code
* Remove templates for ffmpeg mp3 generation. Not needed anymore.
* Adding some documentation
* Check whether offset into track is in range. Fixing potential remove track bug. Documentation
* Make golangci-lint happy: handling return values
* Adding test suite and example dummy for playback package
* Adding some basic queue tests
* Only use Jukebox.Enabled config option
* Adding stream closing handling
* Pass context.Context to all PlaybackDevice methods
* Remove unneeded function
* Correct spelling
* Reduce visibility of ChildFromMediaFile
* Decomplicate action-parsing
* Adding simple tempfile-based AAC->FLAC transcoding. No parallel reading and writing yet.
* Try to optimize pipe-writing, tempfile-handling and reading. Not done yet.
* Do a synchronous copy of the tempfile. Racecondition detected
* More debugging statements and fixing the play/pause bug. More work needed
* Start the trackSwitcher() with each device once. Return JSON position even if its 0. More debug-output
* Moving all track-handling code into own module
* Fix typo. Do not pass ctx around when not applicable
* WIP: More refactoring, debugging output
* Fix nil pointer
* Repairing MP3 playback by pinning indirect dependencies: hajimehoshi/go-mp3 and hajimehoshi/oto
* Do not forget to cleanup after a skip action
* Make resync with master easy
* Adding missing mocks
* Adding missing error-handling found by linter
* Updating github.com/hajimehoshi/oto
* Removing duplicate function
* Move BEEP-related code into own package
* Juggle beep-related code around as preparation for interface access
* More refactoring for interface separation
* Gather CloseDevice() behind Track interface.
* Adding skeleton, draft audio-interface using mpv.io
* Adding majority of interface commands using messages to mpv socket.
* Adding end-of-stream handling
* MPV: start/stop are working
* postition is given in float in mpv
* Unify Close() and CloseDevice(). Using temp filename for controlling socket
* Wait until control-socket shows up. Cleanup socket in Close()
* Use canceable command. Rename to Executor
* Skipping tracks works now
* Now with actually setting the position
* Fix regain
* Add missing error-handling found by linter
* Adding retry mode on time-pos property getter
* Remove unneeded code on queue
* Putting build-tag beep onto beep files
* Remove deprecated call to rand.Seed()
"As of Go 1.20 there is no reason to call Seed with a random value. Programs that call Seed with a known value to get a specific sequence of results should use New(NewSource(seed)) to obtain a local random generator."
* Using int32 to conform to Subsonic API spec
* Fix merge error
* Minor style changes
* Get username from context
---------
Co-authored-by: Deluan <deluan@navidrome.org>
2023-09-10 07:25:22 -08:00
|
|
|
r.Group(func(r chi.Router) {
|
2024-10-21 12:31:56 -08:00
|
|
|
r.Use(getPlayer(api.players))
|
|
|
|
|
h(r, "getMusicFolders", api.GetMusicFolders)
|
|
|
|
|
h(r, "getIndexes", api.GetIndexes)
|
|
|
|
|
h(r, "getArtists", api.GetArtists)
|
|
|
|
|
h(r, "getGenres", api.GetGenres)
|
|
|
|
|
h(r, "getMusicDirectory", api.GetMusicDirectory)
|
|
|
|
|
h(r, "getArtist", api.GetArtist)
|
|
|
|
|
h(r, "getAlbum", api.GetAlbum)
|
|
|
|
|
h(r, "getSong", api.GetSong)
|
|
|
|
|
h(r, "getAlbumInfo", api.GetAlbumInfo)
|
|
|
|
|
h(r, "getAlbumInfo2", api.GetAlbumInfo)
|
|
|
|
|
h(r, "getArtistInfo", api.GetArtistInfo)
|
|
|
|
|
h(r, "getArtistInfo2", api.GetArtistInfo2)
|
|
|
|
|
h(r, "getTopSongs", api.GetTopSongs)
|
|
|
|
|
h(r, "getSimilarSongs", api.GetSimilarSongs)
|
|
|
|
|
h(r, "getSimilarSongs2", api.GetSimilarSongs2)
|
Jukebox mode (#2289)
* Adding cache directory to ignore-list
* Adding jukebox-related config options
* Adding DevEnableJukebox config option pls. dummy server
* Adding types and routers
* Now without panic
* First draft on parsing the action
* Some cleanups
* Adding playback server
* Verify audio device configuration
* Adding debug-build target to have full symbol support
* Adding beep sound library pls some example code. Not working yet
* Play a fixed mp3 on any interface access for testing purposes
* Put action code into separate file, adding stringer, more debug output, prepare structs, validation
* Put action parameter parser code where it belongs
* Have a single Action transporting all information
* User fmt.Errorf for error-generation
* Adding wide playback interface
* Use action map for parsing, stringer instead switch stmt.
* Use but only one switch case and direct dispatch, refactoring
* Add error handling and pushing to client
* send decent errormessage, no internal server error
* Adding playback devices slice and load it from config
* Combine config-verification and structure init
* Return user-specific device
* Separate playback server from device
* Use dataStore to retrieve mediafile by id
* WIP: Playlist and start/stop handling. Doing start/stop the hard way as of now
* WIP: set, start and stop work on one single song. More to come
* Dont need to wait for the end
* Merge jukebox_action.go into jukebox.go
* Remove getParameterAsInt64(). Use existing requiredParamInt() instead
* Dont need to call newFailure() explicitly
* Remove int64, use int instead.
* Add and set action now accept multiple ids
* Kickout copy of childFromMediaFile(). It is not needed here.
* Refactoring devices and playbackServer
* Turn (internal) playback.DeviceStatus into subsonic JukeboxStatus when rendering output. Indexes int64 -> int
* Now we have a position and playing status
* Switching gain to float32, xs:float is defined as 32 bit. Fixing nasty copy/pointer bug
* Now with volume control
* Start working the queue
* Remove user from device interface
* Rename function GetDevice -> GetDeviceForUser to make intention clearer
* Have a nice stringer for the queue
* User Prepared boolean for now to allow pause/unpause
* Skipping works, but without offsets
* Make ChildFromMediaFile public to be used in jukebox get() implementation
* Return position in seconds and implement offset-skip in seconds
* Default offset to 0
* Adding a simple setGain implementation
* Prepare for transcoding AAC
* WIP: transcode to WAV to use beeps wav decoder. Not done yet.
* WIP: out of sheer desparation: convert to MP3 (which works) rather than WAV to troubleshoot issue.
* Use FLAC as intermediate format to play Apple AAC
* A bit of cleanup
* Catching the end-of-stream event for further reactions
* Have a trackSwitching goroutine waiting on channel when track ends
* Move decoder code into own file. Restructure code a bit
* Now with going on to play the next song in the playlist
* Adding shuffle feature
* Implementing remove action
* Cleanup code
* Remove templates for ffmpeg mp3 generation. Not needed anymore.
* Adding some documentation
* Check whether offset into track is in range. Fixing potential remove track bug. Documentation
* Make golangci-lint happy: handling return values
* Adding test suite and example dummy for playback package
* Adding some basic queue tests
* Only use Jukebox.Enabled config option
* Adding stream closing handling
* Pass context.Context to all PlaybackDevice methods
* Remove unneeded function
* Correct spelling
* Reduce visibility of ChildFromMediaFile
* Decomplicate action-parsing
* Adding simple tempfile-based AAC->FLAC transcoding. No parallel reading and writing yet.
* Try to optimize pipe-writing, tempfile-handling and reading. Not done yet.
* Do a synchronous copy of the tempfile. Racecondition detected
* More debugging statements and fixing the play/pause bug. More work needed
* Start the trackSwitcher() with each device once. Return JSON position even if its 0. More debug-output
* Moving all track-handling code into own module
* Fix typo. Do not pass ctx around when not applicable
* WIP: More refactoring, debugging output
* Fix nil pointer
* Repairing MP3 playback by pinning indirect dependencies: hajimehoshi/go-mp3 and hajimehoshi/oto
* Do not forget to cleanup after a skip action
* Make resync with master easy
* Adding missing mocks
* Adding missing error-handling found by linter
* Updating github.com/hajimehoshi/oto
* Removing duplicate function
* Move BEEP-related code into own package
* Juggle beep-related code around as preparation for interface access
* More refactoring for interface separation
* Gather CloseDevice() behind Track interface.
* Adding skeleton, draft audio-interface using mpv.io
* Adding majority of interface commands using messages to mpv socket.
* Adding end-of-stream handling
* MPV: start/stop are working
* postition is given in float in mpv
* Unify Close() and CloseDevice(). Using temp filename for controlling socket
* Wait until control-socket shows up. Cleanup socket in Close()
* Use canceable command. Rename to Executor
* Skipping tracks works now
* Now with actually setting the position
* Fix regain
* Add missing error-handling found by linter
* Adding retry mode on time-pos property getter
* Remove unneeded code on queue
* Putting build-tag beep onto beep files
* Remove deprecated call to rand.Seed()
"As of Go 1.20 there is no reason to call Seed with a random value. Programs that call Seed with a known value to get a specific sequence of results should use New(NewSource(seed)) to obtain a local random generator."
* Using int32 to conform to Subsonic API spec
* Fix merge error
* Minor style changes
* Get username from context
---------
Co-authored-by: Deluan <deluan@navidrome.org>
2023-09-10 07:25:22 -08:00
|
|
|
})
|
2024-10-21 12:31:56 -08:00
|
|
|
r.Group(func(r chi.Router) {
|
|
|
|
|
r.Use(getPlayer(api.players))
|
|
|
|
|
hr(r, "getAlbumList", api.GetAlbumList)
|
|
|
|
|
hr(r, "getAlbumList2", api.GetAlbumList2)
|
|
|
|
|
h(r, "getStarred", api.GetStarred)
|
|
|
|
|
h(r, "getStarred2", api.GetStarred2)
|
2025-12-11 11:44:21 -09:00
|
|
|
h(r, "getNowPlaying", api.GetNowPlaying)
|
2024-10-21 12:31:56 -08:00
|
|
|
h(r, "getRandomSongs", api.GetRandomSongs)
|
|
|
|
|
h(r, "getSongsByGenre", api.GetSongsByGenre)
|
|
|
|
|
})
|
|
|
|
|
r.Group(func(r chi.Router) {
|
|
|
|
|
r.Use(getPlayer(api.players))
|
|
|
|
|
h(r, "setRating", api.SetRating)
|
|
|
|
|
h(r, "star", api.Star)
|
|
|
|
|
h(r, "unstar", api.Unstar)
|
|
|
|
|
h(r, "scrobble", api.Scrobble)
|
|
|
|
|
})
|
|
|
|
|
r.Group(func(r chi.Router) {
|
|
|
|
|
r.Use(getPlayer(api.players))
|
|
|
|
|
h(r, "getPlaylists", api.GetPlaylists)
|
|
|
|
|
h(r, "getPlaylist", api.GetPlaylist)
|
|
|
|
|
h(r, "createPlaylist", api.CreatePlaylist)
|
|
|
|
|
h(r, "deletePlaylist", api.DeletePlaylist)
|
|
|
|
|
h(r, "updatePlaylist", api.UpdatePlaylist)
|
|
|
|
|
})
|
|
|
|
|
r.Group(func(r chi.Router) {
|
|
|
|
|
r.Use(getPlayer(api.players))
|
|
|
|
|
h(r, "getBookmarks", api.GetBookmarks)
|
|
|
|
|
h(r, "createBookmark", api.CreateBookmark)
|
|
|
|
|
h(r, "deleteBookmark", api.DeleteBookmark)
|
|
|
|
|
h(r, "getPlayQueue", api.GetPlayQueue)
|
2025-11-09 08:52:05 -09:00
|
|
|
h(r, "getPlayQueueByIndex", api.GetPlayQueueByIndex)
|
2024-10-21 12:31:56 -08:00
|
|
|
h(r, "savePlayQueue", api.SavePlayQueue)
|
2025-11-09 08:52:05 -09:00
|
|
|
h(r, "savePlayQueueByIndex", api.SavePlayQueueByIndex)
|
2024-10-21 12:31:56 -08:00
|
|
|
})
|
|
|
|
|
r.Group(func(r chi.Router) {
|
|
|
|
|
r.Use(getPlayer(api.players))
|
|
|
|
|
h(r, "search2", api.Search2)
|
|
|
|
|
h(r, "search3", api.Search3)
|
|
|
|
|
})
|
|
|
|
|
r.Group(func(r chi.Router) {
|
|
|
|
|
r.Use(getPlayer(api.players))
|
|
|
|
|
h(r, "getUser", api.GetUser)
|
|
|
|
|
h(r, "getUsers", api.GetUsers)
|
|
|
|
|
})
|
|
|
|
|
r.Group(func(r chi.Router) {
|
|
|
|
|
r.Use(getPlayer(api.players))
|
|
|
|
|
h(r, "getScanStatus", api.GetScanStatus)
|
|
|
|
|
h(r, "startScan", api.StartScan)
|
|
|
|
|
})
|
|
|
|
|
r.Group(func(r chi.Router) {
|
|
|
|
|
r.Use(getPlayer(api.players))
|
|
|
|
|
hr(r, "getAvatar", api.GetAvatar)
|
|
|
|
|
h(r, "getLyrics", api.GetLyrics)
|
|
|
|
|
h(r, "getLyricsBySongId", api.GetLyricsBySongId)
|
|
|
|
|
hr(r, "stream", api.Stream)
|
|
|
|
|
hr(r, "download", api.Download)
|
|
|
|
|
})
|
|
|
|
|
r.Group(func(r chi.Router) {
|
|
|
|
|
// configure request throttling
|
|
|
|
|
if conf.Server.DevArtworkMaxRequests > 0 {
|
|
|
|
|
log.Debug("Throttling Subsonic getCoverArt endpoint", "maxRequests", conf.Server.DevArtworkMaxRequests,
|
|
|
|
|
"backlogLimit", conf.Server.DevArtworkThrottleBacklogLimit, "backlogTimeout",
|
|
|
|
|
conf.Server.DevArtworkThrottleBacklogTimeout)
|
|
|
|
|
r.Use(middleware.ThrottleBacklog(conf.Server.DevArtworkMaxRequests, conf.Server.DevArtworkThrottleBacklogLimit,
|
|
|
|
|
conf.Server.DevArtworkThrottleBacklogTimeout))
|
|
|
|
|
}
|
|
|
|
|
hr(r, "getCoverArt", api.GetCoverArt)
|
|
|
|
|
})
|
|
|
|
|
r.Group(func(r chi.Router) {
|
|
|
|
|
r.Use(getPlayer(api.players))
|
|
|
|
|
h(r, "createInternetRadioStation", api.CreateInternetRadio)
|
|
|
|
|
h(r, "deleteInternetRadioStation", api.DeleteInternetRadio)
|
|
|
|
|
h(r, "getInternetRadioStations", api.GetInternetRadios)
|
|
|
|
|
h(r, "updateInternetRadioStation", api.UpdateInternetRadio)
|
|
|
|
|
})
|
|
|
|
|
if conf.Server.EnableSharing {
|
|
|
|
|
r.Group(func(r chi.Router) {
|
|
|
|
|
r.Use(getPlayer(api.players))
|
|
|
|
|
h(r, "getShares", api.GetShares)
|
|
|
|
|
h(r, "createShare", api.CreateShare)
|
|
|
|
|
h(r, "updateShare", api.UpdateShare)
|
|
|
|
|
h(r, "deleteShare", api.DeleteShare)
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
h501(r, "getShares", "createShare", "updateShare", "deleteShare")
|
|
|
|
|
}
|
Jukebox mode (#2289)
* Adding cache directory to ignore-list
* Adding jukebox-related config options
* Adding DevEnableJukebox config option pls. dummy server
* Adding types and routers
* Now without panic
* First draft on parsing the action
* Some cleanups
* Adding playback server
* Verify audio device configuration
* Adding debug-build target to have full symbol support
* Adding beep sound library pls some example code. Not working yet
* Play a fixed mp3 on any interface access for testing purposes
* Put action code into separate file, adding stringer, more debug output, prepare structs, validation
* Put action parameter parser code where it belongs
* Have a single Action transporting all information
* User fmt.Errorf for error-generation
* Adding wide playback interface
* Use action map for parsing, stringer instead switch stmt.
* Use but only one switch case and direct dispatch, refactoring
* Add error handling and pushing to client
* send decent errormessage, no internal server error
* Adding playback devices slice and load it from config
* Combine config-verification and structure init
* Return user-specific device
* Separate playback server from device
* Use dataStore to retrieve mediafile by id
* WIP: Playlist and start/stop handling. Doing start/stop the hard way as of now
* WIP: set, start and stop work on one single song. More to come
* Dont need to wait for the end
* Merge jukebox_action.go into jukebox.go
* Remove getParameterAsInt64(). Use existing requiredParamInt() instead
* Dont need to call newFailure() explicitly
* Remove int64, use int instead.
* Add and set action now accept multiple ids
* Kickout copy of childFromMediaFile(). It is not needed here.
* Refactoring devices and playbackServer
* Turn (internal) playback.DeviceStatus into subsonic JukeboxStatus when rendering output. Indexes int64 -> int
* Now we have a position and playing status
* Switching gain to float32, xs:float is defined as 32 bit. Fixing nasty copy/pointer bug
* Now with volume control
* Start working the queue
* Remove user from device interface
* Rename function GetDevice -> GetDeviceForUser to make intention clearer
* Have a nice stringer for the queue
* User Prepared boolean for now to allow pause/unpause
* Skipping works, but without offsets
* Make ChildFromMediaFile public to be used in jukebox get() implementation
* Return position in seconds and implement offset-skip in seconds
* Default offset to 0
* Adding a simple setGain implementation
* Prepare for transcoding AAC
* WIP: transcode to WAV to use beeps wav decoder. Not done yet.
* WIP: out of sheer desparation: convert to MP3 (which works) rather than WAV to troubleshoot issue.
* Use FLAC as intermediate format to play Apple AAC
* A bit of cleanup
* Catching the end-of-stream event for further reactions
* Have a trackSwitching goroutine waiting on channel when track ends
* Move decoder code into own file. Restructure code a bit
* Now with going on to play the next song in the playlist
* Adding shuffle feature
* Implementing remove action
* Cleanup code
* Remove templates for ffmpeg mp3 generation. Not needed anymore.
* Adding some documentation
* Check whether offset into track is in range. Fixing potential remove track bug. Documentation
* Make golangci-lint happy: handling return values
* Adding test suite and example dummy for playback package
* Adding some basic queue tests
* Only use Jukebox.Enabled config option
* Adding stream closing handling
* Pass context.Context to all PlaybackDevice methods
* Remove unneeded function
* Correct spelling
* Reduce visibility of ChildFromMediaFile
* Decomplicate action-parsing
* Adding simple tempfile-based AAC->FLAC transcoding. No parallel reading and writing yet.
* Try to optimize pipe-writing, tempfile-handling and reading. Not done yet.
* Do a synchronous copy of the tempfile. Racecondition detected
* More debugging statements and fixing the play/pause bug. More work needed
* Start the trackSwitcher() with each device once. Return JSON position even if its 0. More debug-output
* Moving all track-handling code into own module
* Fix typo. Do not pass ctx around when not applicable
* WIP: More refactoring, debugging output
* Fix nil pointer
* Repairing MP3 playback by pinning indirect dependencies: hajimehoshi/go-mp3 and hajimehoshi/oto
* Do not forget to cleanup after a skip action
* Make resync with master easy
* Adding missing mocks
* Adding missing error-handling found by linter
* Updating github.com/hajimehoshi/oto
* Removing duplicate function
* Move BEEP-related code into own package
* Juggle beep-related code around as preparation for interface access
* More refactoring for interface separation
* Gather CloseDevice() behind Track interface.
* Adding skeleton, draft audio-interface using mpv.io
* Adding majority of interface commands using messages to mpv socket.
* Adding end-of-stream handling
* MPV: start/stop are working
* postition is given in float in mpv
* Unify Close() and CloseDevice(). Using temp filename for controlling socket
* Wait until control-socket shows up. Cleanup socket in Close()
* Use canceable command. Rename to Executor
* Skipping tracks works now
* Now with actually setting the position
* Fix regain
* Add missing error-handling found by linter
* Adding retry mode on time-pos property getter
* Remove unneeded code on queue
* Putting build-tag beep onto beep files
* Remove deprecated call to rand.Seed()
"As of Go 1.20 there is no reason to call Seed with a random value. Programs that call Seed with a known value to get a specific sequence of results should use New(NewSource(seed)) to obtain a local random generator."
* Using int32 to conform to Subsonic API spec
* Fix merge error
* Minor style changes
* Get username from context
---------
Co-authored-by: Deluan <deluan@navidrome.org>
2023-09-10 07:25:22 -08:00
|
|
|
|
2024-10-21 12:31:56 -08:00
|
|
|
if conf.Server.Jukebox.Enabled {
|
|
|
|
|
r.Group(func(r chi.Router) {
|
|
|
|
|
r.Use(getPlayer(api.players))
|
|
|
|
|
h(r, "jukeboxControl", api.JukeboxControl)
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
h501(r, "jukeboxControl")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Not Implemented (yet?)
|
|
|
|
|
h501(r, "getPodcasts", "getNewestPodcasts", "refreshPodcasts", "createPodcastChannel", "deletePodcastChannel",
|
|
|
|
|
"deletePodcastEpisode", "downloadPodcastEpisode")
|
|
|
|
|
h501(r, "createUser", "updateUser", "deleteUser", "changePassword")
|
2021-02-09 17:25:14 -09:00
|
|
|
|
2024-10-21 12:31:56 -08:00
|
|
|
// Deprecated/Won't implement/Out of scope endpoints
|
|
|
|
|
h410(r, "search")
|
|
|
|
|
h410(r, "getChatMessages", "addChatMessage")
|
|
|
|
|
h410(r, "getVideos", "getVideoInfo", "getCaptions", "hls")
|
|
|
|
|
})
|
2020-01-07 10:56:26 -09:00
|
|
|
return r
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-26 09:13:05 -09:00
|
|
|
// Add a Subsonic handler
|
|
|
|
|
func h(r chi.Router, path string, f handler) {
|
|
|
|
|
hr(r, path, func(_ http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
|
|
|
|
|
return f(r)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-27 18:13:57 -08:00
|
|
|
// Add a Subsonic handler that requires an http.ResponseWriter (ex: stream, getCoverArt...)
|
2022-11-21 08:57:56 -09:00
|
|
|
func hr(r chi.Router, path string, f handlerRaw) {
|
2020-01-08 08:52:57 -09:00
|
|
|
handle := func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
res, err := f(w, r)
|
2020-01-07 10:56:26 -09:00
|
|
|
if err != nil {
|
2020-10-27 11:23:29 -08:00
|
|
|
sendError(w, r, err)
|
2020-01-07 10:56:26 -09:00
|
|
|
return
|
|
|
|
|
}
|
2022-07-27 10:27:18 -08:00
|
|
|
if r.Context().Err() != nil {
|
2023-12-25 12:29:59 -09:00
|
|
|
if log.IsGreaterOrEqualTo(log.LevelDebug) {
|
2022-12-14 06:52:46 -09:00
|
|
|
log.Warn(r.Context(), "Request was interrupted", "endpoint", r.URL.Path, r.Context().Err())
|
2022-11-03 08:38:05 -08:00
|
|
|
}
|
2022-07-27 10:27:18 -08:00
|
|
|
return
|
|
|
|
|
}
|
2020-01-07 10:56:26 -09:00
|
|
|
if res != nil {
|
2020-10-27 11:23:29 -08:00
|
|
|
sendResponse(w, r, res)
|
2020-01-07 10:56:26 -09:00
|
|
|
}
|
|
|
|
|
}
|
2022-11-26 09:13:05 -09:00
|
|
|
addHandler(r, path, handle)
|
2022-11-21 08:57:56 -09:00
|
|
|
}
|
|
|
|
|
|
2022-07-26 09:18:08 -08:00
|
|
|
// Add a handler that returns 501 - Not implemented. Used to signal that an endpoint is not implemented yet
|
2022-11-26 09:13:05 -09:00
|
|
|
func h501(r chi.Router, paths ...string) {
|
2021-02-09 17:25:14 -09:00
|
|
|
for _, path := range paths {
|
|
|
|
|
handle := func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Header().Add("Cache-Control", "no-cache")
|
2024-02-17 06:39:29 -09:00
|
|
|
w.WriteHeader(http.StatusNotImplemented)
|
2021-02-09 17:25:14 -09:00
|
|
|
_, _ = w.Write([]byte("This endpoint is not implemented, but may be in future releases"))
|
|
|
|
|
}
|
2022-11-26 09:13:05 -09:00
|
|
|
addHandler(r, path, handle)
|
2021-02-09 17:25:14 -09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-01-13 16:45:38 -09:00
|
|
|
// Add a handler that returns 410 - Gone. Used to signal that an endpoint will not be implemented
|
2021-02-09 17:25:14 -09:00
|
|
|
func h410(r chi.Router, paths ...string) {
|
|
|
|
|
for _, path := range paths {
|
|
|
|
|
handle := func(w http.ResponseWriter, r *http.Request) {
|
2024-02-17 06:39:29 -09:00
|
|
|
w.WriteHeader(http.StatusGone)
|
2021-02-09 17:25:14 -09:00
|
|
|
_, _ = w.Write([]byte("This endpoint will not be implemented"))
|
|
|
|
|
}
|
2022-11-26 09:13:05 -09:00
|
|
|
addHandler(r, path, handle)
|
2020-01-13 16:45:38 -09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-26 09:13:05 -09:00
|
|
|
func addHandler(r chi.Router, path string, handle func(w http.ResponseWriter, r *http.Request)) {
|
|
|
|
|
r.HandleFunc("/"+path, handle)
|
|
|
|
|
r.HandleFunc("/"+path+".view", handle)
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-21 12:32:37 -09:00
|
|
|
func mapToSubsonicError(err error) subError {
|
|
|
|
|
switch {
|
|
|
|
|
case errors.Is(err, errSubsonic): // do nothing
|
|
|
|
|
case errors.Is(err, req.ErrMissingParam):
|
|
|
|
|
err = newError(responses.ErrorMissingParameter, err.Error())
|
|
|
|
|
case errors.Is(err, req.ErrInvalidParam):
|
|
|
|
|
err = newError(responses.ErrorGeneric, err.Error())
|
|
|
|
|
case errors.Is(err, model.ErrNotFound):
|
|
|
|
|
err = newError(responses.ErrorDataNotFound, "data not found")
|
refactor: move playlist business logic from repositories to service layer (#5027)
* refactor: move playlist business logic from repositories to core.Playlists service
Move authorization, permission checks, and orchestration logic from
playlist repositories to the core.Playlists service, following the
existing pattern used by core.Share and core.Library.
Changes:
- Expand core.Playlists interface with read, mutation, track management,
and REST adapter methods
- Add playlistRepositoryWrapper for REST Save/Update/Delete with
permission checks (follows Share/Library pattern)
- Simplify persistence/playlist_repository.go: remove isWritable(),
auth checks from Delete()/Put()/updatePlaylist()
- Simplify persistence/playlist_track_repository.go: remove
isTracksEditable() and permission checks from Add/Delete/Reorder
- Update Subsonic API handlers to route through service
- Update Native API handlers to accept core.Playlists instead of
model.DataStore
* test: add coverage for playlist service methods and REST wrapper
Add 30 new tests covering the service methods added during the playlist
refactoring:
- Delete: owner, admin, denied, not found
- Create: new playlist, replace tracks, admin bypass, denied, not found
- AddTracks: owner, admin, denied, smart playlist, not found
- RemoveTracks: owner, smart playlist denied, non-owner denied
- ReorderTrack: owner, smart playlist denied
- NewRepository wrapper: Save (owner assignment, ID clearing),
Update (owner, admin, denied, ownership change, not found),
Delete (delegation with permission checks)
Expand mockedPlaylistRepo with Get, Delete, Tracks, GetWithTracks, and
rest.Persistable methods. Add mockedPlaylistTrackRepo for track
operation verification.
* fix: add authorization check to playlist Update method
Added ownership verification to the Subsonic Update endpoint in the
playlist service layer. The authorization check was present in the old
repository code but was not carried over during the refactoring to the
service layer, allowing any authenticated user to modify playlists they
don't own via the Subsonic API. Also added corresponding tests for the
Update method's permission logic.
* refactor: improve playlist permission checks and error handling, add e2e tests
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: rename core.Playlists to playlists package and update references
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: rename playlists_internal_test.go to parse_m3u_test.go and update tests; add new parse_nsp.go and rest_adapter.go files
Signed-off-by: Deluan <deluan@navidrome.org>
* fix: block track mutations on smart playlists in Create and Update
Create now rejects replacing tracks on smart playlists (pre-existing
gap). Update now uses checkTracksEditable instead of checkWritable
when track changes are requested, restoring the protection that was
removed from the repository layer during the refactoring. Metadata-only
updates on smart playlists remain allowed.
* test: add smart playlist protection tests to ensure readonly behavior and mutation restrictions
* refactor: optimize track removal and renumbering in playlists
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: implement track reordering in playlists with SQL updates
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: wrap track deletion and reordering in transactions for consistency
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: remove unused getTracks method from playlistTrackRepository
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: optimize playlist track renumbering with CTE-based UPDATE
Replace the DELETE + re-INSERT renumbering strategy with a two-step
UPDATE approach using a materialized CTE and ROW_NUMBER() window
function. The previous approach (SELECT all IDs, DELETE all tracks,
re-INSERT in chunks of 200) required 13 SQL operations for a 2000-track
playlist. The new approach uses just 2 UPDATEs: first negating all IDs
to clear the positive space, then assigning sequential positions via
UPDATE...FROM with a CTE. This avoids the UNIQUE constraint violations
that affected the original correlated subquery while reducing per-delete
request time from ~110ms to ~12ms on a 2000-track playlist.
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: rename New function to NewPlaylists for clarity
Signed-off-by: Deluan <deluan@navidrome.org>
* refactor: update mock playlist repository and tests for consistency
Signed-off-by: Deluan <deluan@navidrome.org>
---------
Signed-off-by: Deluan <deluan@navidrome.org>
2026-02-21 15:57:13 -09:00
|
|
|
case errors.Is(err, model.ErrNotAuthorized):
|
|
|
|
|
err = newError(responses.ErrorAuthorizationFail)
|
2023-12-21 12:32:37 -09:00
|
|
|
default:
|
|
|
|
|
err = newError(responses.ErrorGeneric, fmt.Sprintf("Internal Server Error: %s", err))
|
|
|
|
|
}
|
|
|
|
|
var subErr subError
|
|
|
|
|
errors.As(err, &subErr)
|
|
|
|
|
return subErr
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-27 11:23:29 -08:00
|
|
|
func sendError(w http.ResponseWriter, r *http.Request, err error) {
|
2023-12-21 12:32:37 -09:00
|
|
|
subErr := mapToSubsonicError(err)
|
2020-08-13 18:11:18 -08:00
|
|
|
response := newResponse()
|
2024-02-17 06:39:29 -09:00
|
|
|
response.Status = responses.StatusFailed
|
2024-09-01 10:41:21 -08:00
|
|
|
response.Error = &responses.Error{Code: subErr.code, Message: subErr.Error()}
|
2020-01-07 10:56:26 -09:00
|
|
|
|
2020-10-27 11:23:29 -08:00
|
|
|
sendResponse(w, r, response)
|
2020-01-07 10:56:26 -09:00
|
|
|
}
|
|
|
|
|
|
2020-10-27 11:23:29 -08:00
|
|
|
func sendResponse(w http.ResponseWriter, r *http.Request, payload *responses.Subsonic) {
|
2023-12-21 13:41:09 -09:00
|
|
|
p := req.Params(r)
|
|
|
|
|
f, _ := p.String("f")
|
2020-01-07 10:56:26 -09:00
|
|
|
var response []byte
|
2024-02-16 14:43:36 -09:00
|
|
|
var err error
|
2020-01-07 10:56:26 -09:00
|
|
|
switch f {
|
|
|
|
|
case "json":
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
wrapper := &responses.JsonWrapper{Subsonic: *payload}
|
2024-02-16 14:43:36 -09:00
|
|
|
response, err = json.Marshal(wrapper)
|
2020-01-07 10:56:26 -09:00
|
|
|
case "jsonp":
|
2023-12-21 13:41:09 -09:00
|
|
|
callback, _ := p.String("callback")
|
2026-02-08 06:33:46 -09:00
|
|
|
if !validJSIdentifier.MatchString(callback) {
|
|
|
|
|
log.Warn(r.Context(), "Invalid JSONP callback parameter", "callback", callback)
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
errResp := newResponse()
|
|
|
|
|
errResp.Status = responses.StatusFailed
|
|
|
|
|
errResp.Error = &responses.Error{Code: responses.ErrorGeneric, Message: "invalid callback parameter"}
|
|
|
|
|
response, _ = json.Marshal(responses.JsonWrapper{Subsonic: *errResp})
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
w.Header().Set("Content-Type", "application/javascript")
|
2020-01-07 10:56:26 -09:00
|
|
|
wrapper := &responses.JsonWrapper{Subsonic: *payload}
|
2024-02-16 14:43:36 -09:00
|
|
|
response, err = json.Marshal(wrapper)
|
2026-02-08 05:57:30 -09:00
|
|
|
response = fmt.Appendf(nil, "%s(%s)", callback, response)
|
2020-01-07 10:56:26 -09:00
|
|
|
default:
|
|
|
|
|
w.Header().Set("Content-Type", "application/xml")
|
2024-02-16 14:43:36 -09:00
|
|
|
response, err = xml.Marshal(payload)
|
|
|
|
|
}
|
2024-02-16 15:41:53 -09:00
|
|
|
// This should never happen, but if it does, we need to know
|
2024-02-16 14:43:36 -09:00
|
|
|
if err != nil {
|
|
|
|
|
log.Error(r.Context(), "Error marshalling response", "format", f, err)
|
2024-02-17 06:39:29 -09:00
|
|
|
sendError(w, r, err)
|
2024-02-16 15:41:53 -09:00
|
|
|
return
|
2020-01-07 10:56:26 -09:00
|
|
|
}
|
2025-06-30 07:54:02 -08:00
|
|
|
|
2024-02-17 06:39:29 -09:00
|
|
|
if payload.Status == responses.StatusOK {
|
2023-12-25 12:29:59 -09:00
|
|
|
if log.IsGreaterOrEqualTo(log.LevelTrace) {
|
2022-12-14 06:52:46 -09:00
|
|
|
log.Debug(r.Context(), "API: Successful response", "endpoint", r.URL.Path, "status", "OK", "body", string(response))
|
2020-02-01 16:07:15 -09:00
|
|
|
} else {
|
2022-12-14 06:52:46 -09:00
|
|
|
log.Debug(r.Context(), "API: Successful response", "endpoint", r.URL.Path, "status", "OK")
|
2020-02-01 16:07:15 -09:00
|
|
|
}
|
|
|
|
|
} else {
|
2022-12-14 06:52:46 -09:00
|
|
|
log.Warn(r.Context(), "API: Failed response", "endpoint", r.URL.Path, "error", payload.Error.Code, "message", payload.Error.Message)
|
2020-02-01 16:07:15 -09:00
|
|
|
}
|
2025-06-30 07:54:02 -08:00
|
|
|
|
|
|
|
|
statusPointer, ok := r.Context().Value(subsonicErrorPointer).(*int32)
|
|
|
|
|
|
|
|
|
|
if ok && statusPointer != nil {
|
|
|
|
|
if payload.Status == responses.StatusOK {
|
|
|
|
|
*statusPointer = 0
|
|
|
|
|
} else {
|
|
|
|
|
*statusPointer = payload.Error.Code
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 05:26:45 -09:00
|
|
|
if _, err := w.Write(response); err != nil { //nolint:gosec
|
2022-12-14 06:52:46 -09:00
|
|
|
log.Error(r, "Error sending response to client", "endpoint", r.URL.Path, "payload", string(response), err)
|
2020-04-26 08:35:26 -08:00
|
|
|
}
|
2020-01-07 10:56:26 -09:00
|
|
|
}
|