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/download" "github.com/tdvorak/seen/backend/pkg/httpx" ) type DownloadHandler struct { service *download.Service authService *auth.Service } func NewDownloadHandler(service *download.Service, authService *auth.Service) *DownloadHandler { return &DownloadHandler{ service: service, authService: authService, } } type createDownloadRequest struct { SourceType string `json:"sourceType"` Source string `json:"source"` Title string `json:"title"` } func (h *DownloadHandler) Create(c *gin.Context) { user, ok := h.resolveUser(c) if !ok { return } var request createDownloadRequest if err := c.ShouldBindJSON(&request); err != nil { httpx.JSONError(c, http.StatusBadRequest, "invalid request body") return } job, err := h.service.Create(c.Request.Context(), user.ID, download.CreateInput{ SourceType: request.SourceType, Source: request.Source, Title: request.Title, }) if err != nil { switch { case errors.Is(err, download.ErrInvalidInput): httpx.JSONError(c, http.StatusBadRequest, err.Error()) default: httpx.JSONError(c, http.StatusInternalServerError, "failed to create download job") } return } httpx.JSON(c, http.StatusCreated, job) } func (h *DownloadHandler) List(c *gin.Context) { user, ok := h.resolveUser(c) if !ok { return } items, err := h.service.List(c.Request.Context(), user.ID, download.ListParams{ Status: c.Query("status"), Limit: parseIntSafe(c.Query("limit"), 20), Offset: parseIntSafe(c.Query("offset"), 0), }) if err != nil { switch { case errors.Is(err, download.ErrInvalidInput): httpx.JSONError(c, http.StatusBadRequest, err.Error()) default: httpx.JSONError(c, http.StatusInternalServerError, "failed to list download jobs") } return } httpx.JSON(c, http.StatusOK, items) } func (h *DownloadHandler) Cancel(c *gin.Context) { user, ok := h.resolveUser(c) if !ok { return } jobID := c.Param("id") job, err := h.service.Cancel(c.Request.Context(), user.ID, jobID) if err != nil { switch { case errors.Is(err, download.ErrInvalidInput): httpx.JSONError(c, http.StatusBadRequest, err.Error()) case errors.Is(err, download.ErrNotFound): httpx.JSONError(c, http.StatusNotFound, err.Error()) default: httpx.JSONError(c, http.StatusInternalServerError, "failed to cancel download job") } return } httpx.JSON(c, http.StatusOK, job) } func (h *DownloadHandler) Events(c *gin.Context) { user, ok := h.resolveUser(c) if !ok { return } jobID := c.Param("id") items, err := h.service.Events(c.Request.Context(), user.ID, jobID, download.EventParams{ After: c.Query("after"), Limit: parseIntSafe(c.Query("limit"), 100), }) if err != nil { switch { case errors.Is(err, download.ErrInvalidInput): httpx.JSONError(c, http.StatusBadRequest, err.Error()) case errors.Is(err, download.ErrNotFound): httpx.JSONError(c, http.StatusNotFound, err.Error()) default: httpx.JSONError(c, http.StatusInternalServerError, "failed to list download events") } return } httpx.JSON(c, http.StatusOK, items) } func (h *DownloadHandler) 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 parseIntSafe(raw string, fallback int) int { if raw == "" { return fallback } parsed, err := strconv.Atoi(raw) if err != nil { return fallback } return parsed }