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

1239 lines
43 KiB
Go
Raw Normal View History

2025-12-03 19:29:37 +00:00
package handlers
import (
"errors"
2025-12-03 19:29:37 +00:00
"net/http"
"strconv"
2025-12-16 16:23:49 +00:00
"time"
2025-12-03 19:29:37 +00:00
2025-12-16 16:23:49 +00:00
apperrors "veza-backend-api/internal/errors"
2025-12-03 19:29:37 +00:00
"veza-backend-api/internal/models"
2025-12-16 18:34:08 +00:00
"veza-backend-api/internal/monitoring"
2025-12-03 19:29:37 +00:00
"veza-backend-api/internal/services"
"veza-backend-api/internal/utils"
2025-12-03 19:29:37 +00:00
"github.com/gin-gonic/gin"
"github.com/google/uuid"
P0: stabilisation backend/chat/stream + nouvelle base migrations v1 Backend Go: - Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN. - Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError). - Sécurisation de config.go, CORS, statuts de santé et monitoring. - Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles). - Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés. - Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*. Chat server (Rust): - Refonte du pipeline JWT + sécurité, audit et rate limiting avancé. - Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing). - Nettoyage des panics, gestion d’erreurs robuste, logs structurés. - Migrations chat alignées sur le schéma UUID et nouvelles features. Stream server (Rust): - Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core. - Transactions P0 pour les jobs et segments, garanties d’atomicité. - Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION). Documentation & audits: - TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services. - Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3). - Scripts de reset et de cleanup pour la lab DB et la V1. Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
"go.uber.org/zap"
"gorm.io/gorm"
2025-12-03 19:29:37 +00:00
)
// PlaylistHandler gère les opérations sur les playlists
type PlaylistHandler struct {
playlistService services.PlaylistServiceInterface
2025-12-03 19:29:37 +00:00
playlistAnalyticsService *services.PlaylistAnalyticsService
playlistFollowService *services.PlaylistFollowService
P0: stabilisation backend/chat/stream + nouvelle base migrations v1 Backend Go: - Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN. - Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError). - Sécurisation de config.go, CORS, statuts de santé et monitoring. - Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles). - Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés. - Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*. Chat server (Rust): - Refonte du pipeline JWT + sécurité, audit et rate limiting avancé. - Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing). - Nettoyage des panics, gestion d’erreurs robuste, logs structurés. - Migrations chat alignées sur le schéma UUID et nouvelles features. Stream server (Rust): - Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core. - Transactions P0 pour les jobs et segments, garanties d’atomicité. - Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION). Documentation & audits: - TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services. - Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3). - Scripts de reset et de cleanup pour la lab DB et la V1. Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
db *gorm.DB
commonHandler *CommonHandler
2025-12-03 19:29:37 +00:00
}
// NewPlaylistHandler crée un nouveau handler de playlists
P0: stabilisation backend/chat/stream + nouvelle base migrations v1 Backend Go: - Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN. - Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError). - Sécurisation de config.go, CORS, statuts de santé et monitoring. - Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles). - Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés. - Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*. Chat server (Rust): - Refonte du pipeline JWT + sécurité, audit et rate limiting avancé. - Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing). - Nettoyage des panics, gestion d’erreurs robuste, logs structurés. - Migrations chat alignées sur le schéma UUID et nouvelles features. Stream server (Rust): - Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core. - Transactions P0 pour les jobs et segments, garanties d’atomicité. - Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION). Documentation & audits: - TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services. - Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3). - Scripts de reset et de cleanup pour la lab DB et la V1. Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
func NewPlaylistHandler(playlistService *services.PlaylistService, db *gorm.DB, logger *zap.Logger) *PlaylistHandler {
return &PlaylistHandler{
playlistService: playlistService,
db: db,
commonHandler: NewCommonHandler(logger),
}
2025-12-03 19:29:37 +00:00
}
// NewPlaylistHandlerWithInterface crée un nouveau handler avec l'interface service (pour les tests)
func NewPlaylistHandlerWithInterface(playlistService services.PlaylistServiceInterface, db *gorm.DB, logger *zap.Logger) *PlaylistHandler {
return &PlaylistHandler{
playlistService: playlistService,
db: db,
commonHandler: NewCommonHandler(logger),
}
}
2025-12-03 19:29:37 +00:00
// SetPlaylistAnalyticsService définit le service d'analytics de playlist
// T0491: Create Playlist Analytics Backend
func (h *PlaylistHandler) SetPlaylistAnalyticsService(analyticsService *services.PlaylistAnalyticsService) {
h.playlistAnalyticsService = analyticsService
}
// SetPlaylistFollowService définit le service de follow de playlist
// T0498: Create Playlist Recommendations
func (h *PlaylistHandler) SetPlaylistFollowService(followService *services.PlaylistFollowService) {
h.playlistFollowService = followService
}
// CreatePlaylistRequest représente la requête pour créer une playlist
2025-12-16 18:34:08 +00:00
// MOD-P1-001: Ajout tags validate pour validation systématique (Description manquait)
2025-12-03 19:29:37 +00:00
type CreatePlaylistRequest struct {
Title string `json:"title" binding:"required,min=1,max=200" validate:"required,min=1,max=200"`
2025-12-16 18:34:08 +00:00
Description string `json:"description,omitempty" validate:"omitempty,max=1000"`
2025-12-03 19:29:37 +00:00
IsPublic bool `json:"is_public"`
}
// UpdatePlaylistRequest représente la requête pour mettre à jour une playlist
2025-12-16 18:34:08 +00:00
// MOD-P1-001: Ajout tags validate pour validation systématique (Description manquait)
2025-12-03 19:29:37 +00:00
type UpdatePlaylistRequest struct {
Title *string `json:"title,omitempty" binding:"omitempty,min=1,max=200" validate:"omitempty,min=1,max=200"`
2025-12-16 18:34:08 +00:00
Description *string `json:"description,omitempty" validate:"omitempty,max=1000"`
2025-12-03 19:29:37 +00:00
IsPublic *bool `json:"is_public,omitempty"`
}
// ReorderTracksRequest représente la requête pour réorganiser les tracks
type ReorderTracksRequest struct {
TrackIDs []uuid.UUID `json:"track_ids" binding:"required,min=1" validate:"required,min=1"` // Changed to []uuid.UUID
2025-12-03 19:29:37 +00:00
}
// CreatePlaylist gère la création d'une playlist
// @Summary Create Playlist
// @Description Create a new playlist
// @Tags Playlist
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body CreatePlaylistRequest true "Playlist Metadata"
// @Success 201 {object} APIResponse{data=object{playlist=models.Playlist}}
// @Failure 400 {object} APIResponse "Validation Error"
// @Failure 401 {object} APIResponse "Unauthorized"
// @Failure 500 {object} APIResponse "Internal Error"
// @Router /playlists [post]
2025-12-03 19:29:37 +00:00
func (h *PlaylistHandler) CreatePlaylist(c *gin.Context) {
2025-12-16 16:23:49 +00:00
// MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel
userID, ok := GetUserIDUUID(c)
if !ok {
return // Erreur déjà envoyée par GetUserIDUUID
2025-12-03 19:29:37 +00:00
}
var req CreatePlaylistRequest
P0: stabilisation backend/chat/stream + nouvelle base migrations v1 Backend Go: - Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN. - Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError). - Sécurisation de config.go, CORS, statuts de santé et monitoring. - Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles). - Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés. - Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*. Chat server (Rust): - Refonte du pipeline JWT + sécurité, audit et rate limiting avancé. - Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing). - Nettoyage des panics, gestion d’erreurs robuste, logs structurés. - Migrations chat alignées sur le schéma UUID et nouvelles features. Stream server (Rust): - Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core. - Transactions P0 pour les jobs et segments, garanties d’atomicité. - Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION). Documentation & audits: - TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services. - Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3). - Scripts de reset et de cleanup pour la lab DB et la V1. Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
2025-12-03 19:29:37 +00:00
return
}
// BE-SEC-009: Sanitize user inputs to prevent XSS and injection attacks
req.Title = utils.SanitizeText(req.Title, 200)
if req.Description != "" {
req.Description = utils.SanitizeText(req.Description, 1000)
}
2025-12-16 16:23:49 +00:00
// MOD-P1-004: Ajouter timeout context pour opération DB critique
ctx, cancel := WithTimeout(c.Request.Context(), 5*time.Second)
defer cancel()
playlist, err := h.playlistService.CreatePlaylist(ctx, userID, req.Title, req.Description, req.IsPublic)
2025-12-03 19:29:37 +00:00
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to create playlist", err))
2025-12-03 19:29:37 +00:00
return
}
2025-12-16 18:34:08 +00:00
// MOD-P2-003: Enregistrer la métrique business (depuis le handler pour éviter cycle d'import)
monitoring.RecordPlaylistCreated()
RespondSuccess(c, http.StatusCreated, gin.H{"playlist": playlist})
2025-12-03 19:29:37 +00:00
}
// ImportPlaylistRequest represents JSON import payload (v0.10.4 F145)
type ImportPlaylistRequest struct {
Playlist struct {
Title string `json:"title"`
Description string `json:"description"`
IsPublic bool `json:"is_public"`
} `json:"playlist"`
Tracks []struct {
ID string `json:"id"`
} `json:"tracks"`
}
// ImportPlaylist gère l'import d'une playlist depuis JSON (v0.10.4 F145)
func (h *PlaylistHandler) ImportPlaylist(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
var req ImportPlaylistRequest
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
return
}
title := req.Playlist.Title
if title != "" {
title = utils.SanitizeText(title, 200)
} else {
title = "Imported Playlist"
}
description := req.Playlist.Description
if description != "" {
description = utils.SanitizeText(description, 1000)
}
trackIDs := make([]uuid.UUID, 0, len(req.Tracks))
for _, t := range req.Tracks {
if t.ID == "" {
continue
}
id, err := uuid.Parse(t.ID)
if err != nil {
continue
}
trackIDs = append(trackIDs, id)
}
ctx, cancel := WithTimeout(c.Request.Context(), 30*time.Second)
defer cancel()
playlist, err := h.playlistService.ImportPlaylistWithTracks(ctx, userID, title, description, req.Playlist.IsPublic, trackIDs)
if err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to import playlist", err))
return
}
RespondSuccess(c, http.StatusCreated, gin.H{"playlist": playlist})
}
// GetFavorisPlaylist returns the current user's Favoris playlist, creating it if needed (v0.10.4 F136)
func (h *PlaylistHandler) GetFavorisPlaylist(c *gin.Context) {
userID, ok := GetUserIDUUID(c)
if !ok {
return
}
ctx, cancel := WithTimeout(c.Request.Context(), 5*time.Second)
defer cancel()
playlist, err := h.playlistService.GetOrCreateFavorisPlaylist(ctx, userID)
if err != nil {
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get Favoris playlist", err))
return
}
RespondSuccess(c, http.StatusOK, gin.H{"playlist": playlist})
}
2025-12-03 19:29:37 +00:00
// GetPlaylists gère la récupération des playlists avec pagination
// @Summary Get Playlists
// @Description Get a paginated list of playlists
// @Tags Playlist
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param page query int false "Page number" default(1)
// @Param limit query int false "Items per page" default(20)
// @Param user_id query string false "Filter by User ID"
// @Success 200 {object} APIResponse{data=object{playlists=[]models.Playlist,pagination=object}}
// @Failure 500 {object} APIResponse "Internal Error"
// @Router /playlists [get]
2025-12-03 19:29:37 +00:00
func (h *PlaylistHandler) GetPlaylists(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
// Bounds checking: return 400 with clear message instead of silently normalizing
if page < 1 || limit < 1 || limit > 100 {
msg := "pagination: page must be >= 1 and limit must be between 1 and 100"
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, msg))
return
2025-12-03 19:29:37 +00:00
}
// Filtres optionnels
var filterUserID *uuid.UUID
if filterUserIDStr := c.Query("user_id"); filterUserIDStr != "" {
if uid, err := uuid.Parse(filterUserIDStr); err == nil {
filterUserID = &uid
}
}
// Get current user ID
var currentUserID *uuid.UUID
if uidInterface, exists := c.Get("user_id"); exists {
h.commonHandler.logger.Debug("GetPlaylists: user_id found in context", zap.Any("value", uidInterface))
2025-12-03 19:29:37 +00:00
if uid, ok := uidInterface.(uuid.UUID); ok {
currentUserID = &uid
} else {
h.commonHandler.logger.Debug("GetPlaylists: user_id type assertion failed")
2025-12-03 19:29:37 +00:00
}
} else {
h.commonHandler.logger.Debug("GetPlaylists: user_id not found in context")
2025-12-03 19:29:37 +00:00
}
2025-12-16 16:23:49 +00:00
// MOD-P1-004: Ajouter timeout context pour opération DB
ctx, cancel := WithTimeout(c.Request.Context(), 5*time.Second)
defer cancel()
playlists, total, err := h.playlistService.GetPlaylists(ctx, currentUserID, filterUserID, page, limit)
2025-12-03 19:29:37 +00:00
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get playlists", err))
2025-12-03 19:29:37 +00:00
return
}
RespondSuccess(c, http.StatusOK, gin.H{
2025-12-03 19:29:37 +00:00
"playlists": playlists,
"total": total,
"page": page,
"limit": limit,
})
}
// GetPlaylist gère la récupération d'une playlist
// @Summary Get Playlist by ID
// @Description Get detailed information about a playlist
// @Tags Playlist
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Playlist ID"
// @Success 200 {object} APIResponse{data=object{playlist=models.Playlist}}
// @Failure 400 {object} APIResponse "Invalid ID"
// @Failure 404 {object} APIResponse "Playlist not found"
// @Router /playlists/{id} [get]
2025-12-03 19:29:37 +00:00
func (h *PlaylistHandler) GetPlaylist(c *gin.Context) {
// Playlist IDs are uuid.UUID
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
2025-12-03 19:29:37 +00:00
return
}
var currentUserID *uuid.UUID
if uidInterface, exists := c.Get("user_id"); exists {
if uid, ok := uidInterface.(uuid.UUID); ok {
currentUserID = &uid
}
}
2025-12-16 16:23:49 +00:00
// MOD-P1-004: Ajouter timeout context pour opération DB
ctx, cancel := WithTimeout(c.Request.Context(), 5*time.Second)
defer cancel()
playlist, err := h.playlistService.GetPlaylist(ctx, playlistID, currentUserID)
2025-12-03 19:29:37 +00:00
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
if errors.Is(err, services.ErrPlaylistNotFound) || errors.Is(err, services.ErrAccessDenied) {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
2025-12-03 19:29:37 +00:00
return
}
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get playlist", err))
2025-12-03 19:29:37 +00:00
return
}
RespondSuccess(c, http.StatusOK, playlist)
2025-12-03 19:29:37 +00:00
}
// GetPlaylistByShareToken returns a playlist by its public share token (v0.10.4 F143).
// No authentication required.
func (h *PlaylistHandler) GetPlaylistByShareToken(c *gin.Context) {
token := c.Param("token")
if token == "" {
RespondWithAppError(c, apperrors.NewValidationError("share token is required"))
return
}
ctx, cancel := WithTimeout(c.Request.Context(), 5*time.Second)
defer cancel()
playlist, err := h.playlistService.GetPlaylistByShareToken(ctx, token)
if err != nil {
if errors.Is(err, services.ErrPlaylistNotFound) {
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
return
}
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get playlist", err))
return
}
RespondSuccess(c, http.StatusOK, playlist)
}
2025-12-03 19:29:37 +00:00
// UpdatePlaylist gère la mise à jour d'une playlist
// @Summary Update Playlist
// @Description Update playlist metadata
// @Tags Playlist
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Playlist ID"
// @Param playlist body UpdatePlaylistRequest true "Playlist Metadata"
// @Success 200 {object} APIResponse{data=object{playlist=models.Playlist}}
// @Failure 400 {object} APIResponse "Validation Error"
// @Failure 401 {object} APIResponse "Unauthorized"
// @Failure 403 {object} APIResponse "Forbidden"
// @Failure 404 {object} APIResponse "Playlist not found"
// @Router /playlists/{id} [put]
2025-12-03 19:29:37 +00:00
func (h *PlaylistHandler) UpdatePlaylist(c *gin.Context) {
2025-12-16 16:23:49 +00:00
// MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel
userID, ok := GetUserIDUUID(c)
if !ok {
return // Erreur déjà envoyée par GetUserIDUUID
2025-12-03 19:29:37 +00:00
}
// Playlist IDs are uuid.UUID
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
2025-12-03 19:29:37 +00:00
return
}
var req UpdatePlaylistRequest
P0: stabilisation backend/chat/stream + nouvelle base migrations v1 Backend Go: - Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN. - Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError). - Sécurisation de config.go, CORS, statuts de santé et monitoring. - Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles). - Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés. - Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*. Chat server (Rust): - Refonte du pipeline JWT + sécurité, audit et rate limiting avancé. - Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing). - Nettoyage des panics, gestion d’erreurs robuste, logs structurés. - Migrations chat alignées sur le schéma UUID et nouvelles features. Stream server (Rust): - Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core. - Transactions P0 pour les jobs et segments, garanties d’atomicité. - Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION). Documentation & audits: - TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services. - Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3). - Scripts de reset et de cleanup pour la lab DB et la V1. Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
2025-12-03 19:29:37 +00:00
return
}
// BE-SEC-009: Sanitize user inputs to prevent XSS and injection attacks
if req.Title != nil {
sanitized := utils.SanitizeText(*req.Title, 200)
req.Title = &sanitized
}
if req.Description != nil {
sanitized := utils.SanitizeText(*req.Description, 1000)
req.Description = &sanitized
}
2025-12-16 16:23:49 +00:00
// MOD-P1-004: Ajouter timeout context pour opération DB
ctx, cancel := WithTimeout(c.Request.Context(), 5*time.Second)
defer cancel()
playlist, err := h.playlistService.UpdatePlaylist(ctx, playlistID, userID, req.Title, req.Description, req.IsPublic)
2025-12-03 19:29:37 +00:00
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
if errors.Is(err, services.ErrPlaylistNotFound) {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
2025-12-03 19:29:37 +00:00
return
}
if errors.Is(err, services.ErrAccessDenied) {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
2025-12-03 19:29:37 +00:00
return
}
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to update playlist", err))
2025-12-03 19:29:37 +00:00
return
}
RespondSuccess(c, http.StatusOK, playlist)
2025-12-03 19:29:37 +00:00
}
// DeletePlaylist gère la suppression d'une playlist
// @Summary Delete Playlist
// @Description Permanently delete a playlist
// @Tags Playlist
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Playlist ID"
// @Success 200 {object} APIResponse{data=object{message=string}}
// @Failure 401 {object} APIResponse "Unauthorized"
// @Failure 403 {object} APIResponse "Forbidden"
// @Failure 404 {object} APIResponse "Playlist not found"
// @Router /playlists/{id} [delete]
2025-12-03 19:29:37 +00:00
func (h *PlaylistHandler) DeletePlaylist(c *gin.Context) {
2025-12-16 16:23:49 +00:00
// MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel
userID, ok := GetUserIDUUID(c)
if !ok {
return // Erreur déjà envoyée par GetUserIDUUID
2025-12-03 19:29:37 +00:00
}
// Playlist IDs are uuid.UUID
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
2025-12-03 19:29:37 +00:00
return
}
2025-12-16 16:23:49 +00:00
// MOD-P1-004: Ajouter timeout context pour opération DB
ctx, cancel := WithTimeout(c.Request.Context(), 5*time.Second)
defer cancel()
if err := h.playlistService.DeletePlaylist(ctx, playlistID, userID); err != nil {
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
if errors.Is(err, services.ErrPlaylistNotFound) {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
2025-12-03 19:29:37 +00:00
return
}
if errors.Is(err, services.ErrAccessDenied) {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
2025-12-03 19:29:37 +00:00
return
}
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to delete playlist", err))
2025-12-03 19:29:37 +00:00
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "playlist deleted"})
2025-12-03 19:29:37 +00:00
}
// AddTrack gère l'ajout d'un track à une playlist
// @Summary Add Track to Playlist
// @Description Add a track to the playlist
// @Tags Playlist
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Playlist ID"
// @Param trackId body object{track_id=string} true "Track ID (in body)"
// @Success 200 {object} APIResponse{data=object{message=string}}
// @Failure 400 {object} APIResponse "Track already present or invalid ID"
// @Failure 404 {object} APIResponse "Playlist or Track not found"
// @Router /playlists/{id}/tracks [post]
2025-12-03 19:29:37 +00:00
func (h *PlaylistHandler) AddTrack(c *gin.Context) {
2025-12-16 16:23:49 +00:00
// MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel
userID, ok := GetUserIDUUID(c)
if !ok {
return // Erreur déjà envoyée par GetUserIDUUID
2025-12-03 19:29:37 +00:00
}
// Playlist IDs are uuid.UUID
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
2025-12-03 19:29:37 +00:00
return
}
// Track IDs are uuid.UUID
trackID, err := uuid.Parse(c.Param("trackId")) // Changed to uuid.Parse
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewValidationError("invalid track id"))
2025-12-03 19:29:37 +00:00
return
}
if err := h.playlistService.AddTrack(c.Request.Context(), playlistID, trackID, userID); err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
if errors.Is(err, services.ErrPlaylistNotFound) {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
2025-12-03 19:29:37 +00:00
return
}
if errors.Is(err, services.ErrTrackNotFound) {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewNotFoundError("track"))
2025-12-03 19:29:37 +00:00
return
}
if errors.Is(err, services.ErrTrackAlreadyInPlaylist) {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewValidationError("track already in playlist"))
2025-12-03 19:29:37 +00:00
return
}
if errors.Is(err, services.ErrAccessDenied) {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
2025-12-03 19:29:37 +00:00
return
}
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to add track to playlist", err))
2025-12-03 19:29:37 +00:00
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "track added to playlist"})
2025-12-03 19:29:37 +00:00
}
// RemoveTrack gère la suppression d'un track d'une playlist
// @Summary Remove Track from Playlist
// @Description Remove a track from the playlist
// @Tags Playlist
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Playlist ID"
// @Param trackId path string true "Track ID"
// @Success 200 {object} APIResponse{data=object{message=string}}
// @Failure 404 {object} APIResponse "Playlist or Track not found"
// @Router /playlists/{id}/tracks/{trackId} [delete]
2025-12-03 19:29:37 +00:00
func (h *PlaylistHandler) RemoveTrack(c *gin.Context) {
2025-12-16 16:23:49 +00:00
// MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel
userID, ok := GetUserIDUUID(c)
if !ok {
return // Erreur déjà envoyée par GetUserIDUUID
2025-12-03 19:29:37 +00:00
}
// Playlist IDs are uuid.UUID
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
2025-12-03 19:29:37 +00:00
return
}
// Track IDs are uuid.UUID
trackID, err := uuid.Parse(c.Param("trackId")) // Changed to uuid.Parse
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewValidationError("invalid track id"))
2025-12-03 19:29:37 +00:00
return
}
if err := h.playlistService.RemoveTrack(c.Request.Context(), playlistID, trackID, userID); err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
2025-12-03 19:29:37 +00:00
if err.Error() == "playlist not found" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
2025-12-03 19:29:37 +00:00
return
}
if err.Error() == "track not in playlist" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewNotFoundError("track not in playlist"))
2025-12-03 19:29:37 +00:00
return
}
if err.Error() == "forbidden" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
2025-12-03 19:29:37 +00:00
return
}
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to remove track from playlist", err))
2025-12-03 19:29:37 +00:00
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "track removed from playlist"})
2025-12-03 19:29:37 +00:00
}
// ReorderTracks gère la réorganisation des tracks d'une playlist
// @Summary Reorder Tracks
// @Description Reorder tracks in the playlist
// @Tags Playlist
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path string true "Playlist ID"
// @Param order body ReorderTracksRequest true "New Track Order"
// @Success 200 {object} APIResponse{data=object{message=string}}
// @Failure 400 {object} APIResponse "Validation Error"
// @Router /playlists/{id}/tracks/reorder [put]
2025-12-03 19:29:37 +00:00
func (h *PlaylistHandler) ReorderTracks(c *gin.Context) {
2025-12-16 16:23:49 +00:00
// MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel
userID, ok := GetUserIDUUID(c)
if !ok {
return // Erreur déjà envoyée par GetUserIDUUID
2025-12-03 19:29:37 +00:00
}
// Playlist IDs are uuid.UUID
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
2025-12-03 19:29:37 +00:00
return
}
var req ReorderTracksRequest
P0: stabilisation backend/chat/stream + nouvelle base migrations v1 Backend Go: - Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN. - Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError). - Sécurisation de config.go, CORS, statuts de santé et monitoring. - Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles). - Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés. - Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*. Chat server (Rust): - Refonte du pipeline JWT + sécurité, audit et rate limiting avancé. - Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing). - Nettoyage des panics, gestion d’erreurs robuste, logs structurés. - Migrations chat alignées sur le schéma UUID et nouvelles features. Stream server (Rust): - Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core. - Transactions P0 pour les jobs et segments, garanties d’atomicité. - Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION). Documentation & audits: - TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services. - Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3). - Scripts de reset et de cleanup pour la lab DB et la V1. Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
2025-12-03 19:29:37 +00:00
return
}
if err := h.playlistService.ReorderTracks(c.Request.Context(), playlistID, userID, req.TrackIDs); err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
2025-12-03 19:29:37 +00:00
if err.Error() == "playlist not found" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
2025-12-03 19:29:37 +00:00
return
}
if err.Error() == "some tracks are not in the playlist" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewValidationError("some tracks are not in the playlist"))
2025-12-03 19:29:37 +00:00
return
}
if err.Error() == "forbidden" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
2025-12-03 19:29:37 +00:00
return
}
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to reorder tracks", err))
2025-12-03 19:29:37 +00:00
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "tracks reordered"})
2025-12-03 19:29:37 +00:00
}
// AddCollaboratorRequest représente la requête pour ajouter un collaborateur
type AddCollaboratorRequest struct {
UserID uuid.UUID `json:"user_id" binding:"required" validate:"required"`
Permission string `json:"permission" binding:"required,oneof=read write admin" validate:"required,oneof=read write admin"`
2025-12-03 19:29:37 +00:00
}
// UpdateCollaboratorPermissionRequest représente la requête pour mettre à jour la permission d'un collaborateur
type UpdateCollaboratorPermissionRequest struct {
Permission string `json:"permission" binding:"required,oneof=read write admin" validate:"required,oneof=read write admin"`
2025-12-03 19:29:37 +00:00
}
// AddCollaborator gère l'ajout d'un collaborateur à une playlist
// T0479: POST /api/v1/playlists/:id/collaborators
func (h *PlaylistHandler) AddCollaborator(c *gin.Context) {
2025-12-16 16:23:49 +00:00
// MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel
userID, ok := GetUserIDUUID(c)
if !ok {
return // Erreur déjà envoyée par GetUserIDUUID
2025-12-03 19:29:37 +00:00
}
// Playlist IDs are uuid.UUID
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
2025-12-03 19:29:37 +00:00
return
}
var req AddCollaboratorRequest
P0: stabilisation backend/chat/stream + nouvelle base migrations v1 Backend Go: - Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN. - Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError). - Sécurisation de config.go, CORS, statuts de santé et monitoring. - Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles). - Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés. - Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*. Chat server (Rust): - Refonte du pipeline JWT + sécurité, audit et rate limiting avancé. - Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing). - Nettoyage des panics, gestion d’erreurs robuste, logs structurés. - Migrations chat alignées sur le schéma UUID et nouvelles features. Stream server (Rust): - Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core. - Transactions P0 pour les jobs et segments, garanties d’atomicité. - Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION). Documentation & audits: - TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services. - Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3). - Scripts de reset et de cleanup pour la lab DB et la V1. Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
2025-12-03 19:29:37 +00:00
return
}
// Convertir la permission string en PlaylistPermission
var permission models.PlaylistPermission
switch req.Permission {
case "read":
permission = models.PlaylistPermissionRead
case "write":
permission = models.PlaylistPermissionWrite
case "admin":
permission = models.PlaylistPermissionAdmin
default:
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewValidationError("invalid permission"))
2025-12-03 19:29:37 +00:00
return
}
collaborator, err := h.playlistService.AddCollaborator(c.Request.Context(), playlistID, userID, req.UserID, permission)
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
2025-12-03 19:29:37 +00:00
if err.Error() == "playlist not found" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
2025-12-03 19:29:37 +00:00
return
}
if err.Error() == "user not found" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewNotFoundError("user"))
2025-12-03 19:29:37 +00:00
return
}
if err.Error() == "user is already a collaborator" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeConflict, "user is already a collaborator"))
2025-12-03 19:29:37 +00:00
return
}
if err.Error() == "cannot add playlist owner as collaborator" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewValidationError("cannot add playlist owner as collaborator"))
2025-12-03 19:29:37 +00:00
return
}
if err.Error() == "forbidden: only playlist owner can add collaborators" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
2025-12-03 19:29:37 +00:00
return
}
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to add collaborator", err))
2025-12-03 19:29:37 +00:00
return
}
RespondSuccess(c, http.StatusCreated, collaborator)
2025-12-03 19:29:37 +00:00
}
// RemoveCollaborator gère la suppression d'un collaborateur d'une playlist
// T0479: DELETE /api/v1/playlists/:id/collaborators/:userId
func (h *PlaylistHandler) RemoveCollaborator(c *gin.Context) {
2025-12-16 16:23:49 +00:00
// MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel
userID, ok := GetUserIDUUID(c)
if !ok {
return // Erreur déjà envoyée par GetUserIDUUID
2025-12-03 19:29:37 +00:00
}
// Playlist IDs are uuid.UUID
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
2025-12-03 19:29:37 +00:00
return
}
// User IDs are UUID
collaboratorUserID, err := uuid.Parse(c.Param("userId"))
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewValidationError("invalid user id"))
2025-12-03 19:29:37 +00:00
return
}
if err := h.playlistService.RemoveCollaborator(c.Request.Context(), playlistID, userID, collaboratorUserID); err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
2025-12-03 19:29:37 +00:00
if err.Error() == "playlist not found" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
2025-12-03 19:29:37 +00:00
return
}
if err.Error() == "collaborator not found" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewNotFoundError("collaborator"))
2025-12-03 19:29:37 +00:00
return
}
if err.Error() == "forbidden: only playlist owner can remove collaborators" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
2025-12-03 19:29:37 +00:00
return
}
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to remove collaborator", err))
2025-12-03 19:29:37 +00:00
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "collaborator removed"})
2025-12-03 19:29:37 +00:00
}
// UpdateCollaboratorPermission gère la mise à jour de la permission d'un collaborateur
// T0479: PUT /api/v1/playlists/:id/collaborators/:userId
func (h *PlaylistHandler) UpdateCollaboratorPermission(c *gin.Context) {
2025-12-16 16:23:49 +00:00
// MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel
userID, ok := GetUserIDUUID(c)
if !ok {
return // Erreur déjà envoyée par GetUserIDUUID
2025-12-03 19:29:37 +00:00
}
// Playlist IDs are uuid.UUID
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
if err != nil {
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
2025-12-03 19:29:37 +00:00
return
}
// User IDs are UUID
collaboratorUserID, err := uuid.Parse(c.Param("userId"))
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewValidationError("invalid user id"))
2025-12-03 19:29:37 +00:00
return
}
var req UpdateCollaboratorPermissionRequest
P0: stabilisation backend/chat/stream + nouvelle base migrations v1 Backend Go: - Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN. - Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError). - Sécurisation de config.go, CORS, statuts de santé et monitoring. - Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles). - Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés. - Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*. Chat server (Rust): - Refonte du pipeline JWT + sécurité, audit et rate limiting avancé. - Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing). - Nettoyage des panics, gestion d’erreurs robuste, logs structurés. - Migrations chat alignées sur le schéma UUID et nouvelles features. Stream server (Rust): - Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core. - Transactions P0 pour les jobs et segments, garanties d’atomicité. - Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION). Documentation & audits: - TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services. - Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3). - Scripts de reset et de cleanup pour la lab DB et la V1. Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
2025-12-03 19:29:37 +00:00
return
}
// Convertir la permission string en PlaylistPermission
var permission models.PlaylistPermission
switch req.Permission {
case "read":
permission = models.PlaylistPermissionRead
case "write":
permission = models.PlaylistPermissionWrite
case "admin":
permission = models.PlaylistPermissionAdmin
default:
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewValidationError("invalid permission"))
2025-12-03 19:29:37 +00:00
return
}
if err := h.playlistService.UpdateCollaboratorPermission(c.Request.Context(), playlistID, userID, collaboratorUserID, permission); err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
2025-12-03 19:29:37 +00:00
if err.Error() == "playlist not found" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
2025-12-03 19:29:37 +00:00
return
}
if err.Error() == "collaborator not found" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewNotFoundError("collaborator"))
2025-12-03 19:29:37 +00:00
return
}
if err.Error() == "invalid permission" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewValidationError("invalid permission"))
2025-12-03 19:29:37 +00:00
return
}
if err.Error() == "forbidden: only playlist owner can update collaborator permissions" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
2025-12-03 19:29:37 +00:00
return
}
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to update collaborator permission", err))
2025-12-03 19:29:37 +00:00
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "collaborator permission updated"})
2025-12-03 19:29:37 +00:00
}
// GetCollaborators gère la récupération des collaborateurs d'une playlist
// T0479: GET /api/v1/playlists/:id/collaborators
func (h *PlaylistHandler) GetCollaborators(c *gin.Context) {
2025-12-16 16:23:49 +00:00
// MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel
userID, ok := GetUserIDUUID(c)
if !ok {
return // Erreur déjà envoyée par GetUserIDUUID
2025-12-03 19:29:37 +00:00
}
// Playlist IDs are uuid.UUID
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
2025-12-03 19:29:37 +00:00
return
}
collaborators, err := h.playlistService.GetCollaborators(c.Request.Context(), playlistID, userID)
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
2025-12-03 19:29:37 +00:00
if err.Error() == "playlist not found" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
2025-12-03 19:29:37 +00:00
return
}
if err.Error() == "forbidden: access denied" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
2025-12-03 19:29:37 +00:00
return
}
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get collaborators", err))
2025-12-03 19:29:37 +00:00
return
}
RespondSuccess(c, http.StatusOK, gin.H{"collaborators": collaborators})
2025-12-03 19:29:37 +00:00
}
// CreateShareLink gère la création d'un lien de partage public pour une playlist
// T0488: Create Playlist Public Share Link
func (h *PlaylistHandler) CreateShareLink(c *gin.Context) {
2025-12-16 16:23:49 +00:00
// MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel
userID, ok := GetUserIDUUID(c)
if !ok {
return // Erreur déjà envoyée par GetUserIDUUID
2025-12-03 19:29:37 +00:00
}
// Playlist IDs are uuid.UUID
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
2025-12-03 19:29:37 +00:00
return
}
// Créer le lien de partage via le service
// La vérification des permissions (owner ou admin) est faite dans PlaylistService.CreateShareLink
shareLink, err := h.playlistService.CreateShareLink(c.Request.Context(), playlistID, userID, nil)
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
2025-12-03 19:29:37 +00:00
if err.Error() == "playlist not found" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
2025-12-03 19:29:37 +00:00
return
}
if err.Error() == "forbidden: only owner or admin can create share links" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
2025-12-03 19:29:37 +00:00
return
}
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to create share link", err))
2025-12-03 19:29:37 +00:00
return
}
RespondSuccess(c, http.StatusOK, shareLink)
2025-12-03 19:29:37 +00:00
}
// FollowPlaylist gère le follow d'une playlist
// T0489: Create Playlist Follow Feature
func (h *PlaylistHandler) FollowPlaylist(c *gin.Context) {
2025-12-16 16:23:49 +00:00
// MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel
userID, ok := GetUserIDUUID(c)
if !ok {
return // Erreur déjà envoyée par GetUserIDUUID
2025-12-03 19:29:37 +00:00
}
// Playlist IDs are uuid.UUID
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
2025-12-03 19:29:37 +00:00
return
}
err = h.playlistService.FollowPlaylist(c.Request.Context(), playlistID, userID)
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
2025-12-03 19:29:37 +00:00
if err.Error() == "playlist not found" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
2025-12-03 19:29:37 +00:00
return
}
if err.Error() == "cannot follow own playlist" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewValidationError("cannot follow own playlist"))
2025-12-03 19:29:37 +00:00
return
}
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to follow playlist", err))
2025-12-03 19:29:37 +00:00
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "playlist followed"})
2025-12-03 19:29:37 +00:00
}
// UnfollowPlaylist gère l'unfollow d'une playlist
// T0489: Create Playlist Follow Feature
func (h *PlaylistHandler) UnfollowPlaylist(c *gin.Context) {
2025-12-16 16:23:49 +00:00
// MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel
userID, ok := GetUserIDUUID(c)
if !ok {
return // Erreur déjà envoyée par GetUserIDUUID
2025-12-03 19:29:37 +00:00
}
// Playlist IDs are uuid.UUID
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
2025-12-03 19:29:37 +00:00
return
}
err = h.playlistService.UnfollowPlaylist(c.Request.Context(), playlistID, userID)
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
2025-12-03 19:29:37 +00:00
if err.Error() == "playlist not found" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
2025-12-03 19:29:37 +00:00
return
}
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to unfollow playlist", err))
2025-12-03 19:29:37 +00:00
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "playlist unfollowed"})
2025-12-03 19:29:37 +00:00
}
// GetPlaylistStats gère la récupération des statistiques d'une playlist
// T0491: Create Playlist Analytics Backend
func (h *PlaylistHandler) GetPlaylistStats(c *gin.Context) {
// Playlist IDs are uuid.UUID
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
2025-12-03 19:29:37 +00:00
return
}
// Vérifier que la playlist existe et que l'utilisateur a accès
var userID *uuid.UUID
if uidInterface, exists := c.Get("user_id"); exists {
if uid, ok := uidInterface.(uuid.UUID); ok {
userID = &uid
}
}
playlist, err := h.playlistService.GetPlaylist(c.Request.Context(), playlistID, userID)
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
2025-12-03 19:29:37 +00:00
if err.Error() == "playlist not found" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
2025-12-03 19:29:37 +00:00
return
}
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get playlist", err))
2025-12-03 19:29:37 +00:00
return
}
// Vérifier que l'utilisateur a accès (propriétaire, collaborateur ou playlist publique)
// Use uuid.Nil for comparison if userID is nil
currentUserID := uuid.Nil
if userID != nil {
currentUserID = *userID
}
if playlist.UserID != currentUserID && !playlist.IsPublic {
// Vérifier si l'utilisateur est collaborateur
if userID != nil {
hasAccess, err := h.playlistService.CheckPermission(c.Request.Context(), playlistID, *userID, models.PlaylistPermissionRead)
if err != nil || !hasAccess {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
2025-12-03 19:29:37 +00:00
return
}
} else {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
2025-12-03 19:29:37 +00:00
return
}
}
// Récupérer les statistiques via le service d'analytics
if h.playlistAnalyticsService == nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "analytics service not available"))
2025-12-03 19:29:37 +00:00
return
}
stats, err := h.playlistAnalyticsService.GetPlaylistStats(c.Request.Context(), playlistID)
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
2025-12-03 19:29:37 +00:00
if err.Error() == "playlist not found" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
2025-12-03 19:29:37 +00:00
return
}
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get playlist stats", err))
2025-12-03 19:29:37 +00:00
return
}
RespondSuccess(c, http.StatusOK, gin.H{"stats": stats})
2025-12-03 19:29:37 +00:00
}
// DuplicatePlaylistRequest représente la requête pour dupliquer une playlist
type DuplicatePlaylistRequest struct {
NewTitle string `json:"new_title"`
NewDescription string `json:"new_description,omitempty"`
IsPublic *bool `json:"is_public,omitempty"`
}
// DuplicatePlaylist gère la duplication d'une playlist
// T0495: Create Playlist Duplicate Feature
func (h *PlaylistHandler) DuplicatePlaylist(c *gin.Context) {
// Playlist IDs are uuid.UUID
playlistID, err := uuid.Parse(c.Param("id")) // Changed to uuid.Parse
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewValidationError("invalid playlist id"))
2025-12-03 19:29:37 +00:00
return
}
2025-12-16 16:23:49 +00:00
// MOD-P1-001: Utiliser GetUserIDUUID au lieu de c.Get manuel
userID, ok := GetUserIDUUID(c)
if !ok {
return // Erreur déjà envoyée par GetUserIDUUID
2025-12-03 19:29:37 +00:00
}
var req DuplicatePlaylistRequest
P0: stabilisation backend/chat/stream + nouvelle base migrations v1 Backend Go: - Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN. - Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError). - Sécurisation de config.go, CORS, statuts de santé et monitoring. - Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles). - Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés. - Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*. Chat server (Rust): - Refonte du pipeline JWT + sécurité, audit et rate limiting avancé. - Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing). - Nettoyage des panics, gestion d’erreurs robuste, logs structurés. - Migrations chat alignées sur le schéma UUID et nouvelles features. Stream server (Rust): - Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core. - Transactions P0 pour les jobs et segments, garanties d’atomicité. - Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION). Documentation & audits: - TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services. - Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3). - Scripts de reset et de cleanup pour la lab DB et la V1. Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
RespondWithAppError(c, appErr)
2025-12-03 19:29:37 +00:00
return
}
// Créer le service de duplication
P0: stabilisation backend/chat/stream + nouvelle base migrations v1 Backend Go: - Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN. - Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError). - Sécurisation de config.go, CORS, statuts de santé et monitoring. - Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles). - Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés. - Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*. Chat server (Rust): - Refonte du pipeline JWT + sécurité, audit et rate limiting avancé. - Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing). - Nettoyage des panics, gestion d’erreurs robuste, logs structurés. - Migrations chat alignées sur le schéma UUID et nouvelles features. Stream server (Rust): - Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core. - Transactions P0 pour les jobs et segments, garanties d’atomicité. - Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION). Documentation & audits: - TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services. - Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3). - Scripts de reset et de cleanup pour la lab DB et la V1. Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
duplicateService := services.NewPlaylistDuplicateService(h.playlistService, h.db, nil)
2025-12-03 19:29:37 +00:00
// Dupliquer la playlist
newPlaylist, err := duplicateService.DuplicatePlaylist(
c.Request.Context(),
playlistID,
userID,
services.DuplicatePlaylistRequest{
NewTitle: req.NewTitle,
NewDescription: req.NewDescription,
IsPublic: req.IsPublic,
},
)
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
2025-12-03 19:29:37 +00:00
if err.Error() == "playlist not found" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewNotFoundError("playlist"))
2025-12-03 19:29:37 +00:00
return
}
if err.Error() == "forbidden: you don't have access to this playlist" {
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
2025-12-03 19:29:37 +00:00
return
}
2025-12-16 16:23:49 +00:00
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to duplicate playlist", err))
2025-12-03 19:29:37 +00:00
return
}
RespondSuccess(c, http.StatusOK, gin.H{
2025-12-03 19:29:37 +00:00
"message": "playlist duplicated successfully",
"playlist": newPlaylist,
})
}
// SearchPlaylists gère la recherche de playlists
// T0496: Create Playlist Search Backend
func (h *PlaylistHandler) SearchPlaylists(c *gin.Context) {
// Get current user ID
var currentUserID *uuid.UUID
if uidInterface, exists := c.Get("user_id"); exists {
if uid, ok := uidInterface.(uuid.UUID); ok {
currentUserID = &uid
}
}
// Récupérer les paramètres de recherche
query := c.Query("q")
userIDParam := c.Query("user_id")
isPublicParam := c.Query("is_public")
pageParam := c.DefaultQuery("page", "1")
limitParam := c.DefaultQuery("limit", "20")
// Parser les paramètres
var filterUserID *uuid.UUID
if userIDParam != "" {
if parsed, err := uuid.Parse(userIDParam); err == nil {
filterUserID = &parsed
}
}
var filterIsPublic *bool
if isPublicParam != "" {
if parsed, err := strconv.ParseBool(isPublicParam); err == nil {
filterIsPublic = &parsed
}
}
page, err := strconv.Atoi(pageParam)
if err != nil || page < 1 {
page = 1
}
limit, err := strconv.Atoi(limitParam)
if err != nil || limit < 1 {
limit = 20
}
// Rechercher les playlists
playlists, total, err := h.playlistService.SearchPlaylists(c.Request.Context(), services.SearchPlaylistsParams{
Query: query,
UserID: filterUserID,
IsPublic: filterIsPublic,
Page: page,
Limit: limit,
CurrentUserID: currentUserID,
})
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to search playlists", err))
2025-12-03 19:29:37 +00:00
return
}
RespondSuccess(c, http.StatusOK, gin.H{
2025-12-03 19:29:37 +00:00
"playlists": playlists,
"total": total,
"page": page,
"limit": limit,
})
}
// GetRecommendations gère la récupération des recommandations de playlists
// T0498: Create Playlist Recommendations
func (h *PlaylistHandler) GetRecommendations(c *gin.Context) {
2025-12-16 16:23:49 +00:00
userID, ok := GetUserIDUUID(c)
if !ok {
return // Erreur déjà envoyée par GetUserIDUUID
}
2025-12-03 19:29:37 +00:00
if userID == uuid.Nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.NewUnauthorizedError("unauthorized"))
2025-12-03 19:29:37 +00:00
return
}
// Parser les paramètres de requête
limitParam := c.DefaultQuery("limit", "20")
limit, err := strconv.Atoi(limitParam)
if err != nil || limit < 1 {
limit = 20
}
if limit > 100 {
limit = 100
}
minScoreParam := c.DefaultQuery("min_score", "0.1")
minScore, err := strconv.ParseFloat(minScoreParam, 64)
if err != nil || minScore < 0 {
minScore = 0.1
}
includeOwnParam := c.DefaultQuery("include_own", "false")
includeOwn := includeOwnParam == "true"
// Vérifier que le service de follow est disponible
if h.playlistFollowService == nil {
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "follow service not available"))
return
}
2025-12-03 19:29:37 +00:00
// Créer le service de recommandations
recommendationService := services.NewPlaylistRecommendationService(
h.db,
2025-12-03 19:29:37 +00:00
h.playlistService,
h.playlistFollowService,
h.commonHandler.logger,
2025-12-03 19:29:37 +00:00
)
// Obtenir les recommandations
recommendations, err := recommendationService.GetRecommendations(
c.Request.Context(),
services.GetRecommendationsParams{
UserID: userID,
Limit: limit,
MinScore: minScore,
IncludeOwn: includeOwn,
},
)
if err != nil {
2025-12-16 16:23:49 +00:00
// MOD-P1-002: Utiliser RespondWithAppError au lieu de gin.H{"error"}
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to get recommendations", err))
2025-12-03 19:29:37 +00:00
return
}
// Formater la réponse
response := make([]gin.H, 0, len(recommendations))
for _, rec := range recommendations {
response = append(response, gin.H{
"playlist": rec.Playlist,
"score": rec.Score,
"reason": rec.Reason,
})
}
RespondSuccess(c, http.StatusOK, gin.H{
2025-12-03 19:29:37 +00:00
"recommendations": response,
"count": len(response),
})
}