Files
silo-server/internal/api/handlers/library_playback_prefs.go
T

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
}