mirror of
https://github.com/navidrome/navidrome.git
synced 2025-07-14 15:41:18 +03:00
* Refactor queue payload handling * Refine queue update validation * refactor(queue): avoid loading tracks for validation * refactor/rename repository methods Signed-off-by: Deluan <deluan@navidrome.org> * more tests Signed-off-by: Deluan <deluan@navidrome.org> * refactor Signed-off-by: Deluan <deluan@navidrome.org> --------- Signed-off-by: Deluan <deluan@navidrome.org>
215 lines
6.5 KiB
Go
215 lines
6.5 KiB
Go
package nativeapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
"github.com/navidrome/navidrome/model/request"
|
|
. "github.com/navidrome/navidrome/utils/gg"
|
|
"github.com/navidrome/navidrome/utils/slice"
|
|
)
|
|
|
|
type updateQueuePayload struct {
|
|
Ids *[]string `json:"ids,omitempty"`
|
|
Current *int `json:"current,omitempty"`
|
|
Position *int64 `json:"position,omitempty"`
|
|
}
|
|
|
|
// validateCurrentIndex validates that the current index is within bounds of the items array.
|
|
// Returns false if validation fails (and sends error response), true if validation passes.
|
|
func validateCurrentIndex(w http.ResponseWriter, current int, itemsLength int) bool {
|
|
if current < 0 || current >= itemsLength {
|
|
http.Error(w, "current index out of bounds", http.StatusBadRequest)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// retrieveExistingQueue retrieves an existing play queue for a user with proper error handling.
|
|
// Returns the queue (nil if not found) and false if an error occurred and response was sent.
|
|
func retrieveExistingQueue(ctx context.Context, w http.ResponseWriter, ds model.DataStore, userID string) (*model.PlayQueue, bool) {
|
|
existing, err := ds.PlayQueue(ctx).Retrieve(userID)
|
|
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
|
log.Error(ctx, "Error retrieving queue", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return nil, false
|
|
}
|
|
return existing, true
|
|
}
|
|
|
|
// decodeUpdatePayload decodes the JSON payload from the request body.
|
|
// Returns false if decoding fails (and sends error response), true if successful.
|
|
func decodeUpdatePayload(w http.ResponseWriter, r *http.Request) (*updateQueuePayload, bool) {
|
|
var payload updateQueuePayload
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return nil, false
|
|
}
|
|
return &payload, true
|
|
}
|
|
|
|
// createMediaFileItems converts a slice of IDs to MediaFile items.
|
|
func createMediaFileItems(ids []string) []model.MediaFile {
|
|
return slice.Map(ids, func(id string) model.MediaFile {
|
|
return model.MediaFile{ID: id}
|
|
})
|
|
}
|
|
|
|
// extractUserAndClient extracts user and client from the request context.
|
|
func extractUserAndClient(ctx context.Context) (model.User, string) {
|
|
user, _ := request.UserFrom(ctx)
|
|
client, _ := request.ClientFrom(ctx)
|
|
return user, client
|
|
}
|
|
|
|
func getQueue(ds model.DataStore) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
user, _ := request.UserFrom(ctx)
|
|
repo := ds.PlayQueue(ctx)
|
|
pq, err := repo.RetrieveWithMediaFiles(user.ID)
|
|
if err != nil && !errors.Is(err, model.ErrNotFound) {
|
|
log.Error(ctx, "Error retrieving queue", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if pq == nil {
|
|
pq = &model.PlayQueue{}
|
|
}
|
|
resp, err := json.Marshal(pq)
|
|
if err != nil {
|
|
log.Error(ctx, "Error marshalling queue", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write(resp)
|
|
}
|
|
}
|
|
|
|
func saveQueue(ds model.DataStore) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
payload, ok := decodeUpdatePayload(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
user, client := extractUserAndClient(ctx)
|
|
ids := V(payload.Ids)
|
|
items := createMediaFileItems(ids)
|
|
current := V(payload.Current)
|
|
if len(ids) > 0 && !validateCurrentIndex(w, current, len(ids)) {
|
|
return
|
|
}
|
|
pq := &model.PlayQueue{
|
|
UserID: user.ID,
|
|
Current: current,
|
|
Position: max(V(payload.Position), 0),
|
|
ChangedBy: client,
|
|
Items: items,
|
|
}
|
|
if err := ds.PlayQueue(ctx).Store(pq); err != nil {
|
|
log.Error(ctx, "Error saving queue", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
func updateQueue(ds model.DataStore) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
|
|
// Decode and validate the JSON payload
|
|
payload, ok := decodeUpdatePayload(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
// Extract user and client information from request context
|
|
user, client := extractUserAndClient(ctx)
|
|
|
|
// Initialize play queue with user ID and client info
|
|
pq := &model.PlayQueue{UserID: user.ID, ChangedBy: client}
|
|
var cols []string // Track which columns to update in the database
|
|
|
|
// Handle queue items update
|
|
if payload.Ids != nil {
|
|
pq.Items = createMediaFileItems(*payload.Ids)
|
|
cols = append(cols, "items")
|
|
|
|
// If current index is not being updated, validate existing current index
|
|
// against the new items list to ensure it remains valid
|
|
if payload.Current == nil {
|
|
existing, ok := retrieveExistingQueue(ctx, w, ds, user.ID)
|
|
if !ok {
|
|
return
|
|
}
|
|
if existing != nil && !validateCurrentIndex(w, existing.Current, len(*payload.Ids)) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle current track index update
|
|
if payload.Current != nil {
|
|
pq.Current = *payload.Current
|
|
cols = append(cols, "current")
|
|
|
|
if payload.Ids != nil {
|
|
// If items are also being updated, validate current index against new items
|
|
if !validateCurrentIndex(w, *payload.Current, len(*payload.Ids)) {
|
|
return
|
|
}
|
|
} else {
|
|
// If only current index is being updated, validate against existing items
|
|
existing, ok := retrieveExistingQueue(ctx, w, ds, user.ID)
|
|
if !ok {
|
|
return
|
|
}
|
|
if existing != nil && !validateCurrentIndex(w, *payload.Current, len(existing.Items)) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle playback position update
|
|
if payload.Position != nil {
|
|
pq.Position = max(*payload.Position, 0) // Ensure position is non-negative
|
|
cols = append(cols, "position")
|
|
}
|
|
|
|
// If no fields were specified for update, return success without doing anything
|
|
if len(cols) == 0 {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
// Perform partial update of the specified columns only
|
|
if err := ds.PlayQueue(ctx).Store(pq, cols...); err != nil {
|
|
log.Error(ctx, "Error updating queue", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
func clearQueue(ds model.DataStore) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
user, _ := request.UserFrom(ctx)
|
|
if err := ds.PlayQueue(ctx).Clear(user.ID); err != nil {
|
|
log.Error(ctx, "Error clearing queue", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
}
|