Replace all `utils.Param*` with `req.Params`

This commit is contained in:
Deluan 2023-12-21 17:41:09 -05:00
parent 00597e01e9
commit dfcc189cff
27 changed files with 269 additions and 513 deletions

View File

@ -18,7 +18,7 @@ import (
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/server" "github.com/navidrome/navidrome/server"
"github.com/navidrome/navidrome/utils" "github.com/navidrome/navidrome/utils/req"
) )
//go:embed token_received.html //go:embed token_received.html
@ -89,13 +89,14 @@ func (s *Router) unlink(w http.ResponseWriter, r *http.Request) {
} }
func (s *Router) callback(w http.ResponseWriter, r *http.Request) { func (s *Router) callback(w http.ResponseWriter, r *http.Request) {
token := utils.ParamString(r, "token") p := req.Params(r)
if token == "" { token, err := p.String("token")
if err != nil {
_ = rest.RespondWithError(w, http.StatusBadRequest, "token not received") _ = rest.RespondWithError(w, http.StatusBadRequest, "token not received")
return return
} }
uid := utils.ParamString(r, "uid") uid, err := p.String("uid")
if uid == "" { if err != nil {
_ = rest.RespondWithError(w, http.StatusBadRequest, "uid not received") _ = rest.RespondWithError(w, http.StatusBadRequest, "uid not received")
return return
} }
@ -103,7 +104,7 @@ func (s *Router) callback(w http.ResponseWriter, r *http.Request) {
// Need to add user to context, as this is a non-authenticated endpoint, so it does not // Need to add user to context, as this is a non-authenticated endpoint, so it does not
// automatically contain any user info // automatically contain any user info
ctx := request.WithUser(r.Context(), model.User{ID: uid}) ctx := request.WithUser(r.Context(), model.User{ID: uid})
err := s.fetchSessionKey(ctx, uid, token) err = s.fetchSessionKey(ctx, uid, token)
if err != nil { if err != nil {
w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)

View File

@ -14,7 +14,7 @@ import (
"github.com/navidrome/navidrome/core" "github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils" "github.com/navidrome/navidrome/utils/req"
) )
type restHandler = func(rest.RepositoryConstructor, ...rest.Logger) http.HandlerFunc type restHandler = func(rest.RepositoryConstructor, ...rest.Logger) http.HandlerFunc
@ -95,8 +95,9 @@ func handleExportPlaylist(ds model.DataStore) http.HandlerFunc {
func deleteFromPlaylist(ds model.DataStore) http.HandlerFunc { func deleteFromPlaylist(ds model.DataStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
playlistId := utils.ParamString(r, ":playlistId") p := req.Params(r)
ids := r.URL.Query()["id"] playlistId, _ := p.String(":playlistId")
ids, _ := p.Strings("id")
err := ds.WithTx(func(tx model.DataStore) error { err := ds.WithTx(func(tx model.DataStore) error {
tracksRepo := tx.Playlist(r.Context()).Tracks(playlistId, true) tracksRepo := tx.Playlist(r.Context()).Tracks(playlistId, true)
return tracksRepo.Delete(ids...) return tracksRepo.Delete(ids...)
@ -139,7 +140,8 @@ func addToPlaylist(ds model.DataStore) http.HandlerFunc {
} }
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
playlistId := utils.ParamString(r, ":playlistId") p := req.Params(r)
playlistId, _ := p.String(":playlistId")
var payload addTracksPayload var payload addTracksPayload
err := json.NewDecoder(r.Body).Decode(&payload) err := json.NewDecoder(r.Body).Decode(&payload)
if err != nil { if err != nil {
@ -183,8 +185,9 @@ func reorderItem(ds model.DataStore) http.HandlerFunc {
} }
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
playlistId := utils.ParamString(r, ":playlistId") p := req.Params(r)
id := utils.ParamInt(r, ":id", 0) playlistId, _ := p.String(":playlistId")
id := p.IntOr(":id", 0)
if id == 0 { if id == 0 {
http.Error(w, "invalid id", http.StatusBadRequest) http.Error(w, "invalid id", http.StatusBadRequest)
return return

View File

@ -2,15 +2,17 @@ package public
import ( import (
"net/http" "net/http"
"github.com/navidrome/navidrome/utils/req"
) )
func (p *Router) handleDownloads(w http.ResponseWriter, r *http.Request) { func (pub *Router) handleDownloads(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get(":id") id, err := req.Params(r).String(":id")
if id == "" { if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
err := p.archiver.ZipShare(r.Context(), id, w) err = pub.archiver.ZipShare(r.Context(), id, w)
checkShareError(r.Context(), w, err, id) checkShareError(r.Context(), w, err, id)
} }

View File

@ -10,10 +10,10 @@ import (
"github.com/navidrome/navidrome/core/artwork" "github.com/navidrome/navidrome/core/artwork"
"github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/utils" "github.com/navidrome/navidrome/utils/req"
) )
func (p *Router) handleImages(w http.ResponseWriter, r *http.Request) { func (pub *Router) handleImages(w http.ResponseWriter, r *http.Request) {
// If context is already canceled, discard request without further processing // If context is already canceled, discard request without further processing
if r.Context().Err() != nil { if r.Context().Err() != nil {
return return
@ -21,7 +21,9 @@ func (p *Router) handleImages(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel() defer cancel()
id := r.URL.Query().Get(":id")
p := req.Params(r)
id, _ := p.String(":id")
if id == "" { if id == "" {
http.Error(w, "invalid id", http.StatusBadRequest) http.Error(w, "invalid id", http.StatusBadRequest)
return return
@ -32,9 +34,9 @@ func (p *Router) handleImages(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
size := utils.ParamInt(r, "size", 0) size := p.IntOr("size", 0)
imgReader, lastUpdate, err := p.artwork.Get(ctx, artId, size) imgReader, lastUpdate, err := pub.artwork.Get(ctx, artId, size)
switch { switch {
case errors.Is(err, context.Canceled): case errors.Is(err, context.Canceled):
return return

View File

@ -10,31 +10,32 @@ import (
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/server" "github.com/navidrome/navidrome/server"
"github.com/navidrome/navidrome/ui" "github.com/navidrome/navidrome/ui"
"github.com/navidrome/navidrome/utils/req"
) )
func (p *Router) handleShares(w http.ResponseWriter, r *http.Request) { func (pub *Router) handleShares(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get(":id") id, err := req.Params(r).String(":id")
if id == "" { if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
// If requested file is a UI asset, just serve it // If requested file is a UI asset, just serve it
_, err := ui.BuildAssets().Open(id) _, err = ui.BuildAssets().Open(id)
if err == nil { if err == nil {
p.assetsHandler.ServeHTTP(w, r) pub.assetsHandler.ServeHTTP(w, r)
return return
} }
// If it is not, consider it a share ID // If it is not, consider it a share ID
s, err := p.share.Load(r.Context(), id) s, err := pub.share.Load(r.Context(), id)
if err != nil { if err != nil {
checkShareError(r.Context(), w, err, id) checkShareError(r.Context(), w, err, id)
return return
} }
s = p.mapShareInfo(r, *s) s = pub.mapShareInfo(r, *s)
server.IndexWithShare(p.ds, ui.BuildAssets(), s)(w, r) server.IndexWithShare(pub.ds, ui.BuildAssets(), s)(w, r)
} }
func checkShareError(ctx context.Context, w http.ResponseWriter, err error, id string) { func checkShareError(ctx context.Context, w http.ResponseWriter, err error, id string) {
@ -54,7 +55,7 @@ func checkShareError(ctx context.Context, w http.ResponseWriter, err error, id s
} }
} }
func (p *Router) mapShareInfo(r *http.Request, s model.Share) *model.Share { func (pub *Router) mapShareInfo(r *http.Request, s model.Share) *model.Share {
s.URL = ShareURL(r, s.ID) s.URL = ShareURL(r, s.ID)
s.ImageURL = ImageURL(r, s.CoverArtID(), consts.UICoverArtSize) s.ImageURL = ImageURL(r, s.CoverArtID(), consts.UICoverArtSize)
for i := range s.Tracks { for i := range s.Tracks {

View File

@ -10,12 +10,13 @@ import (
"github.com/lestrrat-go/jwx/v2/jwt" "github.com/lestrrat-go/jwx/v2/jwt"
"github.com/navidrome/navidrome/core/auth" "github.com/navidrome/navidrome/core/auth"
"github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/utils" "github.com/navidrome/navidrome/utils/req"
) )
func (p *Router) handleStream(w http.ResponseWriter, r *http.Request) { func (pub *Router) handleStream(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
tokenId := r.URL.Query().Get(":id") p := req.Params(r)
tokenId, _ := p.String(":id")
info, err := decodeStreamInfo(tokenId) info, err := decodeStreamInfo(tokenId)
if err != nil { if err != nil {
log.Error(ctx, "Error parsing shared stream info", err) log.Error(ctx, "Error parsing shared stream info", err)
@ -23,7 +24,7 @@ func (p *Router) handleStream(w http.ResponseWriter, r *http.Request) {
return return
} }
stream, err := p.streamer.NewStream(ctx, info.id, info.format, info.bitrate, 0) stream, err := pub.streamer.NewStream(ctx, info.id, info.format, info.bitrate, 0)
if err != nil { if err != nil {
log.Error(ctx, "Error starting shared stream", err) log.Error(ctx, "Error starting shared stream", err)
http.Error(w, "invalid request", http.StatusInternalServerError) http.Error(w, "invalid request", http.StatusInternalServerError)
@ -46,7 +47,7 @@ func (p *Router) handleStream(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Accept-Ranges", "none") w.Header().Set("Accept-Ranges", "none")
w.Header().Set("Content-Type", stream.ContentType()) w.Header().Set("Content-Type", stream.ContentType())
estimateContentLength := utils.ParamBool(r, "estimateContentLength", false) estimateContentLength := p.BoolOr("estimateContentLength", false)
// if Client requests the estimated content-length, send it // if Client requests the estimated content-length, send it
if estimateContentLength { if estimateContentLength {

View File

@ -35,7 +35,7 @@ func New(ds model.DataStore, artwork artwork.Artwork, streamer core.MediaStreame
return p return p
} }
func (p *Router) routes() http.Handler { func (pub *Router) routes() http.Handler {
r := chi.NewRouter() r := chi.NewRouter()
r.Group(func(r chi.Router) { r.Group(func(r chi.Router) {
@ -48,16 +48,16 @@ func (p *Router) routes() http.Handler {
r.Use(middleware.ThrottleBacklog(conf.Server.DevArtworkMaxRequests, conf.Server.DevArtworkThrottleBacklogLimit, r.Use(middleware.ThrottleBacklog(conf.Server.DevArtworkMaxRequests, conf.Server.DevArtworkThrottleBacklogLimit,
conf.Server.DevArtworkThrottleBacklogTimeout)) conf.Server.DevArtworkThrottleBacklogTimeout))
} }
r.HandleFunc("/img/{id}", p.handleImages) r.HandleFunc("/img/{id}", pub.handleImages)
}) })
if conf.Server.EnableSharing { if conf.Server.EnableSharing {
r.HandleFunc("/s/{id}", p.handleStream) r.HandleFunc("/s/{id}", pub.handleStream)
if conf.Server.EnableDownloads { if conf.Server.EnableDownloads {
r.HandleFunc("/d/{id}", p.handleDownloads) r.HandleFunc("/d/{id}", pub.handleDownloads)
} }
r.HandleFunc("/{id}", p.handleShares) r.HandleFunc("/{id}", pub.handleShares)
r.HandleFunc("/", p.handleShares) r.HandleFunc("/", pub.handleShares)
r.Handle("/*", p.assetsHandler) r.Handle("/*", pub.assetsHandler)
} }
}) })
return r return r

View File

@ -10,12 +10,13 @@ import (
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/server/subsonic/filter" "github.com/navidrome/navidrome/server/subsonic/filter"
"github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/utils"
"github.com/navidrome/navidrome/utils/number" "github.com/navidrome/navidrome/utils/number"
"github.com/navidrome/navidrome/utils/req"
) )
func (api *Router) getAlbumList(r *http.Request) (model.Albums, int64, error) { func (api *Router) getAlbumList(r *http.Request) (model.Albums, int64, error) {
typ, err := requiredParamString(r, "type") p := req.Params(r)
typ, err := p.String("type")
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@ -39,17 +40,17 @@ func (api *Router) getAlbumList(r *http.Request) (model.Albums, int64, error) {
case "highest": case "highest":
opts = filter.AlbumsByRating() opts = filter.AlbumsByRating()
case "byGenre": case "byGenre":
genre, err := requiredParamString(r, "genre") genre, err := p.String("genre")
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
opts = filter.AlbumsByGenre(genre) opts = filter.AlbumsByGenre(genre)
case "byYear": case "byYear":
fromYear, err := requiredParamInt(r, "fromYear") fromYear, err := p.Int("fromYear")
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
toYear, err := requiredParamInt(r, "toYear") toYear, err := p.Int("toYear")
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@ -59,8 +60,8 @@ func (api *Router) getAlbumList(r *http.Request) (model.Albums, int64, error) {
return nil, 0, newError(responses.ErrorGeneric, "type '%s' not implemented", typ) return nil, 0, newError(responses.ErrorGeneric, "type '%s' not implemented", typ)
} }
opts.Offset = utils.ParamInt(r, "offset", 0) opts.Offset = p.IntOr("offset", 0)
opts.Max = number.Min(utils.ParamInt(r, "size", 10), 500) opts.Max = number.Min(p.IntOr("size", 10), 500)
albums, err := api.ds.Album(r.Context()).GetAllWithoutGenres(opts) albums, err := api.ds.Album(r.Context()).GetAllWithoutGenres(opts)
if err != nil { if err != nil {
@ -163,10 +164,11 @@ func (api *Router) GetNowPlaying(r *http.Request) (*responses.Subsonic, error) {
} }
func (api *Router) GetRandomSongs(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetRandomSongs(r *http.Request) (*responses.Subsonic, error) {
size := number.Min(utils.ParamInt(r, "size", 10), 500) p := req.Params(r)
genre := utils.ParamString(r, "genre") size := number.Min(p.IntOr("size", 10), 500)
fromYear := utils.ParamInt(r, "fromYear", 0) genre, _ := p.String("genre")
toYear := utils.ParamInt(r, "toYear", 0) fromYear := p.IntOr("fromYear", 0)
toYear := p.IntOr("toYear", 0)
songs, err := api.getSongs(r.Context(), 0, size, filter.SongsByRandom(genre, fromYear, toYear)) songs, err := api.getSongs(r.Context(), 0, size, filter.SongsByRandom(genre, fromYear, toYear))
if err != nil { if err != nil {
@ -181,9 +183,10 @@ func (api *Router) GetRandomSongs(r *http.Request) (*responses.Subsonic, error)
} }
func (api *Router) GetSongsByGenre(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetSongsByGenre(r *http.Request) (*responses.Subsonic, error) {
count := number.Min(utils.ParamInt(r, "count", 10), 500) p := req.Params(r)
offset := utils.ParamInt(r, "offset", 0) count := number.Min(p.IntOr("count", 10), 500)
genre := utils.ParamString(r, "genre") offset := p.IntOr("offset", 0)
genre, _ := p.String("genre")
songs, err := api.getSongs(r.Context(), offset, count, filter.SongsByGenre(genre)) songs, err := api.getSongs(r.Context(), offset, count, filter.SongsByGenre(genre))
if err != nil { if err != nil {

View File

@ -6,6 +6,7 @@ import (
"net/http/httptest" "net/http/httptest"
"github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/utils/req"
"github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
@ -36,7 +37,7 @@ var _ = Describe("Album Lists", func() {
}) })
resp, err := router.GetAlbumList(w, r) resp, err := router.GetAlbumList(w, r)
Expect(err).To(BeNil()) Expect(err).ToNot(HaveOccurred())
Expect(resp.AlbumList.Album[0].Id).To(Equal("1")) Expect(resp.AlbumList.Album[0].Id).To(Equal("1"))
Expect(resp.AlbumList.Album[1].Id).To(Equal("2")) Expect(resp.AlbumList.Album[1].Id).To(Equal("2"))
Expect(w.Header().Get("x-total-count")).To(Equal("2")) Expect(w.Header().Get("x-total-count")).To(Equal("2"))
@ -47,12 +48,8 @@ var _ = Describe("Album Lists", func() {
It("should fail if missing type parameter", func() { It("should fail if missing type parameter", func() {
r := newGetRequest() r := newGetRequest()
_, err := router.GetAlbumList(w, r) _, err := router.GetAlbumList(w, r)
var subErr subError
isSubError := errors.As(err, &subErr)
Expect(isSubError).To(BeTrue()) Expect(err).To(MatchError(req.ErrMissingParam))
Expect(subErr).To(MatchError("required 'type' parameter is missing"))
Expect(subErr.code).To(Equal(responses.ErrorMissingParameter))
}) })
It("should return error if call fails", func() { It("should return error if call fails", func() {
@ -61,7 +58,7 @@ var _ = Describe("Album Lists", func() {
_, err := router.GetAlbumList(w, r) _, err := router.GetAlbumList(w, r)
Expect(err).ToNot(BeNil()) Expect(err).To(MatchError(errSubsonic))
var subErr subError var subErr subError
errors.As(err, &subErr) errors.As(err, &subErr)
Expect(subErr.code).To(Equal(responses.ErrorGeneric)) Expect(subErr.code).To(Equal(responses.ErrorGeneric))
@ -76,7 +73,7 @@ var _ = Describe("Album Lists", func() {
}) })
resp, err := router.GetAlbumList2(w, r) resp, err := router.GetAlbumList2(w, r)
Expect(err).To(BeNil()) Expect(err).ToNot(HaveOccurred())
Expect(resp.AlbumList2.Album[0].Id).To(Equal("1")) Expect(resp.AlbumList2.Album[0].Id).To(Equal("1"))
Expect(resp.AlbumList2.Album[1].Id).To(Equal("2")) Expect(resp.AlbumList2.Album[1].Id).To(Equal("2"))
Expect(w.Header().Get("x-total-count")).To(Equal("2")) Expect(w.Header().Get("x-total-count")).To(Equal("2"))
@ -86,13 +83,10 @@ var _ = Describe("Album Lists", func() {
It("should fail if missing type parameter", func() { It("should fail if missing type parameter", func() {
r := newGetRequest() r := newGetRequest()
_, err := router.GetAlbumList2(w, r) _, err := router.GetAlbumList2(w, r)
var subErr subError Expect(err).To(MatchError(req.ErrMissingParam))
errors.As(err, &subErr)
Expect(subErr).To(MatchError("required 'type' parameter is missing"))
Expect(subErr.code).To(Equal(responses.ErrorMissingParameter))
}) })
It("should return error if call fails", func() { It("should return error if call fails", func() {
@ -101,9 +95,9 @@ var _ = Describe("Album Lists", func() {
_, err := router.GetAlbumList2(w, r) _, err := router.GetAlbumList2(w, r)
Expect(err).To(MatchError(errSubsonic))
var subErr subError var subErr subError
errors.As(err, &subErr) errors.As(err, &subErr)
Expect(subErr).ToNot(BeNil())
Expect(subErr.code).To(Equal(responses.ErrorGeneric)) Expect(subErr.code).To(Equal(responses.ErrorGeneric))
}) })
}) })

View File

@ -18,7 +18,6 @@ import (
"github.com/navidrome/navidrome/scanner" "github.com/navidrome/navidrome/scanner"
"github.com/navidrome/navidrome/server/events" "github.com/navidrome/navidrome/server/events"
"github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/utils"
"github.com/navidrome/navidrome/utils/req" "github.com/navidrome/navidrome/utils/req"
) )
@ -283,7 +282,8 @@ func sendError(w http.ResponseWriter, r *http.Request, err error) {
} }
func sendResponse(w http.ResponseWriter, r *http.Request, payload *responses.Subsonic) { func sendResponse(w http.ResponseWriter, r *http.Request, payload *responses.Subsonic) {
f := utils.ParamString(r, "f") p := req.Params(r)
f, _ := p.String("f")
var response []byte var response []byte
switch f { switch f {
case "json": case "json":
@ -292,7 +292,7 @@ func sendResponse(w http.ResponseWriter, r *http.Request, payload *responses.Sub
response, _ = json.Marshal(wrapper) response, _ = json.Marshal(wrapper)
case "jsonp": case "jsonp":
w.Header().Set("Content-Type", "application/javascript") w.Header().Set("Content-Type", "application/javascript")
callback := utils.ParamString(r, "callback") callback, _ := p.String("callback")
wrapper := &responses.JsonWrapper{Subsonic: *payload} wrapper := &responses.JsonWrapper{Subsonic: *payload}
data, _ := json.Marshal(wrapper) data, _ := json.Marshal(wrapper)
response = []byte(fmt.Sprintf("%s(%s)", callback, data)) response = []byte(fmt.Sprintf("%s(%s)", callback, data))

View File

@ -7,7 +7,7 @@ import (
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/utils" "github.com/navidrome/navidrome/utils/req"
) )
func (api *Router) GetBookmarks(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetBookmarks(r *http.Request) (*responses.Subsonic, error) {
@ -36,13 +36,14 @@ func (api *Router) GetBookmarks(r *http.Request) (*responses.Subsonic, error) {
} }
func (api *Router) CreateBookmark(r *http.Request) (*responses.Subsonic, error) { func (api *Router) CreateBookmark(r *http.Request) (*responses.Subsonic, error) {
id, err := requiredParamString(r, "id") p := req.Params(r)
id, err := p.String("id")
if err != nil { if err != nil {
return nil, err return nil, err
} }
comment := utils.ParamString(r, "comment") comment, _ := p.String("comment")
position := utils.ParamInt(r, "position", int64(0)) position := p.Int64Or("position", 0)
repo := api.ds.MediaFile(r.Context()) repo := api.ds.MediaFile(r.Context())
err = repo.AddBookmark(id, comment, position) err = repo.AddBookmark(id, comment, position)
@ -53,7 +54,8 @@ func (api *Router) CreateBookmark(r *http.Request) (*responses.Subsonic, error)
} }
func (api *Router) DeleteBookmark(r *http.Request) (*responses.Subsonic, error) { func (api *Router) DeleteBookmark(r *http.Request) (*responses.Subsonic, error) {
id, err := requiredParamString(r, "id") p := req.Params(r)
id, err := p.String("id")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -88,13 +90,14 @@ func (api *Router) GetPlayQueue(r *http.Request) (*responses.Subsonic, error) {
} }
func (api *Router) SavePlayQueue(r *http.Request) (*responses.Subsonic, error) { func (api *Router) SavePlayQueue(r *http.Request) (*responses.Subsonic, error) {
ids, err := requiredParamStrings(r, "id") p := req.Params(r)
ids, err := p.Strings("id")
if err != nil { if err != nil {
return nil, err return nil, err
} }
current := utils.ParamString(r, "current") current, _ := p.String("current")
position := utils.ParamInt(r, "position", int64(0)) position := p.Int64Or("position", 0)
user, _ := request.UserFrom(r.Context()) user, _ := request.UserFrom(r.Context())
client, _ := request.ClientFrom(r.Context()) client, _ := request.ClientFrom(r.Context())

View File

@ -14,6 +14,7 @@ import (
"github.com/navidrome/navidrome/server/subsonic/filter" "github.com/navidrome/navidrome/server/subsonic/filter"
"github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/utils" "github.com/navidrome/navidrome/utils"
"github.com/navidrome/navidrome/utils/req"
) )
func (api *Router) GetMusicFolders(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetMusicFolders(r *http.Request) (*responses.Subsonic, error) {
@ -67,8 +68,9 @@ func (api *Router) getArtistIndex(r *http.Request, mediaFolderId int, ifModified
} }
func (api *Router) GetIndexes(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetIndexes(r *http.Request) (*responses.Subsonic, error) {
musicFolderId := utils.ParamInt(r, "musicFolderId", 0) p := req.Params(r)
ifModifiedSince := utils.ParamTime(r, "ifModifiedSince", time.Time{}) musicFolderId := p.IntOr("musicFolderId", 0)
ifModifiedSince := p.TimeOr("ifModifiedSince", time.Time{})
res, err := api.getArtistIndex(r, musicFolderId, ifModifiedSince) res, err := api.getArtistIndex(r, musicFolderId, ifModifiedSince)
if err != nil { if err != nil {
@ -81,7 +83,8 @@ func (api *Router) GetIndexes(r *http.Request) (*responses.Subsonic, error) {
} }
func (api *Router) GetArtists(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetArtists(r *http.Request) (*responses.Subsonic, error) {
musicFolderId := utils.ParamInt(r, "musicFolderId", 0) p := req.Params(r)
musicFolderId := p.IntOr("musicFolderId", 0)
res, err := api.getArtistIndex(r, musicFolderId, time.Time{}) res, err := api.getArtistIndex(r, musicFolderId, time.Time{})
if err != nil { if err != nil {
return nil, err return nil, err
@ -93,7 +96,8 @@ func (api *Router) GetArtists(r *http.Request) (*responses.Subsonic, error) {
} }
func (api *Router) GetMusicDirectory(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetMusicDirectory(r *http.Request) (*responses.Subsonic, error) {
id := utils.ParamString(r, "id") p := req.Params(r)
id, _ := p.String("id")
ctx := r.Context() ctx := r.Context()
entity, err := model.GetEntityByID(ctx, api.ds, id) entity, err := model.GetEntityByID(ctx, api.ds, id)
@ -129,7 +133,8 @@ func (api *Router) GetMusicDirectory(r *http.Request) (*responses.Subsonic, erro
} }
func (api *Router) GetArtist(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetArtist(r *http.Request) (*responses.Subsonic, error) {
id := utils.ParamString(r, "id") p := req.Params(r)
id, _ := p.String("id")
ctx := r.Context() ctx := r.Context()
artist, err := api.ds.Artist(ctx).Get(id) artist, err := api.ds.Artist(ctx).Get(id)
@ -151,7 +156,8 @@ func (api *Router) GetArtist(r *http.Request) (*responses.Subsonic, error) {
} }
func (api *Router) GetAlbum(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetAlbum(r *http.Request) (*responses.Subsonic, error) {
id := utils.ParamString(r, "id") p := req.Params(r)
id, _ := p.String("id")
ctx := r.Context() ctx := r.Context()
@ -177,7 +183,8 @@ func (api *Router) GetAlbum(r *http.Request) (*responses.Subsonic, error) {
} }
func (api *Router) GetAlbumInfo(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetAlbumInfo(r *http.Request) (*responses.Subsonic, error) {
id, err := requiredParamString(r, "id") p := req.Params(r)
id, err := p.String("id")
ctx := r.Context() ctx := r.Context()
if err != nil { if err != nil {
@ -204,7 +211,8 @@ func (api *Router) GetAlbumInfo(r *http.Request) (*responses.Subsonic, error) {
} }
func (api *Router) GetSong(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetSong(r *http.Request) (*responses.Subsonic, error) {
id := utils.ParamString(r, "id") p := req.Params(r)
id, _ := p.String("id")
ctx := r.Context() ctx := r.Context()
mf, err := api.ds.MediaFile(ctx).Get(id) mf, err := api.ds.MediaFile(ctx).Get(id)
@ -243,12 +251,13 @@ func (api *Router) GetGenres(r *http.Request) (*responses.Subsonic, error) {
func (api *Router) GetArtistInfo(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetArtistInfo(r *http.Request) (*responses.Subsonic, error) {
ctx := r.Context() ctx := r.Context()
id, err := requiredParamString(r, "id") p := req.Params(r)
id, err := p.String("id")
if err != nil { if err != nil {
return nil, err return nil, err
} }
count := utils.ParamInt(r, "count", 20) count := p.IntOr("count", 20)
includeNotPresent := utils.ParamBool(r, "includeNotPresent", false) includeNotPresent := p.BoolOr("includeNotPresent", false)
artist, err := api.externalMetadata.UpdateArtistInfo(ctx, id, count, includeNotPresent) artist, err := api.externalMetadata.UpdateArtistInfo(ctx, id, count, includeNotPresent)
if err != nil { if err != nil {
@ -295,11 +304,12 @@ func (api *Router) GetArtistInfo2(r *http.Request) (*responses.Subsonic, error)
func (api *Router) GetSimilarSongs(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetSimilarSongs(r *http.Request) (*responses.Subsonic, error) {
ctx := r.Context() ctx := r.Context()
id, err := requiredParamString(r, "id") p := req.Params(r)
id, err := p.String("id")
if err != nil { if err != nil {
return nil, err return nil, err
} }
count := utils.ParamInt(r, "count", 50) count := p.IntOr("count", 50)
songs, err := api.externalMetadata.SimilarSongs(ctx, id, count) songs, err := api.externalMetadata.SimilarSongs(ctx, id, count)
if err != nil { if err != nil {
@ -328,11 +338,12 @@ func (api *Router) GetSimilarSongs2(r *http.Request) (*responses.Subsonic, error
func (api *Router) GetTopSongs(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetTopSongs(r *http.Request) (*responses.Subsonic, error) {
ctx := r.Context() ctx := r.Context()
artist, err := requiredParamString(r, "artist") p := req.Params(r)
artist, err := p.String("artist")
if err != nil { if err != nil {
return nil, err return nil, err
} }
count := utils.ParamInt(r, "count", 50) count := p.IntOr("count", 50)
songs, err := api.externalMetadata.TopSongs(ctx, artist, count) songs, err := api.externalMetadata.TopSongs(ctx, artist, count)
if err != nil { if err != nil {

View File

@ -14,7 +14,6 @@ import (
"github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/server/public" "github.com/navidrome/navidrome/server/public"
"github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/utils"
) )
func newResponse() *responses.Subsonic { func newResponse() *responses.Subsonic {
@ -27,30 +26,6 @@ func newResponse() *responses.Subsonic {
} }
} }
func requiredParamString(r *http.Request, param string) (string, error) {
p := utils.ParamString(r, param)
if p == "" {
return "", newError(responses.ErrorMissingParameter, "required '%s' parameter is missing", param)
}
return p, nil
}
func requiredParamStrings(r *http.Request, param string) ([]string, error) {
ps := utils.ParamStrings(r, param)
if len(ps) == 0 {
return nil, newError(responses.ErrorMissingParameter, "required '%s' parameter is missing", param)
}
return ps, nil
}
func requiredParamInt(r *http.Request, param string) (int, error) {
p := utils.ParamString(r, param)
if p == "" {
return 0, newError(responses.ErrorMissingParameter, "required '%s' parameter is missing", param)
}
return utils.ParamInt(r, param, 0), nil
}
type subError struct { type subError struct {
code int code int
messages []interface{} messages []interface{}

View File

@ -7,7 +7,7 @@ import (
"github.com/navidrome/navidrome/core/playback" "github.com/navidrome/navidrome/core/playback"
"github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/utils" "github.com/navidrome/navidrome/utils/req"
) )
const ( const (
@ -27,8 +27,9 @@ const (
func (api *Router) JukeboxControl(r *http.Request) (*responses.Subsonic, error) { func (api *Router) JukeboxControl(r *http.Request) (*responses.Subsonic, error) {
ctx := r.Context() ctx := r.Context()
user := getUser(ctx) user := getUser(ctx)
p := req.Params(r)
actionString, err := requiredParamString(r, "action") actionString, err := p.String("action")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -58,31 +59,31 @@ func (api *Router) JukeboxControl(r *http.Request) (*responses.Subsonic, error)
case ActionStatus: case ActionStatus:
return createResponse(pb.Status(ctx)) return createResponse(pb.Status(ctx))
case ActionSet: case ActionSet:
ids := utils.ParamStrings(r, "id") ids, _ := p.Strings("id")
return createResponse(pb.Set(ctx, ids)) return createResponse(pb.Set(ctx, ids))
case ActionStart: case ActionStart:
return createResponse(pb.Start(ctx)) return createResponse(pb.Start(ctx))
case ActionStop: case ActionStop:
return createResponse(pb.Stop(ctx)) return createResponse(pb.Stop(ctx))
case ActionSkip: case ActionSkip:
index, err := requiredParamInt(r, "index") index, err := p.Int("index")
if err != nil { if err != nil {
return nil, newError(responses.ErrorMissingParameter, "missing parameter index, err: %s", err) return nil, newError(responses.ErrorMissingParameter, "missing parameter index, err: %s", err)
} }
offset := utils.ParamInt(r, "offset", 0) offset := p.IntOr("offset", 0)
if err != nil { if err != nil {
offset = 0 offset = 0
} }
return createResponse(pb.Skip(ctx, index, offset)) return createResponse(pb.Skip(ctx, index, offset))
case ActionAdd: case ActionAdd:
ids := utils.ParamStrings(r, "id") ids, _ := p.Strings("id")
return createResponse(pb.Add(ctx, ids)) return createResponse(pb.Add(ctx, ids))
case ActionClear: case ActionClear:
return createResponse(pb.Clear(ctx)) return createResponse(pb.Clear(ctx))
case ActionRemove: case ActionRemove:
index, err := requiredParamInt(r, "index") index, err := p.Int("index")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -91,7 +92,7 @@ func (api *Router) JukeboxControl(r *http.Request) (*responses.Subsonic, error)
case ActionShuffle: case ActionShuffle:
return createResponse(pb.Shuffle(ctx)) return createResponse(pb.Shuffle(ctx))
case ActionSetGain: case ActionSetGain:
gainStr, err := requiredParamString(r, "gain") gainStr, err := p.String("gain")
if err != nil { if err != nil {
return nil, newError(responses.ErrorMissingParameter, "missing parameter gain, err: %s", err) return nil, newError(responses.ErrorMissingParameter, "missing parameter gain, err: %s", err)
} }

View File

@ -8,7 +8,7 @@ import (
"github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/utils" "github.com/navidrome/navidrome/utils/req"
) )
func (api *Router) GetScanStatus(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetScanStatus(r *http.Request) (*responses.Subsonic, error) {
@ -41,7 +41,8 @@ func (api *Router) StartScan(r *http.Request) (*responses.Subsonic, error) {
return nil, newError(responses.ErrorAuthorizationFail) return nil, newError(responses.ErrorAuthorizationFail)
} }
fullScan := utils.ParamBool(r, "fullScan", false) p := req.Params(r)
fullScan := p.BoolOr("fullScan", false)
go func() { go func() {
start := time.Now() start := time.Now()

View File

@ -160,7 +160,7 @@ func (api *Router) Scrobble(r *http.Request) (*responses.Subsonic, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
times := p.Times("time") times, _ := p.Times("time")
if len(times) > 0 && len(times) != len(ids) { if len(times) > 0 && len(times) != len(ids) {
return nil, newError(responses.ErrorGeneric, "Wrong number of timestamps: %d, should be %d", len(times), len(ids)) return nil, newError(responses.ErrorGeneric, "Wrong number of timestamps: %d, should be %d", len(times), len(ids))
} }

View File

@ -15,15 +15,16 @@ import (
"github.com/navidrome/navidrome/resources" "github.com/navidrome/navidrome/resources"
"github.com/navidrome/navidrome/server/subsonic/filter" "github.com/navidrome/navidrome/server/subsonic/filter"
"github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/utils"
"github.com/navidrome/navidrome/utils/gravatar" "github.com/navidrome/navidrome/utils/gravatar"
"github.com/navidrome/navidrome/utils/req"
) )
func (api *Router) GetAvatar(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetAvatar(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
if !conf.Server.EnableGravatar { if !conf.Server.EnableGravatar {
return api.getPlaceHolderAvatar(w, r) return api.getPlaceHolderAvatar(w, r)
} }
username, err := requiredParamString(r, "username") p := req.Params(r)
username, err := p.String("username")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -61,8 +62,9 @@ func (api *Router) GetCoverArt(w http.ResponseWriter, r *http.Request) (*respons
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel() defer cancel()
id := utils.ParamString(r, "id") p := req.Params(r)
size := utils.ParamInt(r, "size", 0) id, _ := p.String("id")
size := p.IntOr("size", 0)
imgReader, lastUpdate, err := api.artwork.GetOrPlaceholder(ctx, id, size) imgReader, lastUpdate, err := api.artwork.GetOrPlaceholder(ctx, id, size)
w.Header().Set("cache-control", "public, max-age=315360000") w.Header().Set("cache-control", "public, max-age=315360000")
@ -99,8 +101,9 @@ func isSynced(rawLyrics string) bool {
} }
func (api *Router) GetLyrics(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetLyrics(r *http.Request) (*responses.Subsonic, error) {
artist := utils.ParamString(r, "artist") p := req.Params(r)
title := utils.ParamString(r, "title") artist, _ := p.String("artist")
title, _ := p.String("title")
response := newResponse() response := newResponse()
lyrics := responses.Lyrics{} lyrics := responses.Lyrics{}
response.Lyrics = &lyrics response.Lyrics = &lyrics

View File

@ -20,8 +20,8 @@ import (
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/utils"
. "github.com/navidrome/navidrome/utils/gg" . "github.com/navidrome/navidrome/utils/gg"
"github.com/navidrome/navidrome/utils/req"
) )
func postFormToQueryParams(next http.Handler) http.Handler { func postFormToQueryParams(next http.Handler) http.Handler {
@ -45,19 +45,18 @@ func postFormToQueryParams(next http.Handler) http.Handler {
func checkRequiredParameters(next http.Handler) http.Handler { func checkRequiredParameters(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requiredParameters := []string{"u", "v", "c"} requiredParameters := []string{"u", "v", "c"}
p := req.Params(r)
for _, p := range requiredParameters { for _, param := range requiredParameters {
if utils.ParamString(r, p) == "" { if _, err := p.String(param); err != nil {
msg := fmt.Sprintf(`Missing required parameter "%s"`, p) log.Warn(r, err)
log.Warn(r, msg) sendError(w, r, err)
sendError(w, r, newError(responses.ErrorMissingParameter, msg))
return return
} }
} }
username := utils.ParamString(r, "u") username, _ := p.String("u")
client := utils.ParamString(r, "c") client, _ := p.String("c")
version := utils.ParamString(r, "v") version, _ := p.String("v")
ctx := r.Context() ctx := r.Context()
ctx = request.WithUsername(ctx, username) ctx = request.WithUsername(ctx, username)
ctx = request.WithClient(ctx, client) ctx = request.WithClient(ctx, client)
@ -73,12 +72,13 @@ func authenticate(ds model.DataStore) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
username := utils.ParamString(r, "u") p := req.Params(r)
username, _ := p.String("u")
pass := utils.ParamString(r, "p") pass, _ := p.String("p")
token := utils.ParamString(r, "t") token, _ := p.String("t")
salt := utils.ParamString(r, "s") salt, _ := p.String("s")
jwt := utils.ParamString(r, "jwt") jwt, _ := p.String("jwt")
usr, err := validateUser(ctx, ds, username, pass, token, salt, jwt) usr, err := validateUser(ctx, ds, username, pass, token, salt, jwt)
if errors.Is(err, model.ErrInvalidAuth) { if errors.Is(err, model.ErrInvalidAuth) {

View File

@ -10,7 +10,7 @@ import (
"github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/utils" "github.com/navidrome/navidrome/utils/req"
) )
func (api *Router) GetPlaylists(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetPlaylists(r *http.Request) (*responses.Subsonic, error) {
@ -31,7 +31,8 @@ func (api *Router) GetPlaylists(r *http.Request) (*responses.Subsonic, error) {
func (api *Router) GetPlaylist(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetPlaylist(r *http.Request) (*responses.Subsonic, error) {
ctx := r.Context() ctx := r.Context()
id, err := requiredParamString(r, "id") p := req.Params(r)
id, err := p.String("id")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -84,9 +85,10 @@ func (api *Router) create(ctx context.Context, playlistId, name string, ids []st
func (api *Router) CreatePlaylist(r *http.Request) (*responses.Subsonic, error) { func (api *Router) CreatePlaylist(r *http.Request) (*responses.Subsonic, error) {
ctx := r.Context() ctx := r.Context()
songIds := utils.ParamStrings(r, "songId") p := req.Params(r)
playlistId := utils.ParamString(r, "playlistId") songIds, _ := p.Strings("songId")
name := utils.ParamString(r, "name") playlistId, _ := p.String("playlistId")
name, _ := p.String("name")
if playlistId == "" && name == "" { if playlistId == "" && name == "" {
return nil, errors.New("required parameter name is missing") return nil, errors.New("required parameter name is missing")
} }
@ -99,7 +101,8 @@ func (api *Router) CreatePlaylist(r *http.Request) (*responses.Subsonic, error)
} }
func (api *Router) DeletePlaylist(r *http.Request) (*responses.Subsonic, error) { func (api *Router) DeletePlaylist(r *http.Request) (*responses.Subsonic, error) {
id, err := requiredParamString(r, "id") p := req.Params(r)
id, err := p.String("id")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -115,23 +118,23 @@ func (api *Router) DeletePlaylist(r *http.Request) (*responses.Subsonic, error)
} }
func (api *Router) UpdatePlaylist(r *http.Request) (*responses.Subsonic, error) { func (api *Router) UpdatePlaylist(r *http.Request) (*responses.Subsonic, error) {
playlistId, err := requiredParamString(r, "playlistId") p := req.Params(r)
playlistId, err := p.String("playlistId")
if err != nil { if err != nil {
return nil, err return nil, err
} }
songsToAdd := utils.ParamStrings(r, "songIdToAdd") songsToAdd, _ := p.Strings("songIdToAdd")
songIndexesToRemove := utils.ParamInts(r, "songIndexToRemove") songIndexesToRemove, _ := p.Ints("songIndexToRemove")
var plsName *string var plsName *string
if s, ok := r.URL.Query()["name"]; ok { if s, err := p.String("name"); err == nil {
plsName = &s[0] plsName = &s
} }
var comment *string var comment *string
if c, ok := r.URL.Query()["comment"]; ok { if s, err := p.String("comment"); err == nil {
comment = &c[0] comment = &s
} }
var public *bool var public *bool
if _, ok := r.URL.Query()["public"]; ok { if p, err := p.Bool("public"); err == nil {
p := utils.ParamBool(r, "public", false)
public = &p public = &p
} }

View File

@ -5,21 +5,22 @@ import (
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/utils" "github.com/navidrome/navidrome/utils/req"
) )
func (api *Router) CreateInternetRadio(r *http.Request) (*responses.Subsonic, error) { func (api *Router) CreateInternetRadio(r *http.Request) (*responses.Subsonic, error) {
streamUrl, err := requiredParamString(r, "streamUrl") p := req.Params(r)
streamUrl, err := p.String("streamUrl")
if err != nil { if err != nil {
return nil, err return nil, err
} }
name, err := requiredParamString(r, "name") name, err := p.String("name")
if err != nil { if err != nil {
return nil, err return nil, err
} }
homepageUrl := utils.ParamString(r, "homepageUrl") homepageUrl, _ := p.String("homepageUrl")
ctx := r.Context() ctx := r.Context()
radio := &model.Radio{ radio := &model.Radio{
@ -36,7 +37,8 @@ func (api *Router) CreateInternetRadio(r *http.Request) (*responses.Subsonic, er
} }
func (api *Router) DeleteInternetRadio(r *http.Request) (*responses.Subsonic, error) { func (api *Router) DeleteInternetRadio(r *http.Request) (*responses.Subsonic, error) {
id, err := requiredParamString(r, "id") p := req.Params(r)
id, err := p.String("id")
if err != nil { if err != nil {
return nil, err return nil, err
@ -75,22 +77,23 @@ func (api *Router) GetInternetRadios(r *http.Request) (*responses.Subsonic, erro
} }
func (api *Router) UpdateInternetRadio(r *http.Request) (*responses.Subsonic, error) { func (api *Router) UpdateInternetRadio(r *http.Request) (*responses.Subsonic, error) {
id, err := requiredParamString(r, "id") p := req.Params(r)
id, err := p.String("id")
if err != nil { if err != nil {
return nil, err return nil, err
} }
streamUrl, err := requiredParamString(r, "streamUrl") streamUrl, err := p.String("streamUrl")
if err != nil { if err != nil {
return nil, err return nil, err
} }
name, err := requiredParamString(r, "name") name, err := p.String("name")
if err != nil { if err != nil {
return nil, err return nil, err
} }
homepageUrl := utils.ParamString(r, "homepageUrl") homepageUrl, _ := p.String("homepageUrl")
ctx := r.Context() ctx := r.Context()
radio := &model.Radio{ radio := &model.Radio{

View File

@ -14,7 +14,7 @@ import (
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/server/public" "github.com/navidrome/navidrome/server/public"
"github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/utils" "github.com/navidrome/navidrome/utils/req"
) )
type searchParams struct { type searchParams struct {
@ -28,18 +28,19 @@ type searchParams struct {
} }
func (api *Router) getParams(r *http.Request) (*searchParams, error) { func (api *Router) getParams(r *http.Request) (*searchParams, error) {
p := req.Params(r)
var err error var err error
sp := &searchParams{} sp := &searchParams{}
sp.query, err = requiredParamString(r, "query") sp.query, err = p.String("query")
if err != nil { if err != nil {
return nil, err return nil, err
} }
sp.artistCount = utils.ParamInt(r, "artistCount", 20) sp.artistCount = p.IntOr("artistCount", 20)
sp.artistOffset = utils.ParamInt(r, "artistOffset", 0) sp.artistOffset = p.IntOr("artistOffset", 0)
sp.albumCount = utils.ParamInt(r, "albumCount", 20) sp.albumCount = p.IntOr("albumCount", 20)
sp.albumOffset = utils.ParamInt(r, "albumOffset", 0) sp.albumOffset = p.IntOr("albumOffset", 0)
sp.songCount = utils.ParamInt(r, "songCount", 20) sp.songCount = p.IntOr("songCount", 20)
sp.songOffset = utils.ParamInt(r, "songOffset", 0) sp.songOffset = p.IntOr("songOffset", 0)
return sp, nil return sp, nil
} }

View File

@ -9,7 +9,7 @@ import (
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/server/public" "github.com/navidrome/navidrome/server/public"
"github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/utils" "github.com/navidrome/navidrome/utils/req"
) )
func (api *Router) GetShares(r *http.Request) (*responses.Subsonic, error) { func (api *Router) GetShares(r *http.Request) (*responses.Subsonic, error) {
@ -50,13 +50,14 @@ func (api *Router) buildShare(r *http.Request, share model.Share) responses.Shar
} }
func (api *Router) CreateShare(r *http.Request) (*responses.Subsonic, error) { func (api *Router) CreateShare(r *http.Request) (*responses.Subsonic, error) {
ids := utils.ParamStrings(r, "id") p := req.Params(r)
if len(ids) == 0 { ids, err := p.Strings("id")
return nil, newError(responses.ErrorMissingParameter, "Required id parameter is missing") if err != nil {
return nil, err
} }
description := utils.ParamString(r, "description") description, _ := p.String("description")
expires := utils.ParamTime(r, "expires", time.Time{}) expires := p.TimeOr("expires", time.Time{})
repo := api.share.NewRepository(r.Context()) repo := api.share.NewRepository(r.Context())
share := &model.Share{ share := &model.Share{
@ -81,13 +82,14 @@ func (api *Router) CreateShare(r *http.Request) (*responses.Subsonic, error) {
} }
func (api *Router) UpdateShare(r *http.Request) (*responses.Subsonic, error) { func (api *Router) UpdateShare(r *http.Request) (*responses.Subsonic, error) {
id := utils.ParamString(r, "id") p := req.Params(r)
if id == "" { id, err := p.String("id")
return nil, newError(responses.ErrorMissingParameter, "Required id parameter is missing") if err != nil {
return nil, err
} }
description := utils.ParamString(r, "description") description, _ := p.String("description")
expires := utils.ParamTime(r, "expires", time.Time{}) expires := p.TimeOr("expires", time.Time{})
repo := api.share.NewRepository(r.Context()) repo := api.share.NewRepository(r.Context())
share := &model.Share{ share := &model.Share{
@ -96,7 +98,7 @@ func (api *Router) UpdateShare(r *http.Request) (*responses.Subsonic, error) {
ExpiresAt: expires, ExpiresAt: expires,
} }
err := repo.(rest.Persistable).Update(id, share) err = repo.(rest.Persistable).Update(id, share)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -105,13 +107,14 @@ func (api *Router) UpdateShare(r *http.Request) (*responses.Subsonic, error) {
} }
func (api *Router) DeleteShare(r *http.Request) (*responses.Subsonic, error) { func (api *Router) DeleteShare(r *http.Request) (*responses.Subsonic, error) {
id := utils.ParamString(r, "id") p := req.Params(r)
if id == "" { id, err := p.String("id")
return nil, newError(responses.ErrorMissingParameter, "Required id parameter is missing") if err != nil {
return nil, err
} }
repo := api.share.NewRepository(r.Context()) repo := api.share.NewRepository(r.Context())
err := repo.(rest.Persistable).Delete(id) err = repo.(rest.Persistable).Delete(id)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -14,7 +14,7 @@ import (
"github.com/navidrome/navidrome/model" "github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request" "github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/server/subsonic/responses" "github.com/navidrome/navidrome/server/subsonic/responses"
"github.com/navidrome/navidrome/utils" "github.com/navidrome/navidrome/utils/req"
) )
func (api *Router) serveStream(ctx context.Context, w http.ResponseWriter, r *http.Request, stream *core.Stream, id string) { func (api *Router) serveStream(ctx context.Context, w http.ResponseWriter, r *http.Request, stream *core.Stream, id string) {
@ -25,7 +25,7 @@ func (api *Router) serveStream(ctx context.Context, w http.ResponseWriter, r *ht
w.Header().Set("Accept-Ranges", "none") w.Header().Set("Accept-Ranges", "none")
w.Header().Set("Content-Type", stream.ContentType()) w.Header().Set("Content-Type", stream.ContentType())
estimateContentLength := utils.ParamBool(r, "estimateContentLength", false) estimateContentLength := req.Params(r).BoolOr("estimateContentLength", false)
// if Client requests the estimated content-length, send it // if Client requests the estimated content-length, send it
if estimateContentLength { if estimateContentLength {
@ -51,13 +51,14 @@ func (api *Router) serveStream(ctx context.Context, w http.ResponseWriter, r *ht
func (api *Router) Stream(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { func (api *Router) Stream(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
ctx := r.Context() ctx := r.Context()
id, err := requiredParamString(r, "id") p := req.Params(r)
id, err := p.String("id")
if err != nil { if err != nil {
return nil, err return nil, err
} }
maxBitRate := utils.ParamInt(r, "maxBitRate", 0) maxBitRate := p.IntOr("maxBitRate", 0)
format := utils.ParamString(r, "format") format, _ := p.String("format")
timeOffset := utils.ParamInt(r, "timeOffset", 0) timeOffset := p.IntOr("timeOffset", 0)
stream, err := api.streamer.NewStream(ctx, id, format, maxBitRate, timeOffset) stream, err := api.streamer.NewStream(ctx, id, format, maxBitRate, timeOffset)
if err != nil { if err != nil {
@ -82,7 +83,8 @@ func (api *Router) Stream(w http.ResponseWriter, r *http.Request) (*responses.Su
func (api *Router) Download(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) { func (api *Router) Download(w http.ResponseWriter, r *http.Request) (*responses.Subsonic, error) {
ctx := r.Context() ctx := r.Context()
username, _ := request.UsernameFrom(ctx) username, _ := request.UsernameFrom(ctx)
id, err := requiredParamString(r, "id") p := req.Params(r)
id, err := p.String("id")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -97,8 +99,8 @@ func (api *Router) Download(w http.ResponseWriter, r *http.Request) (*responses.
return nil, err return nil, err
} }
maxBitRate := utils.ParamInt(r, "bitrate", 0) maxBitRate := p.IntOr("bitrate", 0)
format := utils.ParamString(r, "format") format, _ := p.String("format")
if format == "" { if format == "" {
if conf.Server.AutoTranscodeDownload { if conf.Server.AutoTranscodeDownload {

View File

@ -68,8 +68,11 @@ func (r *Values) TimeOr(param string, def time.Time) time.Time {
return t return t
} }
func (r *Values) Times(param string) []time.Time { func (r *Values) Times(param string) ([]time.Time, error) {
pStr, _ := r.Strings(param) pStr, err := r.Strings(param)
if err != nil {
return nil, err
}
times := make([]time.Time, len(pStr)) times := make([]time.Time, len(pStr))
for i, t := range pStr { for i, t := range pStr {
ti, err := strconv.ParseInt(t, 10, 64) ti, err := strconv.ParseInt(t, 10, 64)
@ -80,7 +83,7 @@ func (r *Values) Times(param string) []time.Time {
} }
times[i] = utils.ToTime(ti) times[i] = utils.ToTime(ti)
} }
return times return times, nil
} }
func (r *Values) Int64(param string) (int64, error) { func (r *Values) Int64(param string) (int64, error) {
@ -119,8 +122,11 @@ func (r *Values) Int64Or(param string, def int64) int64 {
return v return v
} }
func (r *Values) Ints(param string) []int { func (r *Values) Ints(param string) ([]int, error) {
pStr, _ := r.Strings(param) pStr, err := r.Strings(param)
if err != nil {
return nil, err
}
ints := make([]int, 0, len(pStr)) ints := make([]int, 0, len(pStr))
for _, s := range pStr { for _, s := range pStr {
i, err := strconv.ParseInt(s, 10, 64) i, err := strconv.ParseInt(s, 10, 64)
@ -128,13 +134,21 @@ func (r *Values) Ints(param string) []int {
ints = append(ints, int(i)) ints = append(ints, int(i))
} }
} }
return ints return ints, nil
}
func (r *Values) Bool(param string) (bool, error) {
v, err := r.String(param)
if err != nil {
return false, err
}
return strings.Contains("/true/on/1/", "/"+strings.ToLower(v)+"/"), nil
} }
func (r *Values) BoolOr(param string, def bool) bool { func (r *Values) BoolOr(param string, def bool) bool {
v, _ := r.String(param) v, err := r.Bool(param)
if v == "" { if err != nil {
return def return def
} }
return strings.Contains("/true/on/1/", "/"+strings.ToLower(v)+"/") return v
} }

View File

@ -102,13 +102,16 @@ var _ = Describe("Request Helpers", func() {
}) })
It("returns empty string if param does not exist", func() { It("returns empty string if param does not exist", func() {
Expect(r.Times("xx")).To(BeEmpty()) v, err := r.Times("xx")
Expect(err).To(MatchError(req.ErrMissingParam))
Expect(v).To(BeEmpty())
}) })
It("returns current time as default if param is invalid", func() { It("returns current time as default if param is invalid", func() {
now := time.Now() now := time.Now()
r = req.Params(httptest.NewRequest("GET", "/ping?t=null", nil)) r = req.Params(httptest.NewRequest("GET", "/ping?t=null", nil))
times := r.Times("t") times, err := r.Times("t")
Expect(err).ToNot(HaveOccurred())
Expect(times).To(HaveLen(1)) Expect(times).To(HaveLen(1))
Expect(times[0]).To(BeTemporally(">=", now)) Expect(times[0]).To(BeTemporally(">=", now))
}) })
@ -130,18 +133,28 @@ var _ = Describe("Request Helpers", func() {
It("returns default value if param is an invalid int", func() { It("returns default value if param is an invalid int", func() {
Expect(r.IntOr("inv", 999)).To(Equal(999)) Expect(r.IntOr("inv", 999)).To(Equal(999))
}) })
It("returns error if param is an invalid int", func() {
_, err := r.Int("inv")
Expect(err).To(MatchError(req.ErrInvalidParam))
})
}) })
Context("int64", func() { Context("int64", func() {
It("returns parsed int64", func() { It("returns parsed int64", func() {
Expect(r.IntOr("i", 999)).To(Equal(123)) Expect(r.Int64Or("i", 999)).To(Equal(int64(123)))
}) })
It("returns default value if param does not exist", func() { It("returns default value if param does not exist", func() {
Expect(r.IntOr("xx", 999)).To(Equal(999)) Expect(r.Int64Or("xx", 999)).To(Equal(int64(999)))
}) })
It("returns default value if param is an invalid int", func() { It("returns default value if param is an invalid int", func() {
Expect(r.IntOr("inv", 999)).To(Equal(999)) Expect(r.Int64Or("inv", 999)).To(Equal(int64(999)))
})
It("returns error if param is an invalid int", func() {
_, err := r.Int64("inv")
Expect(err).To(MatchError(req.ErrInvalidParam))
}) })
}) })
}) })
@ -156,7 +169,9 @@ var _ = Describe("Request Helpers", func() {
}) })
It("returns empty array if param does not exist", func() { It("returns empty array if param does not exist", func() {
Expect(r.Ints("xx")).To(BeEmpty()) v, err := r.Ints("xx")
Expect(err).To(MatchError(req.ErrMissingParam))
Expect(v).To(BeEmpty())
}) })
}) })

View File

@ -1,90 +0,0 @@
package utils
import (
"net/http"
"strconv"
"strings"
"time"
"github.com/navidrome/navidrome/log"
"golang.org/x/exp/constraints"
)
func ParamString(r *http.Request, param string) string {
return r.URL.Query().Get(param)
}
func ParamStringDefault(r *http.Request, param, def string) string {
v := ParamString(r, param)
if v == "" {
return def
}
return v
}
func ParamStrings(r *http.Request, param string) []string {
return r.URL.Query()[param]
}
func ParamTimes(r *http.Request, param string) []time.Time {
pStr := ParamStrings(r, param)
times := make([]time.Time, len(pStr))
for i, t := range pStr {
ti, err := strconv.ParseInt(t, 10, 64)
if err != nil {
log.Warn(r.Context(), "Ignoring invalid time param", "time", t, err)
times[i] = time.Now()
continue
}
times[i] = ToTime(ti)
}
return times
}
func ParamTime(r *http.Request, param string, def time.Time) time.Time {
v := ParamString(r, param)
if v == "" || v == "-1" {
return def
}
value, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return def
}
t := ToTime(value)
if t.Before(time.Date(1970, time.January, 2, 0, 0, 0, 0, time.UTC)) {
return def
}
return t
}
func ParamInt[T constraints.Integer](r *http.Request, param string, def T) T {
v := ParamString(r, param)
if v == "" {
return def
}
value, err := strconv.ParseInt(v, 10, 64)
if err != nil {
return def
}
return T(value)
}
func ParamInts(r *http.Request, param string) []int {
pStr := ParamStrings(r, param)
ints := make([]int, 0, len(pStr))
for _, s := range pStr {
i, err := strconv.ParseInt(s, 10, 32)
if err == nil {
ints = append(ints, int(i))
}
}
return ints
}
func ParamBool(r *http.Request, param string, def bool) bool {
p := strings.ToLower(ParamString(r, param))
if p == "" {
return def
}
return strings.Contains("/true/on/1/", "/"+p+"/")
}

View File

@ -1,196 +0,0 @@
package utils
import (
"fmt"
"net/http"
"net/http/httptest"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
var _ = Describe("Request Helpers", func() {
var r *http.Request
Describe("ParamString", func() {
BeforeEach(func() {
r = httptest.NewRequest("GET", "/ping?a=123", nil)
})
It("returns empty string if param does not exist", func() {
Expect(ParamString(r, "xx")).To(Equal(""))
})
It("returns param as string", func() {
Expect(ParamString(r, "a")).To(Equal("123"))
})
})
Describe("ParamStringDefault", func() {
BeforeEach(func() {
r = httptest.NewRequest("GET", "/ping?a=123", nil)
})
It("returns default string if param does not exist", func() {
Expect(ParamStringDefault(r, "xx", "default_value")).To(Equal("default_value"))
})
It("returns param as string", func() {
Expect(ParamStringDefault(r, "a", "default_value")).To(Equal("123"))
})
})
Describe("ParamStrings", func() {
BeforeEach(func() {
r = httptest.NewRequest("GET", "/ping?a=123&a=456", nil)
})
It("returns empty array if param does not exist", func() {
Expect(ParamStrings(r, "xx")).To(BeEmpty())
})
It("returns all param occurrences as []string", func() {
Expect(ParamStrings(r, "a")).To(Equal([]string{"123", "456"}))
})
})
Describe("ParamTime", func() {
d := time.Date(2002, 8, 9, 12, 11, 13, 1000000, time.Local)
t := ToMillis(d)
now := time.Now()
BeforeEach(func() {
r = httptest.NewRequest("GET", fmt.Sprintf("/ping?t=%d&inv=abc", t), nil)
})
It("returns default time if param does not exist", func() {
Expect(ParamTime(r, "xx", now)).To(Equal(now))
})
It("returns default time if param is an invalid timestamp", func() {
Expect(ParamTime(r, "inv", now)).To(Equal(now))
})
It("returns parsed time", func() {
Expect(ParamTime(r, "t", now)).To(Equal(d))
})
})
Describe("ParamTimes", func() {
d1 := time.Date(2002, 8, 9, 12, 11, 13, 1000000, time.Local)
d2 := time.Date(2002, 8, 9, 12, 13, 56, 0000000, time.Local)
t1 := ToMillis(d1)
t2 := ToMillis(d2)
BeforeEach(func() {
r = httptest.NewRequest("GET", fmt.Sprintf("/ping?t=%d&t=%d", t1, t2), nil)
})
It("returns empty string if param does not exist", func() {
Expect(ParamTimes(r, "xx")).To(BeEmpty())
})
It("returns all param occurrences as []time.Time", func() {
Expect(ParamTimes(r, "t")).To(Equal([]time.Time{d1, d2}))
})
It("returns current time as default if param is invalid", func() {
now := time.Now()
r = httptest.NewRequest("GET", "/ping?t=null", nil)
times := ParamTimes(r, "t")
Expect(times).To(HaveLen(1))
Expect(times[0]).To(BeTemporally(">=", now))
})
})
Describe("ParamInt", func() {
BeforeEach(func() {
r = httptest.NewRequest("GET", "/ping?i=123&inv=123.45", nil)
})
Context("int", func() {
It("returns default value if param does not exist", func() {
Expect(ParamInt(r, "xx", 999)).To(Equal(999))
})
It("returns default value if param is an invalid int", func() {
Expect(ParamInt(r, "inv", 999)).To(Equal(999))
})
It("returns parsed time", func() {
Expect(ParamInt(r, "i", 999)).To(Equal(123))
})
})
Context("int64", func() {
It("returns default value if param does not exist", func() {
Expect(ParamInt(r, "xx", int64(999))).To(Equal(int64(999)))
})
It("returns default value if param is an invalid int", func() {
Expect(ParamInt(r, "inv", int64(999))).To(Equal(int64(999)))
})
It("returns parsed time", func() {
Expect(ParamInt(r, "i", int64(999))).To(Equal(int64(123)))
})
})
})
Describe("ParamInts", func() {
BeforeEach(func() {
r = httptest.NewRequest("GET", "/ping?i=123&i=456", nil)
})
It("returns empty array if param does not exist", func() {
Expect(ParamInts(r, "xx")).To(BeEmpty())
})
It("returns array of occurrences found", func() {
Expect(ParamInts(r, "i")).To(Equal([]int{123, 456}))
})
})
Describe("ParamBool", func() {
Context("value is true", func() {
BeforeEach(func() {
r = httptest.NewRequest("GET", "/ping?b=true&c=on&d=1&e=True", nil)
})
It("parses 'true'", func() {
Expect(ParamBool(r, "b", false)).To(BeTrue())
})
It("parses 'on'", func() {
Expect(ParamBool(r, "c", false)).To(BeTrue())
})
It("parses '1'", func() {
Expect(ParamBool(r, "d", false)).To(BeTrue())
})
It("parses 'True'", func() {
Expect(ParamBool(r, "e", false)).To(BeTrue())
})
})
Context("value is false", func() {
BeforeEach(func() {
r = httptest.NewRequest("GET", "/ping?b=false&c=off&d=0", nil)
})
It("returns default value if param does not exist", func() {
Expect(ParamBool(r, "xx", true)).To(BeTrue())
})
It("parses 'false'", func() {
Expect(ParamBool(r, "b", true)).To(BeFalse())
})
It("parses 'off'", func() {
Expect(ParamBool(r, "c", true)).To(BeFalse())
})
It("parses '0'", func() {
Expect(ParamBool(r, "d", true)).To(BeFalse())
})
})
})
})