882 lines
31 KiB
Go
882 lines
31 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/google/uuid"
|
|
"time"
|
|
|
|
"veza-backend-api/internal/models"
|
|
"veza-backend-api/internal/repositories"
|
|
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// UserRepositoryForPlaylist définit l'interface minimale nécessaire pour PlaylistService
|
|
// T0453: Interface pour vérifier l'existence des utilisateurs
|
|
type UserRepositoryForPlaylist interface {
|
|
GetByID(id string) (*models.User, error)
|
|
GetByEmail(email string) (*models.User, error)
|
|
GetByUsername(username string) (*models.User, error)
|
|
Create(user *models.User) error
|
|
Update(user *models.User) error
|
|
Delete(id string) error
|
|
}
|
|
|
|
// gormUserRepositoryWithExists étend gormUserRepository avec Exists
|
|
type gormUserRepositoryWithExists interface {
|
|
UserRepositoryForPlaylist
|
|
Exists(ctx context.Context, userID uuid.UUID) (bool, error)
|
|
}
|
|
|
|
// PlaylistService gère les opérations sur les playlists
|
|
// T0453: Utilise le repository pattern pour l'accès aux données
|
|
type PlaylistService struct {
|
|
playlistRepo repositories.PlaylistRepository
|
|
playlistTrackRepo repositories.PlaylistTrackRepository
|
|
playlistCollaboratorRepo repositories.PlaylistCollaboratorRepository
|
|
playlistShareService *PlaylistShareService
|
|
playlistFollowService *PlaylistFollowService
|
|
playlistNotificationService *PlaylistNotificationService
|
|
playlistVersionService *PlaylistVersionService
|
|
userRepo UserRepositoryForPlaylist
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewPlaylistService crée un nouveau service de playlists avec repositories
|
|
func NewPlaylistService(playlistRepo repositories.PlaylistRepository, playlistTrackRepo repositories.PlaylistTrackRepository, playlistCollaboratorRepo repositories.PlaylistCollaboratorRepository, userRepo UserRepositoryForPlaylist, logger *zap.Logger) *PlaylistService {
|
|
if logger == nil {
|
|
logger = zap.NewNop()
|
|
}
|
|
return &PlaylistService{
|
|
playlistRepo: playlistRepo,
|
|
playlistTrackRepo: playlistTrackRepo,
|
|
playlistCollaboratorRepo: playlistCollaboratorRepo,
|
|
userRepo: userRepo,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// SetPlaylistShareService définit le service de partage de playlist
|
|
// T0488: Create Playlist Public Share Link
|
|
func (s *PlaylistService) SetPlaylistShareService(shareService *PlaylistShareService) {
|
|
s.playlistShareService = shareService
|
|
}
|
|
|
|
// SetPlaylistFollowService définit le service de follow de playlist
|
|
// T0489: Create Playlist Follow Feature
|
|
func (s *PlaylistService) SetPlaylistFollowService(followService *PlaylistFollowService) {
|
|
s.playlistFollowService = followService
|
|
}
|
|
|
|
// SetPlaylistNotificationService définit le service de notifications de playlist
|
|
// T0508: Create Playlist Notifications
|
|
func (s *PlaylistService) SetPlaylistNotificationService(notificationService *PlaylistNotificationService) {
|
|
s.playlistNotificationService = notificationService
|
|
}
|
|
|
|
// SetPlaylistVersionService définit le service de versions de playlist
|
|
// T0509: Create Playlist Version History
|
|
func (s *PlaylistService) SetPlaylistVersionService(versionService *PlaylistVersionService) {
|
|
s.playlistVersionService = versionService
|
|
}
|
|
|
|
// NewPlaylistServiceWithDB crée un nouveau service de playlists avec GORM (compatibilité)
|
|
// Cette fonction crée les repositories en interne pour maintenir la compatibilité
|
|
func NewPlaylistServiceWithDB(db *gorm.DB, logger *zap.Logger) *PlaylistService {
|
|
if logger == nil {
|
|
logger = zap.NewNop()
|
|
}
|
|
playlistRepo := repositories.NewPlaylistRepository(db)
|
|
playlistTrackRepo := repositories.NewPlaylistTrackRepository(db)
|
|
playlistCollaboratorRepo := repositories.NewPlaylistCollaboratorRepository(db)
|
|
// Pour UserRepository, on utilise une implémentation simple qui utilise GORM
|
|
// Note: On pourrait créer un UserRepository GORM aussi, mais pour l'instant on garde la compatibilité
|
|
userRepo := &gormUserRepository{db: db}
|
|
service := &PlaylistService{
|
|
playlistRepo: playlistRepo,
|
|
playlistTrackRepo: playlistTrackRepo,
|
|
playlistCollaboratorRepo: playlistCollaboratorRepo,
|
|
userRepo: userRepo,
|
|
logger: logger,
|
|
}
|
|
// Créer et injecter le service de partage
|
|
shareService := NewPlaylistShareService(db)
|
|
service.SetPlaylistShareService(shareService)
|
|
return service
|
|
}
|
|
|
|
// gormUserRepository est une implémentation temporaire de UserRepository avec GORM
|
|
// pour maintenir la compatibilité avec le code existant
|
|
type gormUserRepository struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
func (r *gormUserRepository) GetByID(id string) (*models.User, error) {
|
|
var user models.User
|
|
if err := r.db.First(&user, "id = ?", id).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &user, nil
|
|
}
|
|
|
|
func (r *gormUserRepository) GetByEmail(email string) (*models.User, error) {
|
|
var user models.User
|
|
if err := r.db.Where("email = ?", email).First(&user).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &user, nil
|
|
}
|
|
|
|
func (r *gormUserRepository) GetByUsername(username string) (*models.User, error) {
|
|
var user models.User
|
|
if err := r.db.Where("username = ?", username).First(&user).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
return &user, nil
|
|
}
|
|
|
|
func (r *gormUserRepository) Create(user *models.User) error {
|
|
return r.db.Create(user).Error
|
|
}
|
|
|
|
func (r *gormUserRepository) Update(user *models.User) error {
|
|
return r.db.Save(user).Error
|
|
}
|
|
|
|
func (r *gormUserRepository) Delete(id string) error {
|
|
return r.db.Delete(&models.User{}, "id = ?", id).Error
|
|
}
|
|
|
|
// Exists vérifie si un utilisateur existe (méthode helper pour le service)
|
|
func (r *gormUserRepository) Exists(ctx context.Context, userID uuid.UUID) (bool, error) {
|
|
var count int64
|
|
err := r.db.WithContext(ctx).Model(&models.User{}).Where("id = ?", userID).Count(&count).Error
|
|
return count > 0, err
|
|
}
|
|
|
|
// CreatePlaylist crée une nouvelle playlist
|
|
// T0453: Utilise le repository pattern avec validation
|
|
func (s *PlaylistService) CreatePlaylist(ctx context.Context, userID uuid.UUID, title, description string, isPublic bool) (*models.Playlist, error) {
|
|
// Validation
|
|
if title == "" {
|
|
return nil, errors.New("title is required")
|
|
}
|
|
if len(title) > 200 {
|
|
return nil, errors.New("title must be less than 200 characters")
|
|
}
|
|
|
|
// Vérifier que l'utilisateur existe
|
|
// Note: On utilise une méthode helper Exists si disponible
|
|
if gormRepo, ok := s.userRepo.(interface {
|
|
Exists(ctx context.Context, userID uuid.UUID) (bool, error)
|
|
}); ok {
|
|
exists, err := gormRepo.Exists(ctx, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to check user: %w", err)
|
|
}
|
|
if !exists {
|
|
return nil, errors.New("user not found")
|
|
}
|
|
} else {
|
|
// Pour les autres implémentations, on essaie de récupérer l'utilisateur
|
|
_, err := s.userRepo.GetByID(userID.String())
|
|
if err != nil {
|
|
return nil, errors.New("user not found")
|
|
}
|
|
}
|
|
|
|
// Créer la playlist
|
|
playlist := &models.Playlist{
|
|
UserID: userID,
|
|
Title: title,
|
|
Description: description,
|
|
IsPublic: isPublic,
|
|
TrackCount: 0,
|
|
}
|
|
|
|
if err := s.playlistRepo.Create(ctx, playlist); err != nil {
|
|
return nil, fmt.Errorf("failed to create playlist: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Playlist created",
|
|
zap.String("playlist_id", playlist.ID.String()),
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("title", title),
|
|
)
|
|
|
|
// T0509: Sauvegarder la version initiale
|
|
if s.playlistVersionService != nil {
|
|
// FIXME: PlaylistVersionService likely needs update for UUID too, but assuming it takes what we give or we handle it later
|
|
// Assuming PlaylistVersionService needs int64, we might have issues.
|
|
// For now, let's pass UUID if it accepts interface{} or we update it later.
|
|
// Actually, let's assume we need to update it or skip versioning for now if it breaks.
|
|
// Let's try to pass it.
|
|
if _, err := s.playlistVersionService.SaveVersion(ctx, playlist.ID, userID, models.PlaylistVersionActionCreated); err != nil {
|
|
s.logger.Warn("Failed to save initial playlist version", zap.Error(err))
|
|
}
|
|
}
|
|
|
|
return playlist, nil
|
|
}
|
|
|
|
// GetPlaylist récupère une playlist avec ses tracks
|
|
// T0453: Utilise le repository pattern avec vérification d'accès
|
|
// MIGRATION UUID: userID migré vers *uuid.UUID
|
|
func (s *PlaylistService) GetPlaylist(ctx context.Context, playlistID uuid.UUID, userID *uuid.UUID) (*models.Playlist, error) {
|
|
playlist, err := s.playlistRepo.GetByIDWithTracks(ctx, playlistID) // Use GetByIDWithTracks
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, ErrPlaylistNotFound
|
|
}
|
|
return nil, fmt.Errorf("failed to get playlist: %w", err)
|
|
}
|
|
|
|
// Vérifier accès si playlist privée
|
|
if !playlist.IsPublic {
|
|
if userID == nil || *userID != playlist.UserID {
|
|
return nil, ErrPlaylistNotFound // Return NotFound for security (hide private playlists)
|
|
}
|
|
}
|
|
|
|
return playlist, nil
|
|
}
|
|
|
|
// GetPlaylists récupère une liste de playlists avec pagination
|
|
// T0453: Utilise le repository pattern avec filtres
|
|
// T0501: Optimisé avec pagination efficace et lazy loading
|
|
// MIGRATION UUID: currentUserID et filterUserID migrés vers *uuid.UUID
|
|
func (s *PlaylistService) GetPlaylists(ctx context.Context, currentUserID *uuid.UUID, filterUserID *uuid.UUID, page, limit int) ([]*models.Playlist, int64, error) {
|
|
// Appliquer la pagination avec limites optimisées
|
|
if limit <= 0 {
|
|
limit = 20
|
|
}
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
offset := (page - 1) * limit
|
|
|
|
// T0501: Optimisation - Utiliser un offset calculé efficacement
|
|
// Pour les grandes pages, utiliser un curseur si disponible
|
|
if page > 100 {
|
|
// Pour les très grandes pages, limiter à 100 pour éviter les problèmes de performance
|
|
page = 100
|
|
offset = (page - 1) * limit
|
|
}
|
|
|
|
// Déterminer le filtre isPublic selon les règles d'accès
|
|
var isPublic *bool
|
|
if currentUserID == nil {
|
|
// Utilisateur non authentifié : seulement les playlists publiques
|
|
public := true
|
|
isPublic = &public
|
|
} else if filterUserID != nil && *filterUserID != *currentUserID {
|
|
// Filtre sur un autre utilisateur : seulement publiques
|
|
public := true
|
|
isPublic = &public
|
|
}
|
|
// Si filterUserID == currentUserID ou filterUserID == nil, on ne filtre pas par isPublic
|
|
// (on laisse le repository gérer)
|
|
|
|
playlists, total, err := s.playlistRepo.List(ctx, filterUserID, isPublic, limit, offset)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to get playlists: %w", err)
|
|
}
|
|
|
|
// T0501: Lazy loading - Ne pas charger les tracks pour la liste
|
|
for _, p := range playlists {
|
|
p.Tracks = nil
|
|
}
|
|
|
|
// Filtrer les playlists selon les règles d'accès si nécessaire
|
|
if currentUserID != nil && filterUserID == nil {
|
|
// Filtrer pour ne garder que les publiques ou celles de l'utilisateur
|
|
filtered := make([]*models.Playlist, 0)
|
|
for _, p := range playlists {
|
|
if p.IsPublic || p.UserID == *currentUserID {
|
|
filtered = append(filtered, p)
|
|
}
|
|
}
|
|
playlists = filtered
|
|
}
|
|
|
|
return playlists, total, nil
|
|
}
|
|
|
|
// SearchPlaylistsParams représente les paramètres de recherche de playlists
|
|
// T0496: Create Playlist Search Backend
|
|
// MIGRATION UUID: UserID et CurrentUserID migrés vers *uuid.UUID
|
|
type SearchPlaylistsParams struct {
|
|
Query string // Recherche par titre ou description
|
|
UserID *uuid.UUID // Filtrer par utilisateur
|
|
IsPublic *bool // Filtrer par statut public/privé
|
|
Page int // Numéro de page (défaut: 1)
|
|
Limit int // Nombre de résultats par page (défaut: 20, max: 100)
|
|
CurrentUserID *uuid.UUID // ID de l'utilisateur actuel pour les règles d'accès
|
|
}
|
|
|
|
// SearchPlaylists recherche des playlists selon les critères fournis
|
|
// T0496: Create Playlist Search Backend
|
|
func (s *PlaylistService) SearchPlaylists(ctx context.Context, params SearchPlaylistsParams) ([]*models.Playlist, int64, error) {
|
|
// Appliquer la pagination
|
|
if params.Limit <= 0 {
|
|
params.Limit = 20
|
|
}
|
|
if params.Limit > 100 {
|
|
params.Limit = 100
|
|
}
|
|
if params.Page < 1 {
|
|
params.Page = 1
|
|
}
|
|
offset := (params.Page - 1) * params.Limit
|
|
|
|
// Déterminer le filtre isPublic selon les règles d'accès
|
|
var isPublic *bool
|
|
if params.IsPublic != nil {
|
|
isPublic = params.IsPublic
|
|
} else if params.CurrentUserID == nil {
|
|
// Si pas d'utilisateur authentifié, seulement les playlists publiques
|
|
public := true
|
|
isPublic = &public
|
|
} else if params.UserID != nil && *params.UserID != *params.CurrentUserID {
|
|
// Si on recherche les playlists d'un autre utilisateur, seulement publiques
|
|
public := true
|
|
isPublic = &public
|
|
}
|
|
// Si params.UserID == nil ou params.UserID == params.CurrentUserID, on ne filtre pas par isPublic
|
|
// (on laisse le repository gérer)
|
|
|
|
// Utiliser la méthode Search du repository
|
|
playlists, total, err := s.playlistRepo.Search(ctx, params.Query, params.UserID, isPublic, params.Limit, offset)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to search playlists: %w", err)
|
|
}
|
|
|
|
// Filtrer les playlists selon les règles d'accès si nécessaire
|
|
if params.CurrentUserID != nil && params.UserID == nil && isPublic == nil {
|
|
// Recherche globale : filtrer pour ne garder que les publiques ou celles de l'utilisateur
|
|
filtered := make([]*models.Playlist, 0)
|
|
for _, p := range playlists {
|
|
if p.IsPublic || p.UserID == *params.CurrentUserID {
|
|
filtered = append(filtered, p)
|
|
}
|
|
}
|
|
playlists = filtered
|
|
}
|
|
|
|
s.logger.Debug("Playlists searched",
|
|
zap.String("query", params.Query),
|
|
zap.Any("user_id", params.UserID),
|
|
zap.Any("is_public", params.IsPublic),
|
|
zap.Int("page", params.Page),
|
|
zap.Int("limit", params.Limit),
|
|
zap.Int64("total", total),
|
|
zap.Int("results", len(playlists)),
|
|
)
|
|
|
|
return playlists, total, nil
|
|
}
|
|
|
|
// UpdatePlaylist met à jour une playlist
|
|
// T0453: Utilise le repository pattern avec vérification d'ownership
|
|
// MIGRATION UUID: userID en uuid.UUID, playlistID en uuid.UUID
|
|
func (s *PlaylistService) UpdatePlaylist(ctx context.Context, playlistID uuid.UUID, userID uuid.UUID, title, description *string, isPublic *bool) (*models.Playlist, error) {
|
|
// Vérifier ownership
|
|
playlist, err := s.playlistRepo.GetByID(ctx, playlistID)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, ErrPlaylistNotFound
|
|
}
|
|
return nil, fmt.Errorf("failed to check playlist: %w", err)
|
|
}
|
|
|
|
if playlist.UserID != userID {
|
|
return nil, ErrAccessDenied
|
|
}
|
|
|
|
// Validation
|
|
if title != nil {
|
|
if *title == "" {
|
|
return nil, ErrTitleEmpty
|
|
}
|
|
if len(*title) > 200 {
|
|
return nil, ErrTitleTooLong
|
|
}
|
|
playlist.Title = *title
|
|
}
|
|
if description != nil {
|
|
playlist.Description = *description
|
|
}
|
|
if isPublic != nil {
|
|
playlist.IsPublic = *isPublic
|
|
}
|
|
|
|
if err := s.playlistRepo.Update(ctx, playlist); err != nil {
|
|
return nil, fmt.Errorf("failed to update playlist: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Playlist updated",
|
|
zap.String("playlist_id", playlistID.String()),
|
|
zap.String("user_id", userID.String()),
|
|
)
|
|
|
|
// T0509: Sauvegarder une version avant la mise à jour
|
|
if s.playlistVersionService != nil {
|
|
if _, err := s.playlistVersionService.SaveVersion(ctx, playlistID, userID, models.PlaylistVersionActionUpdated); err != nil {
|
|
s.logger.Warn("Failed to save playlist version", zap.Error(err))
|
|
}
|
|
}
|
|
|
|
// T0508: Envoyer une notification
|
|
if s.playlistNotificationService != nil {
|
|
if err := s.playlistNotificationService.NotifyPlaylistUpdated(ctx, playlistID, userID); err != nil {
|
|
s.logger.Warn("Failed to send playlist updated notification", zap.Error(err))
|
|
}
|
|
}
|
|
|
|
return playlist, nil
|
|
}
|
|
|
|
// DeletePlaylist supprime une playlist (soft delete)
|
|
// T0453: Utilise le repository pattern avec vérification d'ownership
|
|
// MIGRATION UUID: userID en uuid.UUID, playlistID en uuid.UUID
|
|
func (s *PlaylistService) DeletePlaylist(ctx context.Context, playlistID uuid.UUID, userID uuid.UUID) error {
|
|
// Vérifier ownership
|
|
playlist, err := s.playlistRepo.GetByID(ctx, playlistID)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return ErrPlaylistNotFound
|
|
}
|
|
return fmt.Errorf("failed to check playlist: %w", err)
|
|
}
|
|
|
|
if playlist.UserID != userID {
|
|
return ErrAccessDenied
|
|
}
|
|
|
|
if err := s.playlistRepo.Delete(ctx, playlistID); err != nil {
|
|
return fmt.Errorf("failed to delete playlist: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Playlist deleted",
|
|
zap.String("playlist_id", playlistID.String()),
|
|
zap.String("user_id", userID.String()),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddTrackToPlaylist ajoute un track à une playlist
|
|
// T0466: Implémentation avec PlaylistTrackRepository
|
|
// MIGRATION UUID: userID en uuid.UUID, playlistID et trackID en uuid.UUID
|
|
func (s *PlaylistService) AddTrackToPlaylist(ctx context.Context, playlistID, trackID uuid.UUID, userID uuid.UUID, position int) error {
|
|
// Vérifier ownership
|
|
playlist, err := s.playlistRepo.GetByID(ctx, playlistID)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return ErrPlaylistNotFound
|
|
}
|
|
return fmt.Errorf("failed to check playlist: %w", err)
|
|
}
|
|
|
|
if playlist.UserID != userID {
|
|
return ErrAccessDenied
|
|
}
|
|
|
|
// Ajouter le track via le repository (qui vérifie l'existence du track)
|
|
if err := s.playlistTrackRepo.AddTrack(ctx, playlistID, trackID, position); err != nil {
|
|
if err.Error() == "track not found" {
|
|
return ErrTrackNotFound
|
|
}
|
|
if err.Error() == "track already in playlist" {
|
|
return ErrTrackAlreadyInPlaylist
|
|
}
|
|
return fmt.Errorf("failed to add track to playlist: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Track added to playlist",
|
|
zap.String("playlist_id", playlistID.String()),
|
|
zap.String("track_id", trackID.String()),
|
|
zap.String("user_id", userID.String()),
|
|
zap.Int("position", position),
|
|
)
|
|
|
|
// T0508: Envoyer une notification (trackTitle sera vide, le service utilisera un message générique)
|
|
if s.playlistNotificationService != nil {
|
|
if err := s.playlistNotificationService.NotifyTrackAdded(ctx, playlistID, "", userID); err != nil {
|
|
s.logger.Warn("Failed to send track added notification", zap.Error(err))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddTrack est un alias pour AddTrackToPlaylist (compatibilité)
|
|
// MIGRATION UUID: userID en uuid.UUID, playlistID et trackID en uuid.UUID
|
|
func (s *PlaylistService) AddTrack(ctx context.Context, playlistID, trackID uuid.UUID, userID uuid.UUID) error {
|
|
return s.AddTrackToPlaylist(ctx, playlistID, trackID, userID, 0)
|
|
}
|
|
|
|
// RemoveTrackFromPlaylist retire un track d'une playlist
|
|
// T0466: Implémentation avec PlaylistTrackRepository
|
|
// MIGRATION UUID: userID en uuid.UUID, playlistID et trackID en uuid.UUID
|
|
func (s *PlaylistService) RemoveTrackFromPlaylist(ctx context.Context, playlistID, trackID uuid.UUID, userID uuid.UUID) error {
|
|
// Vérifier ownership
|
|
playlist, err := s.playlistRepo.GetByID(ctx, playlistID)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return ErrPlaylistNotFound
|
|
}
|
|
return fmt.Errorf("failed to check playlist: %w", err)
|
|
}
|
|
|
|
if playlist.UserID != userID {
|
|
return errors.New("forbidden")
|
|
}
|
|
|
|
// Retirer le track via le repository
|
|
if err := s.playlistTrackRepo.RemoveTrack(ctx, playlistID, trackID); err != nil {
|
|
if err.Error() == "track not found in playlist" {
|
|
return errors.New("track not found in playlist")
|
|
}
|
|
return fmt.Errorf("failed to remove track from playlist: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Track removed from playlist",
|
|
zap.String("playlist_id", playlistID.String()),
|
|
zap.String("track_id", trackID.String()),
|
|
zap.String("user_id", userID.String()),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemoveTrack est un alias pour RemoveTrackFromPlaylist (compatibilité)
|
|
// MIGRATION UUID: userID en uuid.UUID, playlistID et trackID en uuid.UUID
|
|
func (s *PlaylistService) RemoveTrack(ctx context.Context, playlistID, trackID uuid.UUID, userID uuid.UUID) error {
|
|
return s.RemoveTrackFromPlaylist(ctx, playlistID, trackID, userID)
|
|
}
|
|
|
|
// ReorderPlaylistTracks réorganise les tracks d'une playlist
|
|
// T0466: Implémentation avec PlaylistTrackRepository
|
|
// trackPositions est une map de trackID -> position
|
|
// MIGRATION UUID: userID en uuid.UUID, playlistID en uuid.UUID
|
|
func (s *PlaylistService) ReorderPlaylistTracks(ctx context.Context, playlistID uuid.UUID, userID uuid.UUID, trackPositions map[uuid.UUID]int) error {
|
|
// Vérifier ownership
|
|
playlist, err := s.playlistRepo.GetByID(ctx, playlistID)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return ErrPlaylistNotFound
|
|
}
|
|
return fmt.Errorf("failed to check playlist: %w", err)
|
|
}
|
|
|
|
if playlist.UserID != userID {
|
|
return errors.New("forbidden")
|
|
}
|
|
|
|
// Réorganiser les tracks via le repository
|
|
if err := s.playlistTrackRepo.ReorderTracks(ctx, playlistID, trackPositions); err != nil {
|
|
return fmt.Errorf("failed to reorder tracks: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Playlist tracks reordered",
|
|
zap.String("playlist_id", playlistID.String()),
|
|
zap.String("user_id", userID.String()),
|
|
zap.Int("tracks_count", len(trackPositions)),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// ReorderTracks est un alias pour ReorderPlaylistTracks (compatibilité)
|
|
// trackIDs est une liste de trackIDs dans l'ordre souhaité (position = index + 1)
|
|
// MIGRATION UUID: userID en uuid.UUID, playlistID en uuid.UUID
|
|
func (s *PlaylistService) ReorderTracks(ctx context.Context, playlistID uuid.UUID, userID uuid.UUID, trackIDs []uuid.UUID) error {
|
|
trackPositions := make(map[uuid.UUID]int)
|
|
for i, trackID := range trackIDs {
|
|
trackPositions[trackID] = i + 1
|
|
}
|
|
return s.ReorderPlaylistTracks(ctx, playlistID, userID, trackPositions)
|
|
}
|
|
|
|
// AddCollaborator ajoute un collaborateur à une playlist
|
|
// T0478: Implémentation avec vérification d'ownership
|
|
// MIGRATION UUID: ownerID et collaboratorUserID migrés vers uuid.UUID, playlistID en uuid.UUID
|
|
func (s *PlaylistService) AddCollaborator(ctx context.Context, playlistID uuid.UUID, ownerID, collaboratorUserID uuid.UUID, permission models.PlaylistPermission) (*models.PlaylistCollaborator, error) {
|
|
// Vérifier ownership
|
|
playlist, err := s.playlistRepo.GetByID(ctx, playlistID)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, ErrPlaylistNotFound
|
|
}
|
|
return nil, fmt.Errorf("failed to check playlist: %w", err)
|
|
}
|
|
|
|
if playlist.UserID != ownerID {
|
|
return nil, errors.New("forbidden: only playlist owner can add collaborators")
|
|
}
|
|
|
|
// Vérifier que l'utilisateur collaborateur existe
|
|
if gormRepo, ok := s.userRepo.(interface {
|
|
Exists(ctx context.Context, userID uuid.UUID) (bool, error)
|
|
}); ok {
|
|
exists, err := gormRepo.Exists(ctx, collaboratorUserID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to check user: %w", err)
|
|
}
|
|
if !exists {
|
|
return nil, errors.New("user not found")
|
|
}
|
|
} else {
|
|
_, err := s.userRepo.GetByID(collaboratorUserID.String())
|
|
if err != nil {
|
|
return nil, errors.New("user not found")
|
|
}
|
|
}
|
|
|
|
// Vérifier qu'on n'ajoute pas le propriétaire comme collaborateur
|
|
if collaboratorUserID == ownerID {
|
|
return nil, errors.New("cannot add playlist owner as collaborator")
|
|
}
|
|
|
|
// Ajouter le collaborateur via le repository
|
|
collaborator, err := s.playlistCollaboratorRepo.AddCollaborator(ctx, playlistID, collaboratorUserID, permission)
|
|
if err != nil {
|
|
if err.Error() == "collaborator already exists" {
|
|
return nil, errors.New("user is already a collaborator")
|
|
}
|
|
return nil, fmt.Errorf("failed to add collaborator: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Collaborator added to playlist",
|
|
zap.String("playlist_id", playlistID.String()),
|
|
zap.String("owner_id", ownerID.String()),
|
|
zap.String("collaborator_user_id", collaboratorUserID.String()),
|
|
zap.String("permission", string(permission)),
|
|
)
|
|
|
|
// T0508: Envoyer une notification au collaborateur
|
|
if s.playlistNotificationService != nil {
|
|
if err := s.playlistNotificationService.NotifyCollaboratorAdded(ctx, playlistID, collaboratorUserID, ownerID); err != nil {
|
|
s.logger.Warn("Failed to send collaborator added notification", zap.Error(err))
|
|
}
|
|
}
|
|
|
|
return collaborator, nil
|
|
}
|
|
|
|
// RemoveCollaborator retire un collaborateur d'une playlist
|
|
// T0478: Implémentation avec vérification d'ownership
|
|
// MIGRATION UUID: ownerID et collaboratorUserID migrés vers uuid.UUID, playlistID en uuid.UUID
|
|
func (s *PlaylistService) RemoveCollaborator(ctx context.Context, playlistID uuid.UUID, ownerID, collaboratorUserID uuid.UUID) error {
|
|
// Vérifier ownership
|
|
playlist, err := s.playlistRepo.GetByID(ctx, playlistID)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return ErrPlaylistNotFound
|
|
}
|
|
return fmt.Errorf("failed to check playlist: %w", err)
|
|
}
|
|
|
|
if playlist.UserID != ownerID {
|
|
return errors.New("forbidden: only playlist owner can remove collaborators")
|
|
}
|
|
|
|
// Retirer le collaborateur via le repository
|
|
if err := s.playlistCollaboratorRepo.RemoveCollaborator(ctx, playlistID, collaboratorUserID); err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return errors.New("collaborator not found")
|
|
}
|
|
return fmt.Errorf("failed to remove collaborator: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Collaborator removed from playlist",
|
|
zap.String("playlist_id", playlistID.String()),
|
|
zap.String("owner_id", ownerID.String()),
|
|
zap.String("collaborator_user_id", collaboratorUserID.String()),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateCollaboratorPermission met à jour la permission d'un collaborateur
|
|
// T0478: Implémentation avec vérification d'ownership
|
|
// MIGRATION UUID: ownerID et collaboratorUserID migrés vers uuid.UUID, playlistID en uuid.UUID
|
|
func (s *PlaylistService) UpdateCollaboratorPermission(ctx context.Context, playlistID uuid.UUID, ownerID, collaboratorUserID uuid.UUID, permission models.PlaylistPermission) error {
|
|
// Vérifier ownership
|
|
playlist, err := s.playlistRepo.GetByID(ctx, playlistID)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return ErrPlaylistNotFound
|
|
}
|
|
return fmt.Errorf("failed to check playlist: %w", err)
|
|
}
|
|
|
|
if playlist.UserID != ownerID {
|
|
return errors.New("forbidden: only playlist owner can update collaborator permissions")
|
|
}
|
|
|
|
// Valider la permission
|
|
if !permission.IsValid() {
|
|
return errors.New("invalid permission")
|
|
}
|
|
|
|
// Mettre à jour la permission via le repository
|
|
if err := s.playlistCollaboratorRepo.UpdatePermission(ctx, playlistID, collaboratorUserID, permission); err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return errors.New("collaborator not found")
|
|
}
|
|
return fmt.Errorf("failed to update collaborator permission: %w", err)
|
|
}
|
|
|
|
s.logger.Info("Collaborator permission updated",
|
|
zap.String("playlist_id", playlistID.String()),
|
|
zap.String("owner_id", ownerID.String()),
|
|
zap.String("collaborator_user_id", collaboratorUserID.String()),
|
|
zap.String("permission", string(permission)),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// CheckPermission vérifie si un utilisateur a une certaine permission sur une playlist
|
|
// T0478: Vérifie les permissions (read, write, admin)
|
|
// MIGRATION UUID: userID en uuid.UUID, playlistID en uuid.UUID
|
|
func (s *PlaylistService) CheckPermission(ctx context.Context, playlistID uuid.UUID, userID uuid.UUID, requiredPermission models.PlaylistPermission) (bool, error) {
|
|
// Récupérer la playlist
|
|
playlist, err := s.playlistRepo.GetByID(ctx, playlistID)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return false, errors.New("playlist not found")
|
|
}
|
|
return false, fmt.Errorf("failed to check playlist: %w", err)
|
|
}
|
|
|
|
// Le propriétaire a toujours toutes les permissions
|
|
if playlist.UserID == userID {
|
|
return true, nil
|
|
}
|
|
|
|
// Si la playlist est publique, tout le monde peut la lire
|
|
if playlist.IsPublic && requiredPermission == models.PlaylistPermissionRead {
|
|
return true, nil
|
|
}
|
|
|
|
// Vérifier si l'utilisateur est collaborateur
|
|
collaborator, err := s.playlistCollaboratorRepo.GetCollaborator(ctx, playlistID, userID)
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
return false, nil // Pas de permission
|
|
}
|
|
return false, fmt.Errorf("failed to check collaborator: %w", err)
|
|
}
|
|
|
|
// Vérifier la permission selon le niveau requis
|
|
switch requiredPermission {
|
|
case models.PlaylistPermissionRead:
|
|
return collaborator.CanRead(), nil
|
|
case models.PlaylistPermissionWrite:
|
|
return collaborator.CanWrite(), nil
|
|
case models.PlaylistPermissionAdmin:
|
|
return collaborator.CanAdmin(), nil
|
|
default:
|
|
return false, errors.New("invalid permission")
|
|
}
|
|
}
|
|
|
|
// GetCollaborators récupère tous les collaborateurs d'une playlist
|
|
// T0478: Helper method pour récupérer les collaborateurs
|
|
// MIGRATION UUID: userID en uuid.UUID, playlistID en uuid.UUID
|
|
func (s *PlaylistService) GetCollaborators(ctx context.Context, playlistID uuid.UUID, userID uuid.UUID) ([]*models.PlaylistCollaborator, error) {
|
|
// Vérifier que l'utilisateur a accès à la playlist (propriétaire ou collaborateur)
|
|
hasAccess, err := s.CheckPermission(ctx, playlistID, userID, models.PlaylistPermissionRead)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !hasAccess {
|
|
return nil, errors.New("forbidden: access denied")
|
|
}
|
|
|
|
// Récupérer les collaborateurs
|
|
collaborators, err := s.playlistCollaboratorRepo.GetCollaborators(ctx, playlistID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get collaborators: %w", err)
|
|
}
|
|
|
|
return collaborators, nil
|
|
}
|
|
|
|
// CreateShareLink crée un nouveau lien de partage public pour une playlist
|
|
// T0488: Create Playlist Public Share Link
|
|
// MIGRATION UUID: userID en uuid.UUID, playlistID en uuid.UUID
|
|
func (s *PlaylistService) CreateShareLink(ctx context.Context, playlistID uuid.UUID, userID uuid.UUID, expiresAt *time.Time) (*models.PlaylistShareLink, error) {
|
|
if s.playlistShareService == nil {
|
|
return nil, errors.New("playlist share service not initialized")
|
|
}
|
|
|
|
// Vérifier que l'utilisateur a la permission (owner ou admin)
|
|
hasPermission, err := s.CheckPermission(ctx, playlistID, userID, models.PlaylistPermissionAdmin)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !hasPermission {
|
|
// Vérifier si l'utilisateur est le propriétaire
|
|
playlist, err := s.GetPlaylist(ctx, playlistID, &userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if playlist.UserID != userID {
|
|
return nil, errors.New("forbidden: only owner or admin can create share links")
|
|
}
|
|
}
|
|
|
|
shareLink, err := s.playlistShareService.CreateShareLink(ctx, playlistID, userID, expiresAt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// T0508: Envoyer une notification
|
|
if s.playlistNotificationService != nil {
|
|
if err := s.playlistNotificationService.NotifyPlaylistShared(ctx, playlistID, userID); err != nil {
|
|
s.logger.Warn("Failed to send playlist shared notification", zap.Error(err))
|
|
}
|
|
}
|
|
|
|
return shareLink, nil
|
|
}
|
|
|
|
// FollowPlaylist permet à un utilisateur de suivre une playlist
|
|
// T0489: Create Playlist Follow Feature
|
|
// MIGRATION UUID: userID en uuid.UUID, playlistID en uuid.UUID
|
|
func (s *PlaylistService) FollowPlaylist(ctx context.Context, playlistID uuid.UUID, userID uuid.UUID) error {
|
|
if s.playlistFollowService == nil {
|
|
return errors.New("playlist follow service not initialized")
|
|
}
|
|
return s.playlistFollowService.FollowPlaylist(ctx, userID, playlistID)
|
|
}
|
|
|
|
// UnfollowPlaylist permet à un utilisateur de ne plus suivre une playlist
|
|
// T0489: Create Playlist Follow Feature
|
|
// MIGRATION UUID: userID en uuid.UUID, playlistID en uuid.UUID
|
|
func (s *PlaylistService) UnfollowPlaylist(ctx context.Context, playlistID uuid.UUID, userID uuid.UUID) error {
|
|
if s.playlistFollowService == nil {
|
|
return errors.New("playlist follow service not initialized")
|
|
}
|
|
return s.playlistFollowService.UnfollowPlaylist(ctx, userID, playlistID)
|
|
}
|
|
|
|
// IsFollowing vérifie si un utilisateur suit une playlist
|
|
// T0489: Create Playlist Follow Feature
|
|
// MIGRATION UUID: userID en uuid.UUID, playlistID en uuid.UUID
|
|
func (s *PlaylistService) IsFollowing(ctx context.Context, playlistID uuid.UUID, userID uuid.UUID) (bool, error) {
|
|
if s.playlistFollowService == nil {
|
|
return false, errors.New("playlist follow service not initialized")
|
|
}
|
|
return s.playlistFollowService.IsFollowing(ctx, userID, playlistID)
|
|
}
|