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 }