248 lines
7.7 KiB
Go
248 lines
7.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
apimw "github.com/Silo-Server/silo-server/internal/api/middleware"
|
|
"github.com/Silo-Server/silo-server/internal/catalog"
|
|
"github.com/Silo-Server/silo-server/internal/models"
|
|
"github.com/Silo-Server/silo-server/internal/userstore"
|
|
)
|
|
|
|
type libraryLookup interface {
|
|
GetByID(context.Context, int) (*models.MediaFolder, error)
|
|
}
|
|
|
|
// LibraryPlaybackPrefHandler handles per-library playback preference endpoints.
|
|
type LibraryPlaybackPrefHandler struct {
|
|
storeProvider userstore.UserStoreProvider
|
|
libraryLookup libraryLookup
|
|
}
|
|
|
|
// NewLibraryPlaybackPrefHandler creates a new LibraryPlaybackPrefHandler.
|
|
func NewLibraryPlaybackPrefHandler(provider userstore.UserStoreProvider) *LibraryPlaybackPrefHandler {
|
|
return &LibraryPlaybackPrefHandler{storeProvider: provider}
|
|
}
|
|
|
|
// SetLibraryLookup wires in an optional library lookup used to reject
|
|
// nonexistent library IDs before mutating playback preferences.
|
|
func (h *LibraryPlaybackPrefHandler) SetLibraryLookup(lookup libraryLookup) {
|
|
h.libraryLookup = lookup
|
|
}
|
|
|
|
type setLibraryPlaybackPrefRequest struct {
|
|
AudioLanguage *string `json:"audio_language"`
|
|
SubtitleLanguage *string `json:"subtitle_language"`
|
|
SubtitleMode *string `json:"subtitle_mode"`
|
|
ShowForcedSubtitles *bool `json:"show_forced_subtitles"`
|
|
}
|
|
|
|
type libraryPlaybackPrefResponse struct {
|
|
ProfileID string `json:"profile_id"`
|
|
LibraryID int `json:"library_id"`
|
|
AudioLanguage *string `json:"audio_language,omitempty"`
|
|
SubtitleLanguage *string `json:"subtitle_language,omitempty"`
|
|
SubtitleMode *string `json:"subtitle_mode,omitempty"`
|
|
ShowForcedSubtitles *bool `json:"show_forced_subtitles,omitempty"`
|
|
UpdatedAt string `json:"updated_at,omitempty"`
|
|
}
|
|
|
|
type libraryPlaybackPrefsListResponse struct {
|
|
Preferences []libraryPlaybackPrefResponse `json:"preferences"`
|
|
}
|
|
|
|
// HandleListLibraryPlaybackPrefs handles GET /library-playback-prefs.
|
|
func (h *LibraryPlaybackPrefHandler) HandleListLibraryPlaybackPrefs(w http.ResponseWriter, r *http.Request) {
|
|
userID := apimw.GetUserID(r.Context())
|
|
profileID := apimw.GetProfileID(r.Context())
|
|
|
|
store, err := h.storeProvider.ForUser(r.Context(), userID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to access user store")
|
|
return
|
|
}
|
|
|
|
prefs, err := store.ListLibraryPlaybackPreferences(r.Context(), profileID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list library playback preferences")
|
|
return
|
|
}
|
|
|
|
resp := libraryPlaybackPrefsListResponse{
|
|
Preferences: make([]libraryPlaybackPrefResponse, 0, len(prefs)),
|
|
}
|
|
for _, pref := range prefs {
|
|
resp.Preferences = append(resp.Preferences, toLibraryPlaybackPrefResponse(pref))
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
// HandleSetLibraryPlaybackPref handles PUT /library-playback-prefs/{library_id}.
|
|
func (h *LibraryPlaybackPrefHandler) HandleSetLibraryPlaybackPref(w http.ResponseWriter, r *http.Request) {
|
|
userID := apimw.GetUserID(r.Context())
|
|
profileID := apimw.GetProfileID(r.Context())
|
|
libraryID, ok := parseLibraryID(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
if !h.ensureLibraryExists(w, r, libraryID) {
|
|
return
|
|
}
|
|
|
|
var req setLibraryPlaybackPrefRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "Invalid request body")
|
|
return
|
|
}
|
|
|
|
if !isValidLibrarySubtitleMode(req.SubtitleMode) {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "Invalid subtitle_mode")
|
|
return
|
|
}
|
|
|
|
store, err := h.storeProvider.ForUser(r.Context(), userID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to access user store")
|
|
return
|
|
}
|
|
|
|
pref := userstore.LibraryPlaybackPreference{
|
|
ProfileID: profileID,
|
|
LibraryID: libraryID,
|
|
HasAudioLanguage: req.AudioLanguage != nil,
|
|
HasSubtitleLanguage: req.SubtitleLanguage != nil,
|
|
HasSubtitleMode: req.SubtitleMode != nil,
|
|
HasShowForcedSubtitles: req.ShowForcedSubtitles != nil,
|
|
}
|
|
if req.AudioLanguage != nil {
|
|
pref.AudioLanguage = *req.AudioLanguage
|
|
}
|
|
if req.SubtitleLanguage != nil {
|
|
pref.SubtitleLanguage = *req.SubtitleLanguage
|
|
}
|
|
if req.SubtitleMode != nil {
|
|
pref.SubtitleMode = *req.SubtitleMode
|
|
}
|
|
if req.ShowForcedSubtitles != nil {
|
|
pref.ShowForcedSubtitles = *req.ShowForcedSubtitles
|
|
}
|
|
|
|
if !pref.HasAudioLanguage && !pref.HasSubtitleLanguage && !pref.HasSubtitleMode && !pref.HasShowForcedSubtitles {
|
|
if err := store.DeleteLibraryPlaybackPreference(r.Context(), profileID, libraryID); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to delete library playback preference")
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
|
|
if err := store.UpsertLibraryPlaybackPreference(r.Context(), pref); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to set library playback preference")
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
// HandleDeleteLibraryPlaybackPref handles DELETE /library-playback-prefs/{library_id}.
|
|
func (h *LibraryPlaybackPrefHandler) HandleDeleteLibraryPlaybackPref(w http.ResponseWriter, r *http.Request) {
|
|
userID := apimw.GetUserID(r.Context())
|
|
profileID := apimw.GetProfileID(r.Context())
|
|
libraryID, ok := parseLibraryID(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
if !h.ensureLibraryExists(w, r, libraryID) {
|
|
return
|
|
}
|
|
|
|
store, err := h.storeProvider.ForUser(r.Context(), userID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to access user store")
|
|
return
|
|
}
|
|
|
|
if err := store.DeleteLibraryPlaybackPreference(r.Context(), profileID, libraryID); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to delete library playback preference")
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
|
|
func parseLibraryID(w http.ResponseWriter, r *http.Request) (int, bool) {
|
|
libraryIDStr := chi.URLParam(r, "library_id")
|
|
if libraryIDStr == "" {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "Library ID is required")
|
|
return 0, false
|
|
}
|
|
|
|
libraryID, err := strconv.Atoi(libraryIDStr)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "Library ID must be a valid integer")
|
|
return 0, false
|
|
}
|
|
if libraryID <= 0 {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "Library ID must be a positive integer")
|
|
return 0, false
|
|
}
|
|
return libraryID, true
|
|
}
|
|
|
|
func (h *LibraryPlaybackPrefHandler) ensureLibraryExists(w http.ResponseWriter, r *http.Request, libraryID int) bool {
|
|
if h.libraryLookup == nil {
|
|
return true
|
|
}
|
|
|
|
if _, err := h.libraryLookup.GetByID(r.Context(), libraryID); err != nil {
|
|
if errors.Is(err, catalog.ErrFolderNotFound) {
|
|
writeError(w, http.StatusNotFound, "not_found", "Library not found")
|
|
return false
|
|
}
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to look up library")
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func isValidLibrarySubtitleMode(mode *string) bool {
|
|
if mode == nil {
|
|
return true
|
|
}
|
|
switch *mode {
|
|
case "", "auto", "always", "off":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func toLibraryPlaybackPrefResponse(p userstore.LibraryPlaybackPreference) libraryPlaybackPrefResponse {
|
|
resp := libraryPlaybackPrefResponse{
|
|
ProfileID: p.ProfileID,
|
|
LibraryID: p.LibraryID,
|
|
UpdatedAt: p.UpdatedAt,
|
|
}
|
|
if p.HasAudioLanguage {
|
|
resp.AudioLanguage = stringPtr(p.AudioLanguage)
|
|
}
|
|
if p.HasSubtitleLanguage {
|
|
resp.SubtitleLanguage = stringPtr(p.SubtitleLanguage)
|
|
}
|
|
if p.HasSubtitleMode {
|
|
resp.SubtitleMode = stringPtr(p.SubtitleMode)
|
|
}
|
|
if p.HasShowForcedSubtitles {
|
|
resp.ShowForcedSubtitles = boolPtr(p.ShowForcedSubtitles)
|
|
}
|
|
return resp
|
|
}
|