2020-01-20 00:21:44 +01:00
|
|
|
package subsonic
|
2020-01-07 20:56:26 +01:00
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"encoding/xml"
|
2022-10-01 00:54:25 +02:00
|
|
|
"errors"
|
2020-01-07 20:56:26 +01:00
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
|
2021-05-11 23:21:18 +02:00
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
"github.com/go-chi/chi/v5/middleware"
|
2023-01-14 00:10:32 +01:00
|
|
|
"github.com/navidrome/navidrome/conf"
|
2020-07-10 19:11:02 +02:00
|
|
|
"github.com/navidrome/navidrome/core"
|
2022-12-25 22:07:28 +01:00
|
|
|
"github.com/navidrome/navidrome/core/artwork"
|
2024-05-09 04:21:38 +02:00
|
|
|
"github.com/navidrome/navidrome/core/playback"
|
2021-06-20 02:56:56 +02:00
|
|
|
"github.com/navidrome/navidrome/core/scrobbler"
|
2020-02-02 02:07:15 +01:00
|
|
|
"github.com/navidrome/navidrome/log"
|
2020-07-31 19:07:39 +02:00
|
|
|
"github.com/navidrome/navidrome/model"
|
2020-10-27 23:19:56 +01:00
|
|
|
"github.com/navidrome/navidrome/scanner"
|
2021-06-10 18:20:52 +02:00
|
|
|
"github.com/navidrome/navidrome/server/events"
|
2020-01-24 01:44:08 +01:00
|
|
|
"github.com/navidrome/navidrome/server/subsonic/responses"
|
2023-12-21 22:32:37 +01:00
|
|
|
"github.com/navidrome/navidrome/utils/req"
|
2020-01-07 20:56:26 +01:00
|
|
|
)
|
|
|
|
|
2020-11-01 23:04:53 +01:00
|
|
|
const Version = "1.16.1"
|
2020-01-07 20:56:26 +01:00
|
|
|
|
2022-11-21 18:57:56 +01:00
|
|
|
type handler = func(*http.Request) (*responses.Subsonic, error)
|
|
|
|
type handlerRaw = func(http.ResponseWriter, *http.Request) (*responses.Subsonic, error)
|
2020-01-07 20:56:26 +01:00
|
|
|
|
2020-01-11 18:37:05 +01:00
|
|
|
type Router struct {
|
2021-06-13 18:46:36 +02:00
|
|
|
http.Handler
|
2022-11-21 18:57:56 +01:00
|
|
|
ds model.DataStore
|
2022-12-25 22:07:28 +01:00
|
|
|
artwork artwork.Artwork
|
2022-11-21 18:57:56 +01:00
|
|
|
streamer core.MediaStreamer
|
|
|
|
archiver core.Archiver
|
|
|
|
players core.Players
|
|
|
|
externalMetadata core.ExternalMetadata
|
|
|
|
playlists core.Playlists
|
|
|
|
scanner scanner.Scanner
|
|
|
|
broker events.Broker
|
|
|
|
scrobbler scrobbler.PlayTracker
|
2023-01-22 20:38:55 +01:00
|
|
|
share core.Share
|
2024-05-09 04:21:38 +02:00
|
|
|
playback playback.PlaybackServer
|
2020-01-11 18:37:05 +01:00
|
|
|
}
|
|
|
|
|
2022-12-25 22:07:28 +01:00
|
|
|
func New(ds model.DataStore, artwork artwork.Artwork, streamer core.MediaStreamer, archiver core.Archiver,
|
2021-10-26 16:35:58 +02:00
|
|
|
players core.Players, externalMetadata core.ExternalMetadata, scanner scanner.Scanner, broker events.Broker,
|
2024-05-09 04:21:38 +02:00
|
|
|
playlists core.Playlists, scrobbler scrobbler.PlayTracker, share core.Share, playback playback.PlaybackServer,
|
|
|
|
) *Router {
|
2020-10-27 18:52:01 +01:00
|
|
|
r := &Router{
|
2022-11-21 18:57:56 +01:00
|
|
|
ds: ds,
|
|
|
|
artwork: artwork,
|
|
|
|
streamer: streamer,
|
|
|
|
archiver: archiver,
|
|
|
|
players: players,
|
|
|
|
externalMetadata: externalMetadata,
|
|
|
|
playlists: playlists,
|
|
|
|
scanner: scanner,
|
|
|
|
broker: broker,
|
|
|
|
scrobbler: scrobbler,
|
2023-01-22 20:38:55 +01:00
|
|
|
share: share,
|
2024-05-09 04:21:38 +02:00
|
|
|
playback: playback,
|
2020-10-27 18:52:01 +01:00
|
|
|
}
|
2021-06-13 18:46:36 +02:00
|
|
|
r.Handler = r.routes()
|
2020-01-11 19:21:43 +01:00
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
|
|
|
func (api *Router) routes() http.Handler {
|
2020-01-07 20:56:26 +01:00
|
|
|
r := chi.NewRouter()
|
|
|
|
|
2020-01-27 21:10:46 +01:00
|
|
|
r.Use(postFormToQueryParams)
|
2020-01-09 16:35:00 +01:00
|
|
|
r.Use(checkRequiredParameters)
|
2022-11-21 18:57:56 +01:00
|
|
|
r.Use(authenticate(api.ds))
|
2020-01-31 14:35:33 +01:00
|
|
|
// TODO Validate version
|
2020-01-07 20:56:26 +01:00
|
|
|
|
2020-01-14 02:45:38 +01:00
|
|
|
// Subsonic endpoints, grouped by controller
|
2020-01-07 20:56:26 +01:00
|
|
|
r.Group(func(r chi.Router) {
|
2022-11-21 18:57:56 +01:00
|
|
|
r.Use(getPlayer(api.players))
|
|
|
|
h(r, "ping", api.Ping)
|
|
|
|
h(r, "getLicense", api.GetLicense)
|
2020-01-07 20:56:26 +01:00
|
|
|
})
|
|
|
|
r.Group(func(r chi.Router) {
|
2022-11-21 18:57:56 +01: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)
|
2023-01-18 02:22:54 +01:00
|
|
|
h(r, "getAlbumInfo", api.GetAlbumInfo)
|
|
|
|
h(r, "getAlbumInfo2", api.GetAlbumInfo)
|
2022-11-21 18:57:56 +01:00
|
|
|
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)
|
2020-01-07 20:56:26 +01:00
|
|
|
})
|
|
|
|
r.Group(func(r chi.Router) {
|
2022-11-21 18:57:56 +01:00
|
|
|
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)
|
|
|
|
h(r, "getNowPlaying", api.GetNowPlaying)
|
|
|
|
h(r, "getRandomSongs", api.GetRandomSongs)
|
|
|
|
h(r, "getSongsByGenre", api.GetSongsByGenre)
|
2020-01-07 20:56:26 +01:00
|
|
|
})
|
|
|
|
r.Group(func(r chi.Router) {
|
2022-11-21 18:57:56 +01:00
|
|
|
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)
|
2020-01-07 20:56:26 +01:00
|
|
|
})
|
|
|
|
r.Group(func(r chi.Router) {
|
2022-11-21 18:57:56 +01:00
|
|
|
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)
|
2020-01-07 20:56:26 +01:00
|
|
|
})
|
2020-07-31 19:07:39 +02:00
|
|
|
r.Group(func(r chi.Router) {
|
2022-11-21 18:57:56 +01:00
|
|
|
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)
|
|
|
|
h(r, "savePlayQueue", api.SavePlayQueue)
|
2020-07-31 19:07:39 +02:00
|
|
|
})
|
2020-01-07 20:56:26 +01:00
|
|
|
r.Group(func(r chi.Router) {
|
2022-11-21 18:57:56 +01:00
|
|
|
r.Use(getPlayer(api.players))
|
|
|
|
h(r, "search2", api.Search2)
|
|
|
|
h(r, "search3", api.Search3)
|
2020-01-07 20:56:26 +01:00
|
|
|
})
|
|
|
|
r.Group(func(r chi.Router) {
|
2022-11-21 18:57:56 +01:00
|
|
|
h(r, "getUser", api.GetUser)
|
|
|
|
h(r, "getUsers", api.GetUsers)
|
2020-10-27 23:19:56 +01:00
|
|
|
})
|
|
|
|
r.Group(func(r chi.Router) {
|
2022-11-21 18:57:56 +01:00
|
|
|
h(r, "getScanStatus", api.GetScanStatus)
|
|
|
|
h(r, "startScan", api.StartScan)
|
2020-01-07 20:56:26 +01:00
|
|
|
})
|
|
|
|
r.Group(func(r chi.Router) {
|
2022-11-21 18:57:56 +01:00
|
|
|
hr(r, "getAvatar", api.GetAvatar)
|
|
|
|
h(r, "getLyrics", api.GetLyrics)
|
2023-12-28 02:20:29 +01:00
|
|
|
h(r, "getLyricsBySongId", api.GetLyricsBySongId)
|
2020-01-07 20:56:26 +01:00
|
|
|
})
|
2023-01-14 00:10:32 +01:00
|
|
|
r.Group(func(r chi.Router) {
|
|
|
|
// configure request throttling
|
|
|
|
if conf.Server.DevArtworkMaxRequests > 0 {
|
2023-02-04 03:04:54 +01:00
|
|
|
log.Debug("Throttling Subsonic getCoverArt endpoint", "maxRequests", conf.Server.DevArtworkMaxRequests,
|
2023-02-02 22:55:12 +01:00
|
|
|
"backlogLimit", conf.Server.DevArtworkThrottleBacklogLimit, "backlogTimeout",
|
|
|
|
conf.Server.DevArtworkThrottleBacklogTimeout)
|
|
|
|
r.Use(middleware.ThrottleBacklog(conf.Server.DevArtworkMaxRequests, conf.Server.DevArtworkThrottleBacklogLimit,
|
2023-01-14 00:10:32 +01:00
|
|
|
conf.Server.DevArtworkThrottleBacklogTimeout))
|
|
|
|
}
|
|
|
|
hr(r, "getCoverArt", api.GetCoverArt)
|
|
|
|
})
|
2020-01-07 20:56:26 +01:00
|
|
|
r.Group(func(r chi.Router) {
|
2022-11-21 18:57:56 +01:00
|
|
|
r.Use(getPlayer(api.players))
|
|
|
|
hr(r, "stream", api.Stream)
|
|
|
|
hr(r, "download", api.Download)
|
2020-01-07 20:56:26 +01:00
|
|
|
})
|
2023-01-15 21:11:37 +01:00
|
|
|
r.Group(func(r chi.Router) {
|
|
|
|
h(r, "createInternetRadioStation", api.CreateInternetRadio)
|
|
|
|
h(r, "deleteInternetRadioStation", api.DeleteInternetRadio)
|
|
|
|
h(r, "getInternetRadioStations", api.GetInternetRadios)
|
|
|
|
h(r, "updateInternetRadioStation", api.UpdateInternetRadio)
|
|
|
|
})
|
2023-01-30 02:33:10 +01:00
|
|
|
if conf.Server.EnableSharing {
|
2023-01-25 00:31:40 +01:00
|
|
|
r.Group(func(r chi.Router) {
|
|
|
|
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")
|
|
|
|
}
|
2023-04-08 19:25:37 +02:00
|
|
|
r.Group(func(r chi.Router) {
|
|
|
|
h(r, "getOpenSubsonicExtensions", api.GetOpenSubsonicExtensions)
|
|
|
|
})
|
2020-01-14 02:45:38 +01: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 17:25:22 +02:00
|
|
|
if conf.Server.Jukebox.Enabled {
|
|
|
|
r.Group(func(r chi.Router) {
|
|
|
|
h(r, "jukeboxControl", api.JukeboxControl)
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
h501(r, "jukeboxControl")
|
|
|
|
}
|
|
|
|
|
2021-02-10 03:25:14 +01:00
|
|
|
// Not Implemented (yet?)
|
|
|
|
h501(r, "getPodcasts", "getNewestPodcasts", "refreshPodcasts", "createPodcastChannel", "deletePodcastChannel",
|
|
|
|
"deletePodcastEpisode", "downloadPodcastEpisode")
|
|
|
|
h501(r, "createUser", "updateUser", "deleteUser", "changePassword")
|
|
|
|
|
|
|
|
// Deprecated/Won't implement/Out of scope endpoints
|
|
|
|
h410(r, "search")
|
|
|
|
h410(r, "getChatMessages", "addChatMessage")
|
|
|
|
h410(r, "getVideos", "getVideoInfo", "getCaptions", "hls")
|
2020-01-07 20:56:26 +01:00
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
2022-11-26 19:13:05 +01: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)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add a Subsonic handler that requires a http.ResponseWriter (ex: stream, getCoverArt...)
|
2022-11-21 18:57:56 +01:00
|
|
|
func hr(r chi.Router, path string, f handlerRaw) {
|
2020-01-08 18:52:57 +01:00
|
|
|
handle := func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
res, err := f(w, r)
|
2020-01-07 20:56:26 +01:00
|
|
|
if err != nil {
|
2020-10-27 20:23:29 +01:00
|
|
|
sendError(w, r, err)
|
2020-01-07 20:56:26 +01:00
|
|
|
return
|
|
|
|
}
|
2022-07-27 20:27:18 +02:00
|
|
|
if r.Context().Err() != nil {
|
2023-12-25 22:29:59 +01:00
|
|
|
if log.IsGreaterOrEqualTo(log.LevelDebug) {
|
2022-12-14 16:52:46 +01:00
|
|
|
log.Warn(r.Context(), "Request was interrupted", "endpoint", r.URL.Path, r.Context().Err())
|
2022-11-03 17:38:05 +01:00
|
|
|
}
|
2022-07-27 20:27:18 +02:00
|
|
|
return
|
|
|
|
}
|
2020-01-07 20:56:26 +01:00
|
|
|
if res != nil {
|
2020-10-27 20:23:29 +01:00
|
|
|
sendResponse(w, r, res)
|
2020-01-07 20:56:26 +01:00
|
|
|
}
|
|
|
|
}
|
2022-11-26 19:13:05 +01:00
|
|
|
addHandler(r, path, handle)
|
2022-11-21 18:57:56 +01:00
|
|
|
}
|
|
|
|
|
2022-07-26 19:18:08 +02:00
|
|
|
// Add a handler that returns 501 - Not implemented. Used to signal that an endpoint is not implemented yet
|
2022-11-26 19:13:05 +01:00
|
|
|
func h501(r chi.Router, paths ...string) {
|
2021-02-10 03:25:14 +01:00
|
|
|
for _, path := range paths {
|
|
|
|
handle := func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
w.Header().Add("Cache-Control", "no-cache")
|
2024-02-17 16:39:29 +01:00
|
|
|
w.WriteHeader(http.StatusNotImplemented)
|
2021-02-10 03:25:14 +01:00
|
|
|
_, _ = w.Write([]byte("This endpoint is not implemented, but may be in future releases"))
|
|
|
|
}
|
2022-11-26 19:13:05 +01:00
|
|
|
addHandler(r, path, handle)
|
2021-02-10 03:25:14 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-14 02:45:38 +01:00
|
|
|
// Add a handler that returns 410 - Gone. Used to signal that an endpoint will not be implemented
|
2021-02-10 03:25:14 +01:00
|
|
|
func h410(r chi.Router, paths ...string) {
|
|
|
|
for _, path := range paths {
|
|
|
|
handle := func(w http.ResponseWriter, r *http.Request) {
|
2024-02-17 16:39:29 +01:00
|
|
|
w.WriteHeader(http.StatusGone)
|
2021-02-10 03:25:14 +01:00
|
|
|
_, _ = w.Write([]byte("This endpoint will not be implemented"))
|
|
|
|
}
|
2022-11-26 19:13:05 +01:00
|
|
|
addHandler(r, path, handle)
|
2020-01-14 02:45:38 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-26 19:13:05 +01: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 22:32:37 +01: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")
|
|
|
|
default:
|
|
|
|
err = newError(responses.ErrorGeneric, fmt.Sprintf("Internal Server Error: %s", err))
|
|
|
|
}
|
|
|
|
var subErr subError
|
|
|
|
errors.As(err, &subErr)
|
|
|
|
return subErr
|
|
|
|
}
|
|
|
|
|
2020-10-27 20:23:29 +01:00
|
|
|
func sendError(w http.ResponseWriter, r *http.Request, err error) {
|
2023-12-21 22:32:37 +01:00
|
|
|
subErr := mapToSubsonicError(err)
|
2020-08-14 04:11:18 +02:00
|
|
|
response := newResponse()
|
2024-02-17 16:39:29 +01:00
|
|
|
response.Status = responses.StatusFailed
|
2023-12-21 22:32:37 +01:00
|
|
|
response.Error = &responses.Error{Code: int32(subErr.code), Message: subErr.Error()}
|
2020-01-07 20:56:26 +01:00
|
|
|
|
2020-10-27 20:23:29 +01:00
|
|
|
sendResponse(w, r, response)
|
2020-01-07 20:56:26 +01:00
|
|
|
}
|
|
|
|
|
2020-10-27 20:23:29 +01:00
|
|
|
func sendResponse(w http.ResponseWriter, r *http.Request, payload *responses.Subsonic) {
|
2023-12-21 23:41:09 +01:00
|
|
|
p := req.Params(r)
|
|
|
|
f, _ := p.String("f")
|
2020-01-07 20:56:26 +01:00
|
|
|
var response []byte
|
2024-02-17 00:43:36 +01:00
|
|
|
var err error
|
2020-01-07 20:56:26 +01:00
|
|
|
switch f {
|
|
|
|
case "json":
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
wrapper := &responses.JsonWrapper{Subsonic: *payload}
|
2024-02-17 00:43:36 +01:00
|
|
|
response, err = json.Marshal(wrapper)
|
2020-01-07 20:56:26 +01:00
|
|
|
case "jsonp":
|
2020-01-09 06:18:55 +01:00
|
|
|
w.Header().Set("Content-Type", "application/javascript")
|
2023-12-21 23:41:09 +01:00
|
|
|
callback, _ := p.String("callback")
|
2020-01-07 20:56:26 +01:00
|
|
|
wrapper := &responses.JsonWrapper{Subsonic: *payload}
|
2024-02-17 00:43:36 +01:00
|
|
|
response, err = json.Marshal(wrapper)
|
|
|
|
response = []byte(fmt.Sprintf("%s(%s)", callback, response))
|
2020-01-07 20:56:26 +01:00
|
|
|
default:
|
|
|
|
w.Header().Set("Content-Type", "application/xml")
|
2024-02-17 00:43:36 +01:00
|
|
|
response, err = xml.Marshal(payload)
|
|
|
|
}
|
2024-02-17 01:41:53 +01:00
|
|
|
// This should never happen, but if it does, we need to know
|
2024-02-17 00:43:36 +01:00
|
|
|
if err != nil {
|
|
|
|
log.Error(r.Context(), "Error marshalling response", "format", f, err)
|
2024-02-17 16:39:29 +01:00
|
|
|
sendError(w, r, err)
|
2024-02-17 01:41:53 +01:00
|
|
|
return
|
2020-01-07 20:56:26 +01:00
|
|
|
}
|
2024-02-17 16:39:29 +01:00
|
|
|
if payload.Status == responses.StatusOK {
|
2023-12-25 22:29:59 +01:00
|
|
|
if log.IsGreaterOrEqualTo(log.LevelTrace) {
|
2022-12-14 16:52:46 +01:00
|
|
|
log.Debug(r.Context(), "API: Successful response", "endpoint", r.URL.Path, "status", "OK", "body", string(response))
|
2020-02-02 02:07:15 +01:00
|
|
|
} else {
|
2022-12-14 16:52:46 +01:00
|
|
|
log.Debug(r.Context(), "API: Successful response", "endpoint", r.URL.Path, "status", "OK")
|
2020-02-02 02:07:15 +01:00
|
|
|
}
|
|
|
|
} else {
|
2022-12-14 16:52:46 +01:00
|
|
|
log.Warn(r.Context(), "API: Failed response", "endpoint", r.URL.Path, "error", payload.Error.Code, "message", payload.Error.Message)
|
2020-02-02 02:07:15 +01:00
|
|
|
}
|
2020-04-26 18:35:26 +02:00
|
|
|
if _, err := w.Write(response); err != nil {
|
2022-12-14 16:52:46 +01:00
|
|
|
log.Error(r, "Error sending response to client", "endpoint", r.URL.Path, "payload", string(response), err)
|
2020-04-26 18:35:26 +02:00
|
|
|
}
|
2020-01-07 20:56:26 +01:00
|
|
|
}
|