247 lines
7.1 KiB
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})
|
|
}
|