package track import ( "errors" "fmt" "github.com/google/uuid" "net/http" "os" "path/filepath" "strings" "time" "strconv" "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" } }