update
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled
This commit is contained in:
244
server/nativeapi/playlists.go
Normal file
244
server/nativeapi/playlists.go
Normal file
@@ -0,0 +1,244 @@
|
||||
package nativeapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/deluan/rest"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/navidrome/navidrome/core"
|
||||
"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 getPlaylist(ds model.DataStore) http.HandlerFunc {
|
||||
// Add a middleware to capture the playlistId
|
||||
wrapper := func(handler restHandler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
constructor := func(ctx context.Context) rest.Repository {
|
||||
plsRepo := ds.Playlist(ctx)
|
||||
plsId := chi.URLParam(r, "playlistId")
|
||||
p := req.Params(r)
|
||||
start := p.Int64Or("_start", 0)
|
||||
return plsRepo.Tracks(plsId, start == 0)
|
||||
}
|
||||
|
||||
handler(constructor).ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
accept := r.Header.Get("accept")
|
||||
if strings.ToLower(accept) == "audio/x-mpegurl" {
|
||||
handleExportPlaylist(ds)(w, r)
|
||||
return
|
||||
}
|
||||
wrapper(rest.GetAll)(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func getPlaylistTrack(ds model.DataStore) http.HandlerFunc {
|
||||
// Add a middleware to capture the playlistId
|
||||
wrapper := func(handler restHandler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
constructor := func(ctx context.Context) rest.Repository {
|
||||
plsRepo := ds.Playlist(ctx)
|
||||
plsId := chi.URLParam(r, "playlistId")
|
||||
return plsRepo.Tracks(plsId, true)
|
||||
}
|
||||
|
||||
handler(constructor).ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
return wrapper(rest.Get)
|
||||
}
|
||||
|
||||
func createPlaylistFromM3U(playlists core.Playlists) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
pls, err := playlists.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(pls.ToM3U8()))
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error sending m3u contents", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleExportPlaylist(ds model.DataStore) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
plsRepo := ds.Playlist(ctx)
|
||||
plsId := chi.URLParam(r, "playlistId")
|
||||
pls, err := plsRepo.GetWithTracks(plsId, true, false)
|
||||
if errors.Is(err, model.ErrNotFound) {
|
||||
log.Warn(r.Context(), "Playlist not found", "playlistId", plsId)
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Error(r.Context(), "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", pls.Name)
|
||||
w.Header().Set("Content-Type", "audio/x-mpegurl")
|
||||
disposition := fmt.Sprintf("attachment; filename=\"%s.m3u\"", pls.Name)
|
||||
w.Header().Set("Content-Disposition", disposition)
|
||||
|
||||
_, err = w.Write([]byte(pls.ToM3U8()))
|
||||
if err != nil {
|
||||
log.Error(ctx, "Error sending playlist", "name", pls.Name)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteFromPlaylist(ds model.DataStore) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
p := req.Params(r)
|
||||
playlistId, _ := p.String(":playlistId")
|
||||
ids, _ := p.Strings("id")
|
||||
err := ds.WithTxImmediate(func(tx model.DataStore) error {
|
||||
tracksRepo := tx.Playlist(r.Context()).Tracks(playlistId, true)
|
||||
return tracksRepo.Delete(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(ds model.DataStore) 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) {
|
||||
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
|
||||
}
|
||||
tracksRepo := ds.Playlist(r.Context()).Tracks(playlistId, true)
|
||||
count, c := 0, 0
|
||||
if c, err = tracksRepo.Add(payload.Ids); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
count += c
|
||||
if c, err = tracksRepo.AddAlbums(payload.AlbumIds); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
count += c
|
||||
if c, err = tracksRepo.AddArtists(payload.ArtistIds); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
count += c
|
||||
if c, err = tracksRepo.AddDiscs(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)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reorderItem(ds model.DataStore) http.HandlerFunc {
|
||||
type reorderPayload struct {
|
||||
InsertBefore string `json:"insert_before"`
|
||||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
}
|
||||
tracksRepo := ds.Playlist(r.Context()).Tracks(playlistId, true)
|
||||
err = tracksRepo.Reorder(id, newPos)
|
||||
if errors.Is(err, rest.ErrPermissionDenied) {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = w.Write([]byte(fmt.Sprintf(`{"id":"%d"}`, id)))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getSongPlaylists(ds model.DataStore) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
p := req.Params(r)
|
||||
trackId, _ := p.String(":id")
|
||||
playlists, err := ds.Playlist(r.Context()).GetPlaylists(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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user