package discover import ( "net/http" "strconv" "strings" "github.com/gin-gonic/gin" apperrors "veza-backend-api/internal/errors" "veza-backend-api/internal/handlers" ) // Handler v0.10.1 F351-F355: discover by genre/tag, follow genre/tag type Handler struct { service *Service } // NewHandler creates a discover handler func NewHandler(service *Service) *Handler { return &Handler{service: service} } // parseLimitCursor parses limit (default 20, max 50) and cursor from query func parseLimitCursor(c *gin.Context) (limit int, cursor string) { limitStr := c.DefaultQuery("limit", "20") if l, err := strconv.Atoi(limitStr); err == nil && l > 0 { limit = l if limit > 50 { limit = 50 } } else { limit = 20 } cursor = c.Query("cursor") return limit, cursor } // GetTracksByGenre GET /api/v1/discover/genre/:genre func (h *Handler) GetTracksByGenre(c *gin.Context) { genreSlug := strings.TrimSpace(c.Param("genre")) if genreSlug == "" { handlers.RespondWithAppError(c, apperrors.NewValidationError("genre is required")) return } limit, cursor := parseLimitCursor(c) tracks, nextCursor, err := h.service.GetTracksByGenre(c.Request.Context(), genreSlug, limit, cursor) if err != nil { if strings.Contains(err.Error(), "not found") { handlers.RespondWithAppError(c, apperrors.NewNotFoundError("genre")) return } handlers.RespondWithAppError(c, apperrors.NewInternalErrorWrap("failed to get tracks by genre", err)) return } resp := gin.H{"items": tracks} if nextCursor != "" { resp["next_cursor"] = nextCursor } handlers.RespondSuccess(c, http.StatusOK, resp) } // GetTracksByTag GET /api/v1/discover/tag/:tag func (h *Handler) GetTracksByTag(c *gin.Context) { tagName := strings.TrimSpace(c.Param("tag")) if tagName == "" { handlers.RespondWithAppError(c, apperrors.NewValidationError("tag is required")) return } limit, cursor := parseLimitCursor(c) tracks, nextCursor, err := h.service.GetTracksByTag(c.Request.Context(), tagName, limit, cursor) if err != nil { if strings.Contains(err.Error(), "not found") { handlers.RespondWithAppError(c, apperrors.NewNotFoundError("tag")) return } handlers.RespondWithAppError(c, apperrors.NewInternalErrorWrap("failed to get tracks by tag", err)) return } resp := gin.H{"items": tracks} if nextCursor != "" { resp["next_cursor"] = nextCursor } handlers.RespondSuccess(c, http.StatusOK, resp) } // GetEditorialPlaylists GET /api/v1/discover/playlists/editorial (v0.10.4 F141) func (h *Handler) GetEditorialPlaylists(c *gin.Context) { limit, cursor := parseLimitCursor(c) playlists, nextCursor, err := h.service.GetEditorialPlaylists(c.Request.Context(), limit, cursor) if err != nil { handlers.RespondWithAppError(c, apperrors.NewInternalErrorWrap("failed to get editorial playlists", err)) return } resp := gin.H{"items": playlists} if nextCursor != "" { resp["next_cursor"] = nextCursor } handlers.RespondSuccess(c, http.StatusOK, resp) } // ListGenres GET /api/v1/discover/genres func (h *Handler) ListGenres(c *gin.Context) { genres, err := h.service.ListGenres(c.Request.Context()) if err != nil { handlers.RespondWithAppError(c, apperrors.NewInternalErrorWrap("failed to list genres", err)) return } handlers.RespondSuccess(c, http.StatusOK, gin.H{"genres": genres}) } // FollowGenre POST /api/v1/discover/genre/:genre/follow func (h *Handler) FollowGenre(c *gin.Context) { userID, ok := handlers.GetUserIDUUID(c) if !ok { return } genreSlug := strings.TrimSpace(c.Param("genre")) if genreSlug == "" { handlers.RespondWithAppError(c, apperrors.NewValidationError("genre is required")) return } err := h.service.FollowGenre(c.Request.Context(), userID, genreSlug) if err != nil { if strings.Contains(err.Error(), "not found") { handlers.RespondWithAppError(c, apperrors.NewNotFoundError("genre")) return } handlers.RespondWithAppError(c, apperrors.NewInternalErrorWrap("failed to follow genre", err)) return } handlers.RespondSuccess(c, http.StatusOK, gin.H{"followed": true}) } // UnfollowGenre DELETE /api/v1/discover/genre/:genre/follow func (h *Handler) UnfollowGenre(c *gin.Context) { userID, ok := handlers.GetUserIDUUID(c) if !ok { return } genreSlug := strings.TrimSpace(c.Param("genre")) if genreSlug == "" { handlers.RespondWithAppError(c, apperrors.NewValidationError("genre is required")) return } err := h.service.UnfollowGenre(c.Request.Context(), userID, genreSlug) if err != nil { handlers.RespondWithAppError(c, apperrors.NewInternalErrorWrap("failed to unfollow genre", err)) return } handlers.RespondSuccess(c, http.StatusOK, gin.H{"followed": false}) } // FollowTag POST /api/v1/discover/tag/:tag/follow func (h *Handler) FollowTag(c *gin.Context) { userID, ok := handlers.GetUserIDUUID(c) if !ok { return } tagName := strings.TrimSpace(c.Param("tag")) if tagName == "" { handlers.RespondWithAppError(c, apperrors.NewValidationError("tag is required")) return } err := h.service.FollowTag(c.Request.Context(), userID, tagName) if err != nil { if strings.Contains(err.Error(), "not found") { handlers.RespondWithAppError(c, apperrors.NewNotFoundError("tag")) return } handlers.RespondWithAppError(c, apperrors.NewInternalErrorWrap("failed to follow tag", err)) return } handlers.RespondSuccess(c, http.StatusOK, gin.H{"followed": true}) } // UnfollowTag DELETE /api/v1/discover/tag/:tag/follow func (h *Handler) UnfollowTag(c *gin.Context) { userID, ok := handlers.GetUserIDUUID(c) if !ok { return } tagName := strings.TrimSpace(c.Param("tag")) if tagName == "" { handlers.RespondWithAppError(c, apperrors.NewValidationError("tag is required")) return } err := h.service.UnfollowTag(c.Request.Context(), userID, tagName) if err != nil { handlers.RespondWithAppError(c, apperrors.NewInternalErrorWrap("failed to unfollow tag", err)) return } handlers.RespondSuccess(c, http.StatusOK, gin.H{"followed": false}) }