veza/veza-backend-api/internal/services/playlist_duplicate_service.go

145 lines
5 KiB
Go

package services
import (
"context"
"errors"
"fmt"
"github.com/google/uuid"
"go.uber.org/zap"
"gorm.io/gorm"
"veza-backend-api/internal/models"
)
// PlaylistDuplicateService gère la duplication de playlists
// T0495: Create Playlist Duplicate Feature
type PlaylistDuplicateService struct {
playlistService *PlaylistService
db *gorm.DB
logger *zap.Logger
}
// NewPlaylistDuplicateService crée un nouveau service de duplication de playlists
func NewPlaylistDuplicateService(playlistService *PlaylistService, db *gorm.DB, logger *zap.Logger) *PlaylistDuplicateService {
if logger == nil {
logger = zap.NewNop()
}
return &PlaylistDuplicateService{
playlistService: playlistService,
db: db,
logger: logger,
}
}
// DuplicatePlaylistRequest représente la requête de duplication
type DuplicatePlaylistRequest struct {
NewTitle string `json:"new_title"`
NewDescription string `json:"new_description,omitempty"`
IsPublic *bool `json:"is_public,omitempty"`
}
// DuplicatePlaylist duplique une playlist avec tous ses tracks
// T0495: Create Playlist Duplicate Feature
// MIGRATION UUID: Completée. playlistID et userID sont des UUIDs.
// Transactionnelle : Toute la duplication (playlist + tracks + compteur) est dans une seule transaction
func (s *PlaylistDuplicateService) DuplicatePlaylist(
ctx context.Context,
playlistID uuid.UUID,
userID uuid.UUID,
request DuplicatePlaylistRequest,
) (*models.Playlist, error) {
var newPlaylist *models.Playlist
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// 1. VALIDATION : Charger playlist originale + tracks (SELECT avec Preload dans la transaction)
var originalPlaylist models.Playlist
err := tx.Preload("Tracks.Track").First(&originalPlaylist, "id = ?", playlistID).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.New("playlist not found")
}
return fmt.Errorf("DuplicatePlaylist: failed to load original playlist: %w", err)
}
// 2. VALIDATION : Vérifier que l'utilisateur a accès à la playlist (propriétaire, collaborateur ou publique)
// Note: On fait cette vérification dans la transaction pour éviter les race conditions
if originalPlaylist.UserID != userID && !originalPlaylist.IsPublic {
// Vérifier si l'utilisateur est collaborateur (simplifié pour la transaction)
// On peut faire une requête simple dans la transaction
var collaboratorCount int64
err := tx.Raw("SELECT COUNT(*) FROM playlist_collaborators WHERE playlist_id = ? AND user_id = ?", playlistID, userID).Scan(&collaboratorCount).Error
if err != nil || collaboratorCount == 0 {
return errors.New("forbidden: you don't have access to this playlist")
}
}
// 3. DÉTERMINATION : Titre, description, isPublic
newTitle := request.NewTitle
if newTitle == "" {
newTitle = originalPlaylist.Title + " (Copy)"
}
newDescription := request.NewDescription
if newDescription == "" {
newDescription = originalPlaylist.Description
}
isPublic := originalPlaylist.IsPublic
if request.IsPublic != nil {
isPublic = *request.IsPublic
}
// 4. CRÉATION : Nouvelle playlist (INSERT dans la transaction)
newPlaylist = &models.Playlist{
UserID: userID,
Title: newTitle,
Description: newDescription,
IsPublic: isPublic,
TrackCount: 0, // Sera mis à jour après l'ajout des tracks
}
if err := tx.Create(newPlaylist).Error; err != nil {
return fmt.Errorf("DuplicatePlaylist: failed to create duplicate playlist: %w", err)
}
// 5. DUPLICATION : Tous les tracks dans la même transaction
if originalPlaylist.Tracks != nil && len(originalPlaylist.Tracks) > 0 {
for i, playlistTrack := range originalPlaylist.Tracks {
// Créer le PlaylistTrack directement dans la transaction
newPlaylistTrack := models.PlaylistTrack{
PlaylistID: newPlaylist.ID,
TrackID: playlistTrack.Track.ID,
Position: playlistTrack.Position,
}
// Si position <= 0, utiliser l'index + 1
if newPlaylistTrack.Position <= 0 {
newPlaylistTrack.Position = i + 1
}
if err := tx.Create(&newPlaylistTrack).Error; err != nil {
return fmt.Errorf("DuplicatePlaylist: failed to add track %s to duplicate: %w", playlistTrack.Track.ID, err)
}
}
}
// 6. MISE À JOUR : Compteur de tracks (UPDATE dans la transaction)
trackCount := len(originalPlaylist.Tracks)
if err := tx.Model(newPlaylist).Update("track_count", trackCount).Error; err != nil {
return fmt.Errorf("DuplicatePlaylist: failed to update track_count: %w", err)
}
newPlaylist.TrackCount = trackCount
// 7. LOG (dans la transaction, mais ne dépend pas d'états non commit)
s.logger.Info("Playlist duplicated",
zap.String("original_playlist_id", playlistID.String()),
zap.String("new_playlist_id", newPlaylist.ID.String()),
zap.String("user_id", userID.String()),
zap.Int("tracks_count", trackCount),
)
// 8. RETOUR nil = commit automatique
return nil
})
if err != nil {
return nil, err // Rollback automatique si erreur
}
return newPlaylist, nil
}