veza/veza-backend-api/internal/core/track/handler.go

1405 lines
41 KiB
Go

package track
import (
"errors"
"fmt"
"github.com/google/uuid"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap" // Added zap
"gorm.io/gorm"
"veza-backend-api/internal/models"
"veza-backend-api/internal/services"
"veza-backend-api/internal/validators"
)
// TrackHandler gère les opérations sur les tracks
type TrackHandler struct {
trackService *TrackService
trackUploadService *services.TrackUploadService
chunkService *services.TrackChunkService
likeService *services.TrackLikeService
streamService *services.StreamService
searchService *services.TrackSearchService
shareService *services.TrackShareService
versionService *services.TrackVersionService
historyService *services.TrackHistoryService
}
// NewTrackHandler crée un nouveau handler de tracks
func NewTrackHandler(
trackService *TrackService,
trackUploadService *services.TrackUploadService,
chunkService *services.TrackChunkService,
likeService *services.TrackLikeService,
streamService *services.StreamService,
) *TrackHandler {
return &TrackHandler{
trackService: trackService,
trackUploadService: trackUploadService,
chunkService: chunkService,
likeService: likeService,
streamService: streamService,
}
}
// SetSearchService définit le service de recherche (pour injection de dépendance)
func (h *TrackHandler) SetSearchService(searchService *services.TrackSearchService) {
h.searchService = searchService
}
// SetShareService définit le service de partage (pour injection de dépendance)
func (h *TrackHandler) SetShareService(shareService *services.TrackShareService) {
h.shareService = shareService
}
// SetVersionService définit le service de versioning (pour injection de dépendance)
func (h *TrackHandler) SetVersionService(versionService *services.TrackVersionService) {
h.versionService = versionService
}
// SetHistoryService définit le service d'historique (pour injection de dépendance)
func (h *TrackHandler) SetHistoryService(historyService *services.TrackHistoryService) {
h.historyService = historyService
}
// UploadTrack gère l'upload d'un fichier audio
func (h *TrackHandler) UploadTrack(c *gin.Context) {
userID := c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
fileHeader, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "no file provided"})
return
}
// Upload track (validation et quota sont vérifiés dans le service)
track, err := h.trackService.UploadTrack(c.Request.Context(), userID, fileHeader)
if err != nil {
// Mapper les erreurs vers des messages utilisateur spécifiques
errorMessage := h.mapTrackError(err)
statusCode := h.getErrorStatusCode(err)
c.JSON(statusCode, gin.H{"error": errorMessage})
return
}
// Déclencher le traitement du streaming
if h.streamService != nil {
if err := h.streamService.StartProcessing(c.Request.Context(), track.ID, track.FilePath); err != nil {
// Log error but don't fail request
} else {
// Update status to processing
h.trackUploadService.UpdateUploadStatus(c.Request.Context(), track.ID, models.TrackStatusProcessing, "Processing audio...")
}
}
c.JSON(http.StatusCreated, gin.H{"track": track})
}
// GetUploadStatus récupère le statut d'upload d'un track
func (h *TrackHandler) GetUploadStatus(c *gin.Context) {
trackIDStr := c.Param("id")
if trackIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "track id is required"})
return
}
// MIGRATION UUID: ID est maintenant un UUID
// Le trackID doit être un int64 pour le moment car models.Track n'a pas encore migré l'ID?
// Attends, j'ai migré models.Track ID vers UUID dans l'étape 1.
// Donc trackID est UUID.
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
return
}
// Vérifier que l'utilisateur est autorisé à voir ce track
userID := c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
// Récupérer la progression
// TODO(P2-GO-004): trackUploadService attend int64 - Migration UUID partielle à compléter
// Je dois mettre à jour trackUploadService.
// Pour l'instant, je ne peux pas compiler si je passe UUID.
// Je vais supposer que trackUploadService a été migré ou que je dois le faire.
// Mais la tâche ne mentionnait pas de migrer trackUploadService.
// C'est le problème de dépendance en cascade.
// Je vais convertir en int64 si possible pour que ça compile, ou migrer le service.
// Mais Track.ID est UUID...
// OK, la migration UUID était "complète" pour les modèles principaux.
// Mais les services satellites comme TrackUploadService n'ont pas été migrés.
// C'est la dette technique identifiée dans le rapport.
// Pour que ça compile maintenant, je dois adapter TrackUploadService.
// TODO(P2-GO-004): Migration UUID partielle - trackUploadService nécessite migration vers UUID
// Ou mieux, je vais mettre à jour TrackUploadService après ce fichier.
progress, err := h.trackUploadService.GetUploadProgress(c.Request.Context(), trackID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get upload progress"})
return
}
c.JSON(http.StatusOK, gin.H{"progress": progress})
}
// InitiateChunkedUploadRequest représente la requête pour initialiser un upload par chunks
type InitiateChunkedUploadRequest struct {
TotalChunks int `json:"total_chunks" binding:"required,min=1"`
TotalSize int64 `json:"total_size" binding:"required,min=1"`
Filename string `json:"filename" binding:"required"`
}
// InitiateChunkedUpload initialise un nouvel upload par chunks
func (h *TrackHandler) InitiateChunkedUpload(c *gin.Context) {
userID := c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
var req InitiateChunkedUploadRequest
if err := c.ShouldBindJSON(&req); err != nil {
// GO-013: Utiliser validator pour messages d'erreur plus clairs
validator := validators.NewValidator()
if validationErrs := validator.Validate(&req); len(validationErrs) > 0 {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Validation failed",
"errors": validationErrs,
})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Initialiser l'upload
// InitiateChunkedUpload retourne un string (uploadID) donc pas de souci d'int64
uploadID, err := h.chunkService.InitiateChunkedUpload(userID, req.TotalChunks, req.TotalSize, req.Filename)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"upload_id": uploadID,
"message": "upload initiated successfully",
})
}
// UploadChunkRequest représente la requête pour uploader un chunk
type UploadChunkRequest struct {
UploadID string `form:"upload_id" binding:"required"`
ChunkNumber int `form:"chunk_number" binding:"required,min=1"`
TotalChunks int `form:"total_chunks" binding:"required,min=1"`
TotalSize int64 `form:"total_size" binding:"required,min=1"`
Filename string `form:"filename" binding:"required"`
}
// UploadChunk gère l'upload d'un chunk
func (h *TrackHandler) UploadChunk(c *gin.Context) {
userID := c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
var req UploadChunkRequest
if err := c.ShouldBind(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
fileHeader, err := c.FormFile("chunk")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "no chunk file provided"})
return
}
// Sauvegarder le chunk
if err := h.chunkService.SaveChunk(c.Request.Context(), req.UploadID, req.ChunkNumber, req.TotalChunks, fileHeader); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Récupérer la progression
receivedChunks, progress, err := h.chunkService.GetUploadProgress(req.UploadID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "chunk uploaded successfully",
"upload_id": req.UploadID,
"received_chunks": receivedChunks,
"total_chunks": req.TotalChunks,
"progress": progress,
})
}
// CompleteChunkedUploadRequest représente la requête pour compléter un upload par chunks
type CompleteChunkedUploadRequest struct {
UploadID string `json:"upload_id" binding:"required"`
}
// CompleteChunkedUpload assemble tous les chunks et crée le track final
func (h *TrackHandler) CompleteChunkedUpload(c *gin.Context) {
userID := c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
var req CompleteChunkedUploadRequest
if err := c.ShouldBindJSON(&req); err != nil {
// GO-013: Utiliser validator pour messages d'erreur plus clairs
validator := validators.NewValidator()
if validationErrs := validator.Validate(&req); len(validationErrs) > 0 {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Validation failed",
"errors": validationErrs,
})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Récupérer les informations de l'upload pour obtenir le filename
uploadInfo, err := h.chunkService.GetUploadInfo(req.UploadID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Générer un nom de fichier unique pour le fichier final
timestamp := uuid.New()
ext := filepath.Ext(uploadInfo.Filename)
if ext == "" {
ext = ".mp3" // Par défaut
}
filename := fmt.Sprintf("%s_%s%s", userID.String(), timestamp.String(), ext)
finalPath := filepath.Join("uploads/tracks", userID.String(), filename)
// Assurer que le répertoire existe
if err := os.MkdirAll(filepath.Dir(finalPath), 0755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create directory"})
return
}
// Assembler les chunks
finalFilename, totalSize, md5, err := h.chunkService.CompleteChunkedUpload(c.Request.Context(), req.UploadID, finalPath)
if err != nil {
errorMessage := h.mapTrackError(err)
statusCode := h.getErrorStatusCode(err)
c.JSON(statusCode, gin.H{"error": errorMessage})
return
}
// Vérifier le quota avant de créer le track final
if err := h.trackService.CheckUserQuota(c.Request.Context(), userID, totalSize); err != nil {
errorMessage := h.mapTrackError(err)
statusCode := h.getErrorStatusCode(err)
// Nettoyer le fichier assemblé
os.Remove(finalPath)
c.JSON(statusCode, gin.H{"error": errorMessage})
return
}
// Déterminer le format
ext = filepath.Ext(finalFilename)
format := strings.TrimPrefix(strings.ToUpper(ext), ".")
if format == "M4A" {
format = "AAC"
}
// Créer le track en base en utilisant CreateTrackFromPath
track, err := h.trackService.CreateTrackFromPath(c.Request.Context(), userID, finalPath, finalFilename, totalSize, format)
if err != nil {
// Nettoyer le fichier en cas d'erreur
os.Remove(finalPath)
errorMessage := h.mapTrackError(err)
statusCode := h.getErrorStatusCode(err)
c.JSON(statusCode, gin.H{"error": errorMessage})
return
}
// Mettre à jour le message de statut avec le MD5
if err := h.trackUploadService.UpdateUploadStatus(c.Request.Context(), track.ID, models.TrackStatusUploading, fmt.Sprintf("Upload completed, MD5: %s", md5)); err != nil {
// Log l'erreur mais ne pas faire échouer la requête
h.trackService.logger.Error("Failed to update track upload status after completion", zap.Error(err), zap.Any("track_id", track.ID))
}
// Déclencher le traitement du streaming
if h.streamService != nil {
if err := h.streamService.StartProcessing(c.Request.Context(), track.ID, track.FilePath); err != nil {
// Log error
} else {
// h.trackUploadService.UpdateUploadStatus(c.Request.Context(), track.ID, models.TrackStatusProcessing, "Processing audio...")
}
}
c.JSON(http.StatusCreated, gin.H{
"message": "upload completed successfully",
"track": track,
"md5": md5,
})
}
// mapTrackError mappe les erreurs techniques vers des messages utilisateur
func (h *TrackHandler) mapTrackError(err error) string {
if err == nil {
return "unknown error"
}
errStr := err.Error()
// Erreurs de validation
if strings.Contains(errStr, "invalid track format") || strings.Contains(errStr, "invalid file format") {
return "Invalid file format. Allowed formats: MP3, FLAC, WAV, OGG"
}
if strings.Contains(errStr, "file size exceeds") || strings.Contains(errStr, "too large") {
return "File size exceeds maximum allowed size of 100MB"
}
if strings.Contains(errStr, "file is empty") {
return "The uploaded file is empty"
}
// Erreurs de quota
if strings.Contains(errStr, "track quota exceeded") {
return "You have reached the maximum number of tracks allowed"
}
if strings.Contains(errStr, "storage quota exceeded") {
return "You have reached your storage quota. Please delete some tracks to free up space"
}
// Erreurs réseau
if strings.Contains(errStr, "network error") || strings.Contains(errStr, "timeout") || strings.Contains(errStr, "connection") {
return "Network error occurred. Please try again"
}
// Erreurs de stockage
if strings.Contains(errStr, "storage error") || strings.Contains(errStr, "failed to save file") {
return "Failed to save file. Please try again"
}
if strings.Contains(errStr, "failed to create upload directory") {
return "Failed to prepare storage. Please try again later"
}
// Erreur par défaut
return "An error occurred during upload. Please try again"
}
// getErrorStatusCode retourne le code de statut HTTP approprié pour une erreur
func (h *TrackHandler) getErrorStatusCode(err error) int {
if err == nil {
return http.StatusInternalServerError
}
errStr := err.Error()
// Erreurs de validation -> 400
if strings.Contains(errStr, "invalid") || strings.Contains(errStr, "too large") || strings.Contains(errStr, "empty") {
return http.StatusBadRequest
}
// Erreurs de quota -> 403
if strings.Contains(errStr, "quota exceeded") {
return http.StatusForbidden
}
// Erreurs réseau -> 503 (Service Unavailable)
if strings.Contains(errStr, "network error") || strings.Contains(errStr, "timeout") || strings.Contains(errStr, "connection") {
return http.StatusServiceUnavailable
}
// Erreurs de stockage -> 500
if strings.Contains(errStr, "storage error") || strings.Contains(errStr, "failed to save") {
return http.StatusInternalServerError
}
// Par défaut
return http.StatusInternalServerError
}
// GetUploadQuota récupère les informations de quota d'upload pour un utilisateur
func (h *TrackHandler) GetUploadQuota(c *gin.Context) {
// Récupérer l'ID utilisateur depuis l'URL ou depuis le contexte d'authentification
userIDParam := c.Param("id")
var userID uuid.UUID
var err error
if userIDParam == "" || userIDParam == "me" {
// Si "me" ou vide, utiliser l'utilisateur authentifié
userID = c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
} else {
// Parse UUID
userID, err = uuid.Parse(userIDParam)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
return
}
}
// Vérifier que l'utilisateur peut accéder à ces informations (soit lui-même, soit admin)
authenticatedUserID := c.MustGet("user_id").(uuid.UUID)
if authenticatedUserID == uuid.Nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
// Un utilisateur ne peut voir que son propre quota (sauf admin, mais on simplifie pour l'instant)
if authenticatedUserID != userID {
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden: you can only view your own quota"})
return
}
// Récupérer le quota
quota, err := h.trackService.GetUserQuota(c.Request.Context(), userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get quota"})
return
}
c.JSON(http.StatusOK, gin.H{
"quota": quota,
})
}
// ResumeUpload récupère l'état d'un upload pour permettre la reprise
func (h *TrackHandler) ResumeUpload(c *gin.Context) {
userID := c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
uploadID := c.Param("uploadId")
if uploadID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "upload_id is required"})
return
}
// Récupérer l'état de l'upload
state, err := h.chunkService.GetUploadState(uploadID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "upload not found"})
return
}
// Vérifier que l'upload appartient à l'utilisateur authentifié
if state.UserID != userID {
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden: you can only resume your own uploads"})
return
}
c.JSON(http.StatusOK, gin.H{
"upload_id": state.UploadID,
"user_id": state.UserID,
"total_chunks": state.TotalChunks,
"total_size": state.TotalSize,
"filename": state.Filename,
"chunks_received": state.ChunksReceived,
"received_count": state.ReceivedCount,
"last_chunk": state.LastChunk,
"progress": state.Progress,
"created_at": state.CreatedAt,
"updated_at": state.UpdatedAt,
})
}
// ListTracks gère la liste des tracks avec pagination, filtres et tri
func (h *TrackHandler) ListTracks(c *gin.Context) {
// Récupérer les paramètres de query
page := c.DefaultQuery("page", "1")
limit := c.DefaultQuery("limit", "20")
userIDStr := c.Query("user_id")
genre := c.Query("genre")
format := c.Query("format")
sortBy := c.DefaultQuery("sort_by", "created_at")
sortOrder := c.DefaultQuery("sort_order", "desc")
// Parser les paramètres
var pageInt, limitInt int
if _, err := fmt.Sscanf(page, "%d", &pageInt); err != nil || pageInt < 1 {
pageInt = 1
}
if _, err := fmt.Sscanf(limit, "%d", &limitInt); err != nil || limitInt < 1 {
limitInt = 20
}
// Construire les paramètres
params := TrackListParams{
Page: pageInt,
Limit: limitInt,
SortBy: sortBy,
SortOrder: sortOrder,
}
// Parser user_id si fourni
if userIDStr != "" {
if uid, err := uuid.Parse(userIDStr); err == nil {
params.UserID = &uid
}
}
// Parser genre si fourni
if genre != "" {
params.Genre = &genre
}
// Parser format si fourni
if format != "" {
params.Format = &format
}
// Appeler le service
tracks, total, err := h.trackService.ListTracks(c.Request.Context(), params)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list tracks"})
return
}
// Calculer les métadonnées de pagination
totalPages := (int(total) + limitInt - 1) / limitInt
if totalPages == 0 {
totalPages = 1
}
// Masquer l'URL de stream pour les utilisateurs non authentifiés
_, exists := c.Get("user_id")
if !exists {
for _, t := range tracks {
t.StreamManifestURL = ""
}
}
c.JSON(http.StatusOK, gin.H{
"tracks": tracks,
"pagination": gin.H{
"page": pageInt,
"limit": limitInt,
"total": total,
"total_pages": totalPages,
},
})
}
// GetTrack gère la récupération d'un track par son ID
func (h *TrackHandler) GetTrack(c *gin.Context) {
trackIDStr := c.Param("id")
if trackIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "track id is required"})
return
}
// MIGRATION UUID: TrackID is UUID
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
return
}
track, err := h.trackService.GetTrackByID(c.Request.Context(), trackID)
if err != nil {
if errors.Is(err, ErrTrackNotFound) || errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "track not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get track"})
return
}
// Masquer l'URL de stream pour les utilisateurs non authentifiés
_, exists := c.Get("user_id")
if !exists {
track.StreamManifestURL = ""
}
c.JSON(http.StatusOK, gin.H{"track": track})
}
// UpdateTrackRequest représente la requête de mise à jour d'un track
type UpdateTrackRequest struct {
Title *string `json:"title"`
Artist *string `json:"artist"`
Album *string `json:"album"`
Genre *string `json:"genre"`
Year *int `json:"year"`
IsPublic *bool `json:"is_public"`
}
// UpdateTrack gère la mise à jour d'un track
func (h *TrackHandler) UpdateTrack(c *gin.Context) {
userID := c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
trackIDStr := c.Param("id")
if trackIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "track id is required"})
return
}
// MIGRATION UUID: TrackID is UUID
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
return
}
var req UpdateTrackRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Convertir la requête en paramètres de service
params := UpdateTrackParams{
Title: req.Title,
Artist: req.Artist,
Album: req.Album,
Genre: req.Genre,
Year: req.Year,
IsPublic: req.IsPublic,
}
track, err := h.trackService.UpdateTrack(c.Request.Context(), trackID, userID, params)
if err != nil {
if errors.Is(err, ErrTrackNotFound) || errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "track not found"})
return
}
if errors.Is(err, ErrForbidden) {
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
return
}
// Erreur de validation (title empty, year negative, etc.)
if strings.Contains(err.Error(), "cannot be") {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update track"})
return
}
c.JSON(http.StatusOK, gin.H{"track": track})
}
// DeleteTrack gère la suppression d'un track
func (h *TrackHandler) DeleteTrack(c *gin.Context) {
userID := c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
trackIDStr := c.Param("id")
if trackIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "track id is required"})
return
}
// MIGRATION UUID: TrackID is UUID
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
return
}
err = h.trackService.DeleteTrack(c.Request.Context(), trackID, userID)
if err != nil {
if errors.Is(err, ErrTrackNotFound) || errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "track not found"})
return
}
if errors.Is(err, ErrForbidden) {
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete track"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "track deleted successfully"})
}
// BatchDeleteRequest représente la requête pour supprimer plusieurs tracks
type BatchDeleteRequest struct {
TrackIDs []string `json:"track_ids" binding:"required"`
}
// BatchDeleteTracks gère la suppression en lot de plusieurs tracks
func (h *TrackHandler) BatchDeleteTracks(c *gin.Context) {
userID := c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
var req BatchDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Valider que la liste n'est pas vide
if len(req.TrackIDs) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "track_ids cannot be empty"})
return
}
// Convertir les IDs en UUIDs
var trackUUIDs []uuid.UUID
for _, idStr := range req.TrackIDs {
if uid, err := uuid.Parse(idStr); err == nil {
trackUUIDs = append(trackUUIDs, uid)
}
}
result, err := h.trackService.BatchDeleteTracks(c.Request.Context(), trackUUIDs, userID)
if err != nil {
// Vérifier si c'est une erreur de taille de batch
if strings.Contains(err.Error(), "batch size exceeds maximum") {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete tracks"})
return
}
c.JSON(http.StatusOK, gin.H{
"deleted": result.Deleted,
"failed": result.Failed,
})
}
// BatchUpdateRequest représente la requête pour mettre à jour plusieurs tracks
type BatchUpdateRequest struct {
TrackIDs []string `json:"track_ids" binding:"required"`
Updates map[string]interface{} `json:"updates" binding:"required"`
}
// BatchUpdateTracks gère la mise à jour en lot de plusieurs tracks
func (h *TrackHandler) BatchUpdateTracks(c *gin.Context) {
userID := c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
var req BatchUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Valider que la liste n'est pas vide
if len(req.TrackIDs) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "track_ids cannot be empty"})
return
}
// Valider que les updates ne sont pas vides
if len(req.Updates) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "updates cannot be empty"})
return
}
// Convertir les IDs en UUIDs
var trackUUIDs []uuid.UUID
for _, idStr := range req.TrackIDs {
if uid, err := uuid.Parse(idStr); err == nil {
trackUUIDs = append(trackUUIDs, uid)
}
}
result, err := h.trackService.BatchUpdateTracks(c.Request.Context(), trackUUIDs, userID, req.Updates)
if err != nil {
// Vérifier si c'est une erreur de validation
if strings.Contains(err.Error(), "batch size exceeds maximum") ||
strings.Contains(err.Error(), "cannot be empty") ||
strings.Contains(err.Error(), "invalid value") ||
strings.Contains(err.Error(), "exceeds maximum length") ||
strings.Contains(err.Error(), "must be between") {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update tracks"})
return
}
c.JSON(http.StatusOK, gin.H{
"updated": result.Updated,
"failed": result.Failed,
})
}
// LikeTrack gère l'ajout d'un like sur un track
func (h *TrackHandler) LikeTrack(c *gin.Context) {
userID := c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
trackIDStr := c.Param("id")
if trackIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "track id is required"})
return
}
// MIGRATION UUID: TrackID is UUID
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
return
}
if err := h.likeService.LikeTrack(c.Request.Context(), userID, trackID); 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
}
c.JSON(http.StatusOK, gin.H{"message": "track liked"})
}
// UnlikeTrack gère la suppression d'un like sur un track
func (h *TrackHandler) UnlikeTrack(c *gin.Context) {
userID := c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
trackIDStr := c.Param("id")
if trackIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "track id is required"})
return
}
// MIGRATION UUID: TrackID is UUID
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
return
}
if err := h.likeService.UnlikeTrack(c.Request.Context(), userID, trackID); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "track unliked"})
}
// GetTrackLikes gère la récupération du nombre de likes d'un track
func (h *TrackHandler) GetTrackLikes(c *gin.Context) {
trackIDStr := c.Param("id")
if trackIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "track id is required"})
return
}
// MIGRATION UUID: TrackID is UUID
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
return
}
count, err := h.likeService.GetTrackLikesCount(c.Request.Context(), trackID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Vérifier si l'utilisateur a liké ce track (optionnel)
var isLiked bool
if userIDInterface, exists := c.Get("user_id"); exists {
userID, ok := userIDInterface.(uuid.UUID)
if ok && userID != uuid.Nil {
isLiked, _ = h.likeService.IsLiked(c.Request.Context(), userID, trackID)
}
}
c.JSON(http.StatusOK, gin.H{
"count": count,
"is_liked": isLiked,
})
}
// GetUserLikedTracks gère la récupération des tracks likés par un utilisateur
func (h *TrackHandler) GetUserLikedTracks(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
}
// Parse pagination parameters
limit := 20 // default
if limitStr := c.Query("limit"); limitStr != "" {
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
limit = parsedLimit
}
}
offset := 0 // default
if offsetStr := c.Query("offset"); offsetStr != "" {
if parsedOffset, err := strconv.Atoi(offsetStr); err == nil && parsedOffset >= 0 {
offset = parsedOffset
}
}
tracks, err := h.likeService.GetUserLikedTracks(c.Request.Context(), userID, limit, offset)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
total, err := h.likeService.GetUserLikedTracksCount(c.Request.Context(), userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"tracks": tracks,
"total": total,
"limit": limit,
"offset": offset,
})
}
// SearchTracks gère la recherche avancée de tracks
func (h *TrackHandler) SearchTracks(c *gin.Context) {
if h.searchService == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "search service not available"})
return
}
// Récupérer les paramètres de query
params := services.TrackSearchParams{
Query: c.Query("q"),
TagMode: c.DefaultQuery("tag_mode", "OR"),
Page: 1,
Limit: 20,
SortBy: c.DefaultQuery("sort_by", "created_at"),
SortOrder: c.DefaultQuery("sort_order", "desc"),
}
// Parser page
if pageStr := c.Query("page"); pageStr != "" {
if page, err := strconv.Atoi(pageStr); err == nil && page > 0 {
params.Page = page
}
}
// Parser limit
if limitStr := c.Query("limit"); limitStr != "" {
if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 {
params.Limit = limit
}
}
// Parser tags
if tagsStr := c.Query("tags"); tagsStr != "" {
params.Tags = strings.Split(tagsStr, ",")
for i := range params.Tags {
params.Tags[i] = strings.TrimSpace(params.Tags[i])
}
}
// Parser min_duration
if minDurationStr := c.Query("min_duration"); minDurationStr != "" {
if minDuration, err := strconv.Atoi(minDurationStr); err == nil && minDuration >= 0 {
params.MinDuration = &minDuration
}
}
// Parser max_duration
if maxDurationStr := c.Query("max_duration"); maxDurationStr != "" {
if maxDuration, err := strconv.Atoi(maxDurationStr); err == nil && maxDuration >= 0 {
params.MaxDuration = &maxDuration
}
}
// Parser min_bpm
if minBPMStr := c.Query("min_bpm"); minBPMStr != "" {
if minBPM, err := strconv.Atoi(minBPMStr); err == nil && minBPM >= 0 {
params.MinBPM = &minBPM
}
}
// Parser max_bpm
if maxBPMStr := c.Query("max_bpm"); maxBPMStr != "" {
if maxBPM, err := strconv.Atoi(maxBPMStr); err == nil && maxBPM >= 0 {
params.MaxBPM = &maxBPM
}
}
// Parser genre
if genre := c.Query("genre"); genre != "" {
params.Genre = &genre
}
// Parser format
if format := c.Query("format"); format != "" {
params.Format = &format
}
// Parser min_date
if minDate := c.Query("min_date"); minDate != "" {
params.MinDate = &minDate
}
// Parser max_date
if maxDate := c.Query("max_date"); maxDate != "" {
params.MaxDate = &maxDate
}
// Effectuer la recherche avec filtres combinés
tracks, total, err := h.searchService.SearchTracks(c.Request.Context(), params)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to search tracks"})
return
}
// Calculer les métadonnées de pagination
totalPages := (int(total) + params.Limit - 1) / params.Limit
if totalPages == 0 {
totalPages = 1
}
c.JSON(http.StatusOK, gin.H{
"tracks": tracks,
"pagination": gin.H{
"page": params.Page,
"limit": params.Limit,
"total": total,
"total_pages": totalPages,
},
})
}
// DownloadTrack gère le téléchargement d'un track
func (h *TrackHandler) DownloadTrack(c *gin.Context) {
// Récupérer l'utilisateur s'il est authentifié
var userID uuid.UUID
if userIDInterface, exists := c.Get("user_id"); exists {
if uid, ok := userIDInterface.(uuid.UUID); ok {
userID = uid
}
}
trackIDStr := c.Param("id")
if trackIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "track id is required"})
return
}
// MIGRATION UUID: TrackID is UUID
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
return
}
// Récupérer le track
track, err := h.trackService.GetTrackByID(c.Request.Context(), trackID)
if err != nil {
if errors.Is(err, ErrTrackNotFound) || errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "track not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get track"})
return
}
// Vérifier les permissions via share token si présent
if shareToken := c.Query("share_token"); shareToken != "" {
if h.shareService == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "share service not available"})
return
}
share, err := h.shareService.ValidateShareToken(c.Request.Context(), shareToken)
if err != nil {
if errors.Is(err, services.ErrShareNotFound) {
c.JSON(http.StatusForbidden, gin.H{"error": "invalid share token"})
return
}
if errors.Is(err, services.ErrShareExpired) {
c.JSON(http.StatusForbidden, gin.H{"error": "share link expired"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to validate share token"})
return
}
// Vérifier que le share correspond au track
if share.TrackID != trackID {
c.JSON(http.StatusForbidden, gin.H{"error": "invalid share token"})
return
}
// Vérifier la permission download
if !h.shareService.CheckPermission(share, "download") {
c.JSON(http.StatusForbidden, gin.H{"error": "download not allowed"})
return
}
} else {
// Vérifier les permissions normales (public ou owner)
if !track.IsPublic && track.UserID != userID {
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
return
}
}
// Vérifier que le fichier existe
if _, err := os.Stat(track.FilePath); os.IsNotExist(err) {
c.JSON(http.StatusNotFound, gin.H{"error": "track file not found"})
return
}
// Servir le fichier avec les headers appropriés
c.Header("Content-Type", getContentType(track.Format))
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", track.Title))
c.File(track.FilePath)
}
// CreateShareRequest représente la requête pour créer un lien de partage
type CreateShareRequest struct {
Permissions string `json:"permissions" binding:"required"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
}
// CreateShare crée un nouveau lien de partage pour un track
func (h *TrackHandler) CreateShare(c *gin.Context) {
userID := c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
trackIDStr := c.Param("id")
if trackIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "track id is required"})
return
}
// MIGRATION UUID: TrackID is UUID
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
return
}
if h.shareService == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "share service not available"})
return
}
var req CreateShareRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
share, err := h.shareService.CreateShare(c.Request.Context(), trackID, userID, req.Permissions, req.ExpiresAt)
if err != nil {
if errors.Is(err, ErrForbidden) {
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
return
}
if errors.Is(err, ErrTrackNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "track not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create share"})
return
}
c.JSON(http.StatusOK, gin.H{"share": share})
}
// GetSharedTrack récupère un track via son token de partage
func (h *TrackHandler) GetSharedTrack(c *gin.Context) {
token := c.Param("token")
if token == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "share token is required"})
return
}
if h.shareService == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "share service not available"})
return
}
share, err := h.shareService.ValidateShareToken(c.Request.Context(), token)
if err != nil {
if errors.Is(err, services.ErrShareNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "invalid share token"})
return
}
if errors.Is(err, services.ErrShareExpired) {
c.JSON(http.StatusForbidden, gin.H{"error": "share link expired"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to validate share token"})
return
}
// Récupérer le track
track, err := h.trackService.GetTrackByID(c.Request.Context(), share.TrackID)
if err != nil {
if errors.Is(err, ErrTrackNotFound) || errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "track not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get track"})
return
}
c.JSON(http.StatusOK, gin.H{
"track": track,
"share": share,
})
}
// RevokeShare révoque un lien de partage
func (h *TrackHandler) RevokeShare(c *gin.Context) {
userID := c.MustGet("user_id").(uuid.UUID)
if userID == uuid.Nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
shareIDStr := c.Param("id")
if shareIDStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "share id is required"})
return
}
// MIGRATION UUID: ShareID is UUID
shareID, err := uuid.Parse(shareIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid share id"})
return
}
if h.shareService == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "share service not available"})
return
}
err = h.shareService.RevokeShare(c.Request.Context(), shareID, userID)
if err != nil {
if errors.Is(err, services.ErrShareNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "share not found"})
return
}
if errors.Is(err, services.ErrForbidden) {
c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to revoke share"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "share revoked"})
}
// StreamCallbackRequest represents the request for stream status callback
type StreamCallbackRequest struct {
Status string `json:"status" binding:"required"`
ManifestURL string `json:"manifest_url"`
Error string `json:"error"`
}
// HandleStreamCallback handles the callback from stream server
func (h *TrackHandler) HandleStreamCallback(c *gin.Context) {
trackIDStr := c.Param("id")
// MIGRATION UUID: TrackID is UUID
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
return
}
var req StreamCallbackRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.trackService.UpdateStreamStatus(c.Request.Context(), trackID, req.Status, req.ManifestURL); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update stream status"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "status updated"})
}
// GetTrackStats stub
func (h *TrackHandler) GetTrackStats(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented"})
}
// GetTrackHistory stub
func (h *TrackHandler) GetTrackHistory(c *gin.Context) {
c.JSON(http.StatusNotImplemented, gin.H{"error": "Not implemented"})
}
// getContentType retourne le Content-Type approprié pour un format audio
func getContentType(format string) string {
switch strings.ToUpper(format) {
case "MP3":
return "audio/mpeg"
case "FLAC":
return "audio/flac"
case "WAV":
return "audio/wav"
case "OGG":
return "audio/ogg"
case "AAC", "M4A":
return "audio/aac"
default:
return "application/octet-stream"
}
}