Files
SEEN/backend/internal/api/handlers/catalog_handler.go
T
2026-04-10 12:06:24 +02:00

228 lines
5.6 KiB
Go

package handlers
import (
"errors"
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/tdvorak/seen/backend/internal/domain"
"github.com/tdvorak/seen/backend/internal/services/auth"
"github.com/tdvorak/seen/backend/internal/services/catalog"
"github.com/tdvorak/seen/backend/pkg/httpx"
)
type CatalogHandler struct {
service *catalog.Service
authService *auth.Service
}
func NewCatalogHandler(service *catalog.Service, authService *auth.Service) *CatalogHandler {
return &CatalogHandler{service: service, authService: authService}
}
type watchLaterAddRequest struct {
MediaID int `json:"mediaId"`
}
type progressUpdateRequest struct {
MediaID int `json:"mediaId"`
SeasonNumber int `json:"seasonNumber"`
EpisodeNumber int `json:"episodeNumber"`
ProgressPercent int `json:"progressPercent"`
}
func (h *CatalogHandler) Dashboard(c *gin.Context) {
httpx.JSON(c, http.StatusOK, h.service.Dashboard())
}
func (h *CatalogHandler) ContinueWatching(c *gin.Context) {
user, ok := h.resolveUser(c)
if !ok {
return
}
items, err := h.service.ContinueWatching(user.ID)
if err != nil {
switch {
case errors.Is(err, catalog.ErrInvalidInput):
httpx.JSONError(c, http.StatusBadRequest, err.Error())
default:
httpx.JSONError(c, http.StatusInternalServerError, "failed to load continue watching")
}
return
}
httpx.JSON(c, http.StatusOK, items)
}
func (h *CatalogHandler) Discover(c *gin.Context) {
page := parseInt(c.Query("page"), 1)
pageSize := parseInt(c.Query("pageSize"), 6)
httpx.JSON(c, http.StatusOK, h.service.Discover(catalog.DiscoverParams{
Page: page,
PageSize: pageSize,
Query: c.Query("query"),
Genre: c.Query("genre"),
MediaType: c.Query("mediaType"),
}))
}
func (h *CatalogHandler) Games(c *gin.Context) {
page := parseInt(c.Query("page"), 1)
pageSize := parseInt(c.Query("pageSize"), 6)
httpx.JSON(c, http.StatusOK, h.service.Discover(catalog.DiscoverParams{
Page: page,
PageSize: pageSize,
Query: c.Query("query"),
Genre: c.Query("genre"),
MediaType: string(catalog.MediaTypeGame),
}))
}
func (h *CatalogHandler) Search(c *gin.Context) {
httpx.JSON(c, http.StatusOK, h.service.Search(catalog.SearchParams{
Query: c.Query("query"),
Genre: c.Query("genre"),
MediaType: c.Query("mediaType"),
}))
}
func (h *CatalogHandler) WatchLater(c *gin.Context) {
user, ok := h.resolveUser(c)
if !ok {
return
}
items, err := h.service.WatchLater(user.ID)
if err != nil {
switch {
case errors.Is(err, catalog.ErrInvalidInput):
httpx.JSONError(c, http.StatusBadRequest, err.Error())
default:
httpx.JSONError(c, http.StatusInternalServerError, "failed to load watch later")
}
return
}
httpx.JSON(c, http.StatusOK, items)
}
func (h *CatalogHandler) AddWatchLater(c *gin.Context) {
user, ok := h.resolveUser(c)
if !ok {
return
}
var request watchLaterAddRequest
if err := c.ShouldBindJSON(&request); err != nil {
httpx.JSONError(c, http.StatusBadRequest, "invalid request body")
return
}
items, err := h.service.AddWatchLater(user.ID, request.MediaID)
if err != nil {
switch {
case errors.Is(err, catalog.ErrInvalidInput):
httpx.JSONError(c, http.StatusBadRequest, err.Error())
case errors.Is(err, catalog.ErrMediaNotFound):
httpx.JSONError(c, http.StatusNotFound, err.Error())
default:
httpx.JSONError(c, http.StatusInternalServerError, "failed to add watch later item")
}
return
}
httpx.JSON(c, http.StatusOK, items)
}
func (h *CatalogHandler) RemoveWatchLater(c *gin.Context) {
user, ok := h.resolveUser(c)
if !ok {
return
}
mediaID := parseInt(c.Param("mediaId"), 0)
items, err := h.service.RemoveWatchLater(user.ID, mediaID)
if err != nil {
switch {
case errors.Is(err, catalog.ErrInvalidInput):
httpx.JSONError(c, http.StatusBadRequest, err.Error())
default:
httpx.JSONError(c, http.StatusInternalServerError, "failed to remove watch later item")
}
return
}
httpx.JSON(c, http.StatusOK, items)
}
func (h *CatalogHandler) UpdateProgress(c *gin.Context) {
user, ok := h.resolveUser(c)
if !ok {
return
}
var request progressUpdateRequest
if err := c.ShouldBindJSON(&request); err != nil {
httpx.JSONError(c, http.StatusBadRequest, "invalid request body")
return
}
items, err := h.service.UpdateProgress(user.ID, catalog.ProgressUpdateInput{
MediaID: request.MediaID,
SeasonNumber: request.SeasonNumber,
EpisodeNumber: request.EpisodeNumber,
ProgressPercent: request.ProgressPercent,
})
if err != nil {
switch {
case errors.Is(err, catalog.ErrInvalidInput):
httpx.JSONError(c, http.StatusBadRequest, err.Error())
case errors.Is(err, catalog.ErrMediaNotFound):
httpx.JSONError(c, http.StatusNotFound, err.Error())
default:
httpx.JSONError(c, http.StatusInternalServerError, "failed to update progress")
}
return
}
httpx.JSON(c, http.StatusOK, items)
}
func (h *CatalogHandler) resolveUser(c *gin.Context) (*domain.User, bool) {
accessToken, ok := bearerToken(c.GetHeader("Authorization"))
if !ok {
httpx.JSONError(c, http.StatusUnauthorized, "missing bearer token")
return nil, false
}
user, err := h.authService.UserFromAccessToken(c.Request.Context(), accessToken)
if err != nil {
switch {
case errors.Is(err, auth.ErrInvalidToken):
httpx.JSONError(c, http.StatusUnauthorized, err.Error())
default:
httpx.JSONError(c, http.StatusInternalServerError, "failed to resolve user")
}
return nil, false
}
return user, true
}
func parseInt(raw string, fallback int) int {
if raw == "" {
return fallback
}
parsed, err := strconv.Atoi(raw)
if err != nil {
return fallback
}
return parsed
}