veza/veza-backend-api/internal/services/track_stem_service.go
senke 871a0f2a05
Some checks failed
Backend API CI / test-integration (push) Failing after 0s
Frontend CI / test (push) Failing after 0s
Storybook Audit / Build & audit Storybook (push) Failing after 0s
Backend API CI / test-unit (push) Failing after 0s
feat(v0.10.7): Collaboration Temps Réel F481-F483
- F481: Co-listening sessions (WebSocket sync, ListenTogether page)
- F482: Stem sharing (upload/list/download wav,aiff,flac)
- F483: Collaborative rooms (type collaborative, max 10, invite-only)
- Roadmap: v0.10.7 → DONE
2026-03-10 13:34:16 +01:00

147 lines
4.1 KiB
Go

package services
import (
"context"
"errors"
"fmt"
"io"
"mime/multipart"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/google/uuid"
"go.uber.org/zap"
"gorm.io/gorm"
"veza-backend-api/internal/models"
)
var (
ErrTrackStemNotFound = errors.New("track stem not found")
ErrTrackStemForbidden = errors.New("forbidden: only track owner can manage stems")
ErrTrackStemInvalidExt = errors.New("invalid stem format: only wav, aiff, flac allowed")
)
// Allowed stem formats (v0.10.7 F482)
var stemFormats = map[string]string{
".wav": "WAV",
".aiff": "AIFF",
".aif": "AIFF",
".flac": "FLAC",
}
// TrackStemService manages stem upload/download for tracks (v0.10.7 F482)
type TrackStemService struct {
db *gorm.DB
uploadDir string
logger *zap.Logger
}
// NewTrackStemService creates a new TrackStemService
func NewTrackStemService(db *gorm.DB, uploadDir string, logger *zap.Logger) *TrackStemService {
if uploadDir == "" {
uploadDir = "uploads/tracks"
}
stemsDir := filepath.Join(uploadDir, "stems")
return &TrackStemService{db: db, uploadDir: stemsDir, logger: logger}
}
// stemNameRegex allows alphanumeric, underscore, hyphen
var stemNameRegex = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
// UploadStem saves a stem file and creates a record
func (s *TrackStemService) UploadStem(ctx context.Context, trackID, userID uuid.UUID, fileHeader *multipart.FileHeader, name string) (*models.TrackStem, error) {
// Get track and verify ownership
var track models.Track
if err := s.db.WithContext(ctx).First(&track, "id = ?", trackID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrTrackNotFound // from services/errors.go
}
return nil, err
}
if track.UserID != userID {
return nil, ErrTrackStemForbidden
}
// Validate stem name
if name == "" {
name = strings.TrimSuffix(fileHeader.Filename, filepath.Ext(fileHeader.Filename))
}
if !stemNameRegex.MatchString(name) {
return nil, fmt.Errorf("%w: name must be alphanumeric, underscore or hyphen", ErrTrackStemInvalidExt)
}
ext := strings.ToLower(filepath.Ext(fileHeader.Filename))
format, ok := stemFormats[ext]
if !ok {
return nil, ErrTrackStemInvalidExt
}
// Create stems dir: uploads/tracks/stems/{track_id}/
trackStemsDir := filepath.Join(s.uploadDir, trackID.String())
if err := os.MkdirAll(trackStemsDir, 0755); err != nil {
s.logger.Error("Failed to create stems dir", zap.Error(err))
return nil, err
}
filename := fmt.Sprintf("%s_%s%s", name, uuid.New().String()[:8], ext)
filePath := filepath.Join(trackStemsDir, filename)
src, err := fileHeader.Open()
if err != nil {
return nil, err
}
defer src.Close()
dst, err := os.Create(filePath)
if err != nil {
return nil, err
}
defer dst.Close()
if _, err := io.Copy(dst, src); err != nil {
os.Remove(filePath)
return nil, err
}
stem := &models.TrackStem{
TrackID: trackID,
Name: name,
FilePath: filePath,
Format: format,
SizeBytes: fileHeader.Size,
}
if err := s.db.WithContext(ctx).Create(stem).Error; err != nil {
os.Remove(filePath)
return nil, err
}
return stem, nil
}
// ListStems returns stems for a track
func (s *TrackStemService) ListStems(ctx context.Context, trackID uuid.UUID) ([]*models.TrackStem, error) {
var stems []*models.TrackStem
if err := s.db.WithContext(ctx).Where("track_id = ?", trackID).Order("name").Find(&stems).Error; err != nil {
return nil, err
}
return stems, nil
}
// GetStem returns a stem by track_id and name for download
func (s *TrackStemService) GetStem(ctx context.Context, trackID uuid.UUID, name string) (*models.TrackStem, error) {
var stem models.TrackStem
if err := s.db.WithContext(ctx).Where("track_id = ? AND name = ?", trackID, name).First(&stem).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrTrackStemNotFound
}
return nil, err
}
return &stem, nil
}
// CanAccessStem checks if user can download stem (owner or track is public - v0.10.7 F482)
func (s *TrackStemService) CanAccessStem(ctx context.Context, track *models.Track, userID uuid.UUID) bool {
return track.UserID == userID || track.IsPublic
}