veza/veza-backend-api/internal/handlers/analytics_handler.go

247 lines
7.1 KiB
Go

package handlers
import (
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
"veza-backend-api/internal/services"
)
// AnalyticsHandler gère les opérations d'analytics de lecture de tracks
type AnalyticsHandler struct {
analyticsService *services.AnalyticsService
commonHandler *CommonHandler
}
// NewAnalyticsHandler crée un nouveau handler d'analytics
func NewAnalyticsHandler(analyticsService *services.AnalyticsService, logger *zap.Logger) *AnalyticsHandler {
return &AnalyticsHandler{
analyticsService: analyticsService,
commonHandler: NewCommonHandler(logger),
}
}
// RecordPlayRequest représente la requête pour enregistrer une lecture
type RecordPlayRequest struct {
Duration int `json:"duration" binding:"required,min=1"`
Device string `json:"device,omitempty"`
}
// RecordPlay gère l'enregistrement d'une lecture de track
func (h *AnalyticsHandler) RecordPlay(c *gin.Context) {
trackIDStr := c.Param("id")
if trackIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "track id is required"})
return
}
trackID, err := uuid.Parse(trackIDStr) // Changed to uuid.Parse
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
return
}
var req RecordPlayRequest
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
return
}
// Récupérer user_id si authentifié (optionnel pour analytics anonymes)
var userID *uuid.UUID
if uid, ok := c.Get("user_id"); ok {
if uidUUID, ok := uid.(uuid.UUID); ok {
userID = &uidUUID
}
}
// Récupérer IP address et device
ipAddress := c.ClientIP()
device := req.Device
if device == "" {
device = c.GetHeader("User-Agent")
}
err = h.analyticsService.RecordPlay(c.Request.Context(), trackID, userID, req.Duration, device, ipAddress)
if err != nil {
if err.Error() == "track not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "track not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "play recorded"})
}
// GetTrackStats gère la récupération des statistiques d'un track
func (h *AnalyticsHandler) GetTrackStats(c *gin.Context) {
trackIDStr := c.Param("id")
if trackIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "track id is required"})
return
}
trackID, err := uuid.Parse(trackIDStr) // Changed to uuid.Parse
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
return
}
stats, err := h.analyticsService.GetTrackStats(c.Request.Context(), trackID)
if err != nil {
if err.Error() == "track not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "track not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
RespondSuccess(c, http.StatusOK, gin.H{"stats": stats})
}
// GetTopTracks gère la récupération des tracks les plus écoutés
func (h *AnalyticsHandler) GetTopTracks(c *gin.Context) {
// Parse limit
limit := 10
if limitStr := c.Query("limit"); limitStr != "" {
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
limit = l
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid limit (must be between 1 and 100)"})
return
}
}
// Parse start_date (optionnel)
var startDate *time.Time
if startDateStr := c.Query("start_date"); startDateStr != "" {
parsed, err := time.Parse(time.RFC3339, startDateStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid start_date format (use RFC3339)"})
return
}
startDate = &parsed
}
// Parse end_date (optionnel)
var endDate *time.Time
if endDateStr := c.Query("end_date"); endDateStr != "" {
parsed, err := time.Parse(time.RFC3339, endDateStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid end_date format (use RFC3339)"})
return
}
endDate = &parsed
}
topTracks, err := h.analyticsService.GetTopTracks(c.Request.Context(), limit, startDate, endDate)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
RespondSuccess(c, http.StatusOK, gin.H{"tracks": topTracks})
}
// GetPlaysOverTime gère la récupération des lectures sur une période
func (h *AnalyticsHandler) GetPlaysOverTime(c *gin.Context) {
trackIDStr := c.Param("id")
if trackIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "track id is required"})
return
}
trackID, err := uuid.Parse(trackIDStr) // Changed to uuid.Parse
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
return
}
// Parse start_date (optionnel, défaut: 30 jours)
startDate := time.Now().AddDate(0, 0, -30)
if startDateStr := c.Query("start_date"); startDateStr != "" {
parsed, err := time.Parse(time.RFC3339, startDateStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid start_date format (use RFC3339)"})
return
}
startDate = parsed
}
// Parse end_date (optionnel, défaut: maintenant)
endDate := time.Now()
if endDateStr := c.Query("end_date"); endDateStr != "" {
parsed, err := time.Parse(time.RFC3339, endDateStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid end_date format (use RFC3339)"})
return
}
endDate = parsed
}
// Parse interval (optionnel, défaut: day)
interval := c.DefaultQuery("interval", "day")
validIntervals := map[string]bool{"hour": true, "day": true, "week": true, "month": true}
if !validIntervals[interval] {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid interval (must be: hour, day, week, month)"})
return
}
points, err := h.analyticsService.GetPlaysOverTime(c.Request.Context(), trackID, startDate, endDate, interval)
if err != nil {
if err.Error() == "track not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "track not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
RespondSuccess(c, http.StatusOK, gin.H{"points": points})
}
// GetUserStats gère la récupération des statistiques d'un utilisateur
func (h *AnalyticsHandler) GetUserStats(c *gin.Context) {
userIDStr := c.Param("id")
if userIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "user id is required"})
return
}
userID, err := uuid.Parse(userIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
return
}
// Vérifier que l'utilisateur peut accéder à ses propres stats
var authenticatedUserID *uuid.UUID
if uid, ok := c.Get("user_id"); ok {
if uidUUID, ok := uid.(uuid.UUID); ok {
authenticatedUserID = &uidUUID
}
}
if authenticatedUserID != nil && *authenticatedUserID != userID {
c.JSON(http.StatusForbidden, gin.H{"error": "cannot access other user's stats"})
return
}
stats, err := h.analyticsService.GetUserStats(c.Request.Context(), userID)
if err != nil {
if err.Error() == "user not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
RespondSuccess(c, http.StatusOK, gin.H{"stats": stats})
}