mirror of
https://github.com/navidrome/navidrome.git
synced 2026-07-31 01:06:23 -04:00
* refactor(req): drop redundant error return from Strings
The error from Strings carried no information beyond emptiness — it fired
exactly when the param was absent — and nearly every caller discarded it with
a blank identifier. Strings now just returns the values (empty when absent),
making the common optional-list reads one clean expression.
The few required-param callers (scrobble, createShare) check for emptiness and
return the same Subsonic error code 10 as before; their e2e tests now pin that
code. Ints and Times keep their contracts by synthesizing ErrMissingParam
themselves, so selectedMusicFolderIds is untouched. The jellyfin parseFields
helper is inlined away, since ParseFields(p.Strings("fields")...) now
compiles directly.
* docs(req): clarify Strings returns nil when param is absent
242 lines
7.2 KiB
Go
242 lines
7.2 KiB
Go
package nativeapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/deluan/rest"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/navidrome/navidrome/core/playlists"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/utils/req"
|
|
)
|
|
|
|
type restHandler = func(rest.RepositoryConstructor, ...rest.Logger) http.HandlerFunc
|
|
|
|
func playlistTracksHandler(pls playlists.Playlists, handler restHandler, refreshSmartPlaylist func(*http.Request) bool) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
plsId := chi.URLParam(r, "playlistId")
|
|
tracks := pls.TracksRepository(r.Context(), plsId, refreshSmartPlaylist(r))
|
|
if tracks == nil {
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
handler(func(ctx context.Context) rest.Repository { return tracks }).ServeHTTP(w, r)
|
|
}
|
|
}
|
|
|
|
func getPlaylist(pls playlists.Playlists) http.HandlerFunc {
|
|
handler := playlistTracksHandler(pls, rest.GetAll, func(r *http.Request) bool {
|
|
return req.Params(r).Int64Or("_start", 0) == 0
|
|
})
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.ToLower(r.Header.Get("accept")) == "audio/x-mpegurl" {
|
|
handleExportPlaylist(pls)(w, r)
|
|
return
|
|
}
|
|
handler(w, r)
|
|
}
|
|
}
|
|
|
|
func getPlaylistTrack(pls playlists.Playlists) http.HandlerFunc {
|
|
return playlistTracksHandler(pls, rest.Get, func(*http.Request) bool { return true })
|
|
}
|
|
|
|
func createPlaylistFromM3U(pls playlists.Playlists) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
pl, err := pls.ImportM3U(ctx, r.Body)
|
|
if err != nil {
|
|
log.Error(r.Context(), "Error parsing playlist", err)
|
|
// TODO: consider returning StatusBadRequest for playlists that are malformed
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusCreated)
|
|
_, err = w.Write([]byte(pl.ToM3U8())) //nolint:gosec
|
|
if err != nil {
|
|
log.Error(ctx, "Error sending m3u contents", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func handleExportPlaylist(pls playlists.Playlists) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
plsId := chi.URLParam(r, "playlistId")
|
|
playlist, err := pls.GetWithTracks(ctx, plsId)
|
|
if errors.Is(err, model.ErrNotFound) {
|
|
log.Warn(ctx, "Playlist not found", "playlistId", plsId)
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if err != nil {
|
|
log.Error(ctx, "Error retrieving the playlist", "playlistId", plsId, err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
log.Debug(ctx, "Exporting playlist as M3U", "playlistId", plsId, "name", playlist.Name)
|
|
w.Header().Set("Content-Type", "audio/x-mpegurl")
|
|
disposition := fmt.Sprintf("attachment; filename=\"%s.m3u\"", playlist.Name)
|
|
w.Header().Set("Content-Disposition", disposition)
|
|
|
|
_, err = w.Write([]byte(playlist.ToM3U8())) //nolint:gosec
|
|
if err != nil {
|
|
log.Error(ctx, "Error sending playlist", "name", playlist.Name)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func deleteFromPlaylist(pls playlists.Playlists) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
p := req.Params(r)
|
|
playlistId, _ := p.String(":playlistId")
|
|
ids := p.Strings("id")
|
|
err := pls.RemoveTracks(r.Context(), playlistId, ids)
|
|
if len(ids) == 1 && errors.Is(err, model.ErrNotFound) {
|
|
log.Warn(r.Context(), "Track not found in playlist", "playlistId", playlistId, "id", ids[0])
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
if err != nil {
|
|
log.Error(r.Context(), "Error deleting tracks from playlist", "playlistId", playlistId, "ids", ids, err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeDeleteManyResponse(w, r, ids)
|
|
}
|
|
}
|
|
|
|
func addToPlaylist(pls playlists.Playlists) http.HandlerFunc {
|
|
type addTracksPayload struct {
|
|
Ids []string `json:"ids"`
|
|
AlbumIds []string `json:"albumIds"`
|
|
ArtistIds []string `json:"artistIds"`
|
|
Discs []model.DiscID `json:"discs"`
|
|
}
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
p := req.Params(r)
|
|
playlistId, _ := p.String(":playlistId")
|
|
var payload addTracksPayload
|
|
err := json.NewDecoder(r.Body).Decode(&payload)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
count, c := 0, 0
|
|
if c, err = pls.AddTracks(ctx, playlistId, payload.Ids); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
count += c
|
|
if c, err = pls.AddAlbums(ctx, playlistId, payload.AlbumIds); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
count += c
|
|
if c, err = pls.AddArtists(ctx, playlistId, payload.ArtistIds); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
count += c
|
|
if c, err = pls.AddDiscs(ctx, playlistId, payload.Discs); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
count += c
|
|
|
|
// Must return an object with an ID, to satisfy ReactAdmin `create` call
|
|
_, err = fmt.Fprintf(w, `{"added":%d}`, count) //nolint:gosec
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
}
|
|
|
|
func reorderItem(pls playlists.Playlists) http.HandlerFunc {
|
|
type reorderPayload struct {
|
|
InsertBefore string `json:"insert_before"`
|
|
}
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
p := req.Params(r)
|
|
playlistId, _ := p.String(":playlistId")
|
|
id := p.IntOr(":id", 0)
|
|
if id == 0 {
|
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
var payload reorderPayload
|
|
err := json.NewDecoder(r.Body).Decode(&payload)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
newPos, err := strconv.Atoi(payload.InsertBefore)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
err = pls.ReorderTrack(ctx, playlistId, id, newPos)
|
|
if errors.Is(err, model.ErrNotAuthorized) {
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
return
|
|
}
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
_, err = w.Write(fmt.Appendf(nil, `{"id":"%d"}`, id)) //nolint:gosec
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
}
|
|
}
|
|
|
|
func getSongPlaylists(svc playlists.Playlists) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
p := req.Params(r)
|
|
trackId, _ := p.String(":id")
|
|
playlists, err := svc.GetPlaylists(r.Context(), trackId)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
data, err := json.Marshal(playlists)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
_, _ = w.Write(data) //nolint:gosec
|
|
}
|
|
}
|
|
|
|
func uploadPlaylistImage(pls playlists.Playlists) http.HandlerFunc {
|
|
return handleImageUpload(func(ctx context.Context, reader io.Reader, ext string) error {
|
|
playlistId := chi.URLParamFromCtx(ctx, "id")
|
|
return pls.SetImage(ctx, playlistId, reader, ext)
|
|
})
|
|
}
|
|
|
|
func deletePlaylistImage(pls playlists.Playlists) http.HandlerFunc {
|
|
return handleImageDelete(func(ctx context.Context) error {
|
|
playlistId := chi.URLParamFromCtx(ctx, "id")
|
|
return pls.RemoveImage(ctx, playlistId)
|
|
})
|
|
}
|