- Add plans for date-named episodes and Jellyfin autoscan compat - Update requests handlers, service, and UI hooks - Remove Makefile.local.example
633 lines
21 KiB
Go
633 lines
21 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
|
|
apimw "github.com/Silo-Server/silo-server/internal/api/middleware"
|
|
mediarequests "github.com/Silo-Server/silo-server/internal/requests"
|
|
)
|
|
|
|
type RequestService interface {
|
|
Search(ctx context.Context, viewer mediarequests.Viewer, query string, mediaType mediarequests.MediaType, page int) (*mediarequests.MediaPage, error)
|
|
Discover(ctx context.Context, viewer mediarequests.Viewer, section string, page int) (*mediarequests.DiscoverySection, error)
|
|
DiscoverAll(ctx context.Context, viewer mediarequests.Viewer) ([]mediarequests.DiscoverySection, error)
|
|
GetDetail(ctx context.Context, viewer mediarequests.Viewer, mediaType mediarequests.MediaType, tmdbID int) (*mediarequests.MediaDetail, error)
|
|
CreateRequest(ctx context.Context, viewer mediarequests.Viewer, input mediarequests.CreateRequestInput) (*mediarequests.Request, error)
|
|
ListMine(ctx context.Context, viewer mediarequests.Viewer, filter mediarequests.ListFilter) ([]*mediarequests.Request, error)
|
|
ListAdmin(ctx context.Context, viewer mediarequests.Viewer, filter mediarequests.ListFilter) ([]*mediarequests.Request, error)
|
|
GetRequest(ctx context.Context, viewer mediarequests.Viewer, id string) (*mediarequests.Request, error)
|
|
Approve(ctx context.Context, viewer mediarequests.Viewer, id string) (*mediarequests.Request, error)
|
|
Decline(ctx context.Context, viewer mediarequests.Viewer, id, reason string) (*mediarequests.Request, error)
|
|
Cancel(ctx context.Context, viewer mediarequests.Viewer, id, reason string) (*mediarequests.Request, error)
|
|
Retry(ctx context.Context, viewer mediarequests.Viewer, id string) (*mediarequests.Request, error)
|
|
GetFeatureStatus(ctx context.Context, viewer mediarequests.Viewer) (mediarequests.FeatureStatus, error)
|
|
GetSettings(ctx context.Context, viewer mediarequests.Viewer) (mediarequests.Settings, error)
|
|
UpdateSettings(ctx context.Context, viewer mediarequests.Viewer, settings mediarequests.Settings) (mediarequests.Settings, error)
|
|
GetUserLimit(ctx context.Context, viewer mediarequests.Viewer, userID int) (*mediarequests.UserLimit, error)
|
|
UpsertUserLimit(ctx context.Context, viewer mediarequests.Viewer, limit mediarequests.UserLimit) (*mediarequests.UserLimit, error)
|
|
ListIntegrations(ctx context.Context, viewer mediarequests.Viewer) ([]mediarequests.Integration, error)
|
|
UpsertIntegration(ctx context.Context, viewer mediarequests.Viewer, integration mediarequests.Integration) (*mediarequests.Integration, error)
|
|
UpsertIntegrations(ctx context.Context, viewer mediarequests.Viewer, integrations []mediarequests.Integration) ([]mediarequests.Integration, error)
|
|
LoadIntegrationOptions(ctx context.Context, viewer mediarequests.Viewer, integration mediarequests.Integration) (*mediarequests.IntegrationOptions, error)
|
|
|
|
ListStudios(ctx context.Context, viewer mediarequests.Viewer) ([]mediarequests.DiscoverBrandCard, error)
|
|
ListNetworks(ctx context.Context, viewer mediarequests.Viewer) ([]mediarequests.DiscoverBrandCard, error)
|
|
ListGenres(ctx context.Context, viewer mediarequests.Viewer) ([]mediarequests.DiscoverBrandCard, error)
|
|
BrowseStudio(ctx context.Context, viewer mediarequests.Viewer, slug, sort string, page int) (*mediarequests.DiscoverBrowseResponse, error)
|
|
BrowseNetwork(ctx context.Context, viewer mediarequests.Viewer, slug, sort string, page int) (*mediarequests.DiscoverBrowseResponse, error)
|
|
BrowseGenre(ctx context.Context, viewer mediarequests.Viewer, slug string, mediaType mediarequests.MediaType, sort string, page int) (*mediarequests.DiscoverBrowseResponse, error)
|
|
}
|
|
|
|
type RequestsHandler struct {
|
|
service RequestService
|
|
}
|
|
|
|
func NewRequestsHandler(service RequestService) *RequestsHandler {
|
|
return &RequestsHandler{service: service}
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleSearch(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
page, ok := parsePositiveIntQuery(w, r, "page", 1)
|
|
if !ok {
|
|
return
|
|
}
|
|
result, err := h.service.Search(
|
|
r.Context(),
|
|
viewer,
|
|
r.URL.Query().Get("q"),
|
|
mediarequests.MediaType(r.URL.Query().Get("media_type")),
|
|
page,
|
|
)
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, result)
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleDiscover(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
sections, err := h.service.DiscoverAll(r.Context(), viewer)
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, struct {
|
|
Sections []mediarequests.DiscoverySection `json:"sections"`
|
|
}{Sections: sections})
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleDiscoverSection(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
page, ok := parsePositiveIntQuery(w, r, "page", 1)
|
|
if !ok {
|
|
return
|
|
}
|
|
section, err := h.service.Discover(r.Context(), viewer, chi.URLParam(r, "section"), page)
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, section)
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleListStudios(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
studios, err := h.service.ListStudios(r.Context(), viewer)
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, struct {
|
|
Studios []mediarequests.DiscoverBrandCard `json:"studios"`
|
|
}{Studios: studios})
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleListNetworks(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
networks, err := h.service.ListNetworks(r.Context(), viewer)
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, struct {
|
|
Networks []mediarequests.DiscoverBrandCard `json:"networks"`
|
|
}{Networks: networks})
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleListGenres(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
genres, err := h.service.ListGenres(r.Context(), viewer)
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, struct {
|
|
Genres []mediarequests.DiscoverBrandCard `json:"genres"`
|
|
}{Genres: genres})
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleBrowseStudio(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
page, ok := parsePositiveIntQuery(w, r, "page", 1)
|
|
if !ok {
|
|
return
|
|
}
|
|
slug := strings.TrimSpace(chi.URLParam(r, "slug"))
|
|
sort := strings.TrimSpace(r.URL.Query().Get("sort"))
|
|
resp, err := h.service.BrowseStudio(r.Context(), viewer, slug, sort, page)
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleBrowseNetwork(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
page, ok := parsePositiveIntQuery(w, r, "page", 1)
|
|
if !ok {
|
|
return
|
|
}
|
|
slug := strings.TrimSpace(chi.URLParam(r, "slug"))
|
|
sort := strings.TrimSpace(r.URL.Query().Get("sort"))
|
|
resp, err := h.service.BrowseNetwork(r.Context(), viewer, slug, sort, page)
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleBrowseGenre(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
page, ok := parsePositiveIntQuery(w, r, "page", 1)
|
|
if !ok {
|
|
return
|
|
}
|
|
slug := strings.TrimSpace(chi.URLParam(r, "slug"))
|
|
sort := strings.TrimSpace(r.URL.Query().Get("sort"))
|
|
mediaType := mediarequests.MediaType(strings.TrimSpace(r.URL.Query().Get("media_type")))
|
|
resp, err := h.service.BrowseGenre(r.Context(), viewer, slug, mediaType, sort, page)
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, resp)
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleGetDetail(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
mediaType := mediarequests.MediaType(strings.TrimSpace(chi.URLParam(r, "media_type")))
|
|
tmdbID, err := strconv.Atoi(strings.TrimSpace(chi.URLParam(r, "tmdb_id")))
|
|
if err != nil || tmdbID <= 0 {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "Invalid tmdb id")
|
|
return
|
|
}
|
|
detail, err := h.service.GetDetail(r.Context(), viewer, mediaType, tmdbID)
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, detail)
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleCreate(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
var input mediarequests.CreateRequestInput
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "Invalid request body")
|
|
return
|
|
}
|
|
req, err := h.service.CreateRequest(r.Context(), viewer, input)
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusCreated, req)
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleListMine(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
requests, err := h.service.ListMine(r.Context(), viewer, parseRequestListFilter(r))
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, struct {
|
|
Requests []*mediarequests.Request `json:"requests"`
|
|
}{Requests: requests})
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleGet(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
req, err := h.service.GetRequest(r.Context(), viewer, chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, req)
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleAdminList(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
requests, err := h.service.ListAdmin(r.Context(), viewer, parseRequestListFilter(r))
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, struct {
|
|
Requests []*mediarequests.Request `json:"requests"`
|
|
}{Requests: requests})
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleApprove(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
req, err := h.service.Approve(r.Context(), viewer, chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, req)
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleDecline(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
var body struct {
|
|
Reason string `json:"reason"`
|
|
}
|
|
if r.Body != nil {
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "Invalid request body")
|
|
return
|
|
}
|
|
}
|
|
req, err := h.service.Decline(r.Context(), viewer, chi.URLParam(r, "id"), body.Reason)
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, req)
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleCancel(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
var body struct {
|
|
Reason string `json:"reason"`
|
|
}
|
|
if r.Body != nil {
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil && !errors.Is(err, io.EOF) {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "Invalid request body")
|
|
return
|
|
}
|
|
}
|
|
req, err := h.service.Cancel(r.Context(), viewer, chi.URLParam(r, "id"), body.Reason)
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, req)
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleRetry(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
req, err := h.service.Retry(r.Context(), viewer, chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, req)
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleGetStatus(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, true)
|
|
if !ok {
|
|
return
|
|
}
|
|
status, err := h.service.GetFeatureStatus(r.Context(), viewer)
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, status)
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleGetSettings(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
settings, err := h.service.GetSettings(r.Context(), viewer)
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, settings)
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleUpdateSettings(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
var settings mediarequests.Settings
|
|
if err := json.NewDecoder(r.Body).Decode(&settings); err != nil {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "Invalid request body")
|
|
return
|
|
}
|
|
updated, err := h.service.UpdateSettings(r.Context(), viewer, settings)
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, updated)
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleListIntegrations(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
integrations, err := h.service.ListIntegrations(r.Context(), viewer)
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, struct {
|
|
Integrations []requestIntegrationResponse `json:"integrations"`
|
|
}{Integrations: toIntegrationResponses(integrations)})
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleUpdateIntegrations(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
var body struct {
|
|
Integrations []mediarequests.Integration `json:"integrations"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "Invalid request body")
|
|
return
|
|
}
|
|
updated, err := h.service.UpsertIntegrations(r.Context(), viewer, body.Integrations)
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, struct {
|
|
Integrations []requestIntegrationResponse `json:"integrations"`
|
|
}{Integrations: toIntegrationResponses(updated)})
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleLoadIntegrationOptions(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
var integration mediarequests.Integration
|
|
if r.Body != nil {
|
|
if err := json.NewDecoder(r.Body).Decode(&integration); err != nil {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "Invalid request body")
|
|
return
|
|
}
|
|
}
|
|
integration.Kind = chi.URLParam(r, "kind")
|
|
options, err := h.service.LoadIntegrationOptions(r.Context(), viewer, integration)
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, options)
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleGetUserLimit(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
userID, ok := parsePositivePathInt(w, r, "user_id")
|
|
if !ok {
|
|
return
|
|
}
|
|
limit, err := h.service.GetUserLimit(r.Context(), viewer, userID)
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, limit)
|
|
}
|
|
|
|
func (h *RequestsHandler) HandleUpdateUserLimit(w http.ResponseWriter, r *http.Request) {
|
|
viewer, ok := requestViewer(w, r, false)
|
|
if !ok {
|
|
return
|
|
}
|
|
userID, ok := parsePositivePathInt(w, r, "user_id")
|
|
if !ok {
|
|
return
|
|
}
|
|
var limit mediarequests.UserLimit
|
|
if err := json.NewDecoder(r.Body).Decode(&limit); err != nil {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "Invalid request body")
|
|
return
|
|
}
|
|
limit.UserID = userID
|
|
updated, err := h.service.UpsertUserLimit(r.Context(), viewer, limit)
|
|
if err != nil {
|
|
writeRequestServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, updated)
|
|
}
|
|
|
|
func requestViewer(w http.ResponseWriter, r *http.Request, requireProfile bool) (mediarequests.Viewer, bool) {
|
|
claims := apimw.GetClaims(r.Context())
|
|
if claims == nil || claims.UserID == 0 {
|
|
writeError(w, http.StatusUnauthorized, "unauthorized", "Authentication required")
|
|
return mediarequests.Viewer{}, false
|
|
}
|
|
profileID := strings.TrimSpace(apimw.GetProfileID(r.Context()))
|
|
if requireProfile && profileID == "" {
|
|
writeError(w, http.StatusBadRequest, "profile_required", "Profile is required")
|
|
return mediarequests.Viewer{}, false
|
|
}
|
|
return mediarequests.Viewer{
|
|
UserID: claims.UserID,
|
|
ProfileID: profileID,
|
|
IsAdmin: claims.Role == "admin",
|
|
}, true
|
|
}
|
|
|
|
func parseRequestListFilter(r *http.Request) mediarequests.ListFilter {
|
|
q := r.URL.Query()
|
|
limit, _ := strconv.Atoi(q.Get("limit"))
|
|
offset, _ := strconv.Atoi(q.Get("offset"))
|
|
return mediarequests.ListFilter{
|
|
Status: mediarequests.Status(strings.TrimSpace(q.Get("status"))),
|
|
Outcome: mediarequests.Outcome(strings.TrimSpace(q.Get("outcome"))),
|
|
Limit: limit,
|
|
Offset: offset,
|
|
}
|
|
}
|
|
|
|
func parsePositiveIntQuery(w http.ResponseWriter, r *http.Request, key string, fallback int) (int, bool) {
|
|
raw := strings.TrimSpace(r.URL.Query().Get(key))
|
|
if raw == "" {
|
|
return fallback, true
|
|
}
|
|
value, err := strconv.Atoi(raw)
|
|
if err != nil || value <= 0 {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "Invalid "+key)
|
|
return 0, false
|
|
}
|
|
return value, true
|
|
}
|
|
|
|
func parsePositivePathInt(w http.ResponseWriter, r *http.Request, key string) (int, bool) {
|
|
value, err := strconv.Atoi(strings.TrimSpace(chi.URLParam(r, key)))
|
|
if err != nil || value <= 0 {
|
|
writeError(w, http.StatusBadRequest, "bad_request", "Invalid "+key)
|
|
return 0, false
|
|
}
|
|
return value, true
|
|
}
|
|
|
|
type requestIntegrationResponse struct {
|
|
Kind string `json:"kind"`
|
|
Enabled bool `json:"enabled"`
|
|
BaseURL string `json:"base_url"`
|
|
HasAPIKey bool `json:"has_api_key"`
|
|
RootFolder string `json:"root_folder"`
|
|
QualityProfileID *int `json:"quality_profile_id,omitempty"`
|
|
Tags []int `json:"tags"`
|
|
Options map[string]any `json:"options"`
|
|
LastCheckAt *time.Time `json:"last_check_at,omitempty"`
|
|
LastCheckStatus string `json:"last_check_status,omitempty"`
|
|
LastCheckError string `json:"last_check_error,omitempty"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
func toIntegrationResponses(integrations []mediarequests.Integration) []requestIntegrationResponse {
|
|
out := make([]requestIntegrationResponse, 0, len(integrations))
|
|
for _, integration := range integrations {
|
|
out = append(out, requestIntegrationResponse{
|
|
Kind: integration.Kind,
|
|
Enabled: integration.Enabled,
|
|
BaseURL: integration.BaseURL,
|
|
HasAPIKey: strings.TrimSpace(integration.APIKeyRef) != "",
|
|
RootFolder: integration.RootFolder,
|
|
QualityProfileID: integration.QualityProfileID,
|
|
Tags: integration.Tags,
|
|
Options: integration.Options,
|
|
LastCheckAt: integration.LastCheckAt,
|
|
LastCheckStatus: integration.LastCheckStatus,
|
|
LastCheckError: integration.LastCheckError,
|
|
UpdatedAt: integration.UpdatedAt,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func writeRequestServiceError(w http.ResponseWriter, err error) {
|
|
var quota mediarequests.QuotaError
|
|
switch {
|
|
case errors.As(err, "a):
|
|
writeJSON(w, http.StatusTooManyRequests, struct {
|
|
Error string `json:"error"`
|
|
Message string `json:"message"`
|
|
Used int `json:"used"`
|
|
Limit int `json:"limit"`
|
|
WindowDays int `json:"window_days"`
|
|
}{
|
|
Error: "quota_exceeded",
|
|
Message: "Request quota exceeded",
|
|
Used: quota.Used,
|
|
Limit: quota.Limit,
|
|
WindowDays: quota.WindowDays,
|
|
})
|
|
case errors.Is(err, mediarequests.ErrInvalidInput), errors.Is(err, mediarequests.ErrInvalidMediaType):
|
|
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
|
|
case errors.Is(err, mediarequests.ErrRequestsDisabled):
|
|
writeError(w, http.StatusForbidden, "requests_disabled", "Requests are disabled")
|
|
case errors.Is(err, mediarequests.ErrUserBlocked):
|
|
writeError(w, http.StatusForbidden, "requesting_blocked", "User is blocked from requesting")
|
|
case errors.Is(err, mediarequests.ErrAlreadyAvailable):
|
|
writeError(w, http.StatusConflict, "already_available", "Media is already available")
|
|
case errors.Is(err, mediarequests.ErrAlreadyRequested):
|
|
writeError(w, http.StatusConflict, "already_requested", "Media is already requested")
|
|
case errors.Is(err, mediarequests.ErrForbidden):
|
|
writeError(w, http.StatusForbidden, "forbidden", "Request access denied")
|
|
case errors.Is(err, mediarequests.ErrNotFound):
|
|
writeError(w, http.StatusNotFound, "not_found", "Request not found")
|
|
case errors.Is(err, mediarequests.ErrInvalidState):
|
|
writeError(w, http.StatusConflict, "invalid_state", "Request is not in a valid state for this action")
|
|
default:
|
|
writeError(w, http.StatusInternalServerError, "internal_error", "Request operation failed")
|
|
}
|
|
}
|