veza/veza-backend-api/internal/handlers/track_stem_handler.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

199 lines
5.2 KiB
Go

package handlers
import (
"context"
"net/http"
"os"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
apperrors "veza-backend-api/internal/errors"
"veza-backend-api/internal/models"
"veza-backend-api/internal/services"
)
// TrackLoader loads track for ownership check (v0.10.7 F482)
type TrackLoader interface {
GetTrackByID(ctx context.Context, trackID uuid.UUID) (*models.Track, error)
}
// TrackStemHandler handles stem upload/download for tracks (v0.10.7 F482)
type TrackStemHandler struct {
stemService *services.TrackStemService
trackLoader TrackLoader
logger *zap.Logger
}
// NewTrackStemHandler creates a new TrackStemHandler
func NewTrackStemHandler(stemService *services.TrackStemService, trackLoader TrackLoader, logger *zap.Logger) *TrackStemHandler {
return &TrackStemHandler{stemService: stemService, trackLoader: trackLoader, logger: logger}
}
func (h *TrackStemHandler) getUserID(c *gin.Context) (uuid.UUID, bool) {
uid, exists := c.Get("user_id")
if !exists {
RespondWithAppError(c, apperrors.NewUnauthorizedError("unauthorized"))
return uuid.Nil, false
}
userID, ok := uid.(uuid.UUID)
if !ok || userID == uuid.Nil {
RespondWithAppError(c, apperrors.NewUnauthorizedError("unauthorized"))
return uuid.Nil, false
}
return userID, true
}
// UploadStem POST /tracks/:id/stems
func (h *TrackStemHandler) UploadStem(c *gin.Context) {
userID, ok := h.getUserID(c)
if !ok {
return
}
trackIDStr := c.Param("id")
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("invalid track id"))
return
}
form, err := c.MultipartForm()
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("multipart form required"))
return
}
files := form.File["file"]
if len(files) == 0 {
RespondWithAppError(c, apperrors.NewValidationError("file is required"))
return
}
name := c.PostForm("name")
stem, err := h.stemService.UploadStem(c.Request.Context(), trackID, userID, files[0], name)
if err != nil {
if err == services.ErrTrackNotFound {
RespondWithAppError(c, apperrors.NewNotFoundError("track"))
return
}
if err == services.ErrTrackStemForbidden {
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
return
}
if err == services.ErrTrackStemInvalidExt {
RespondWithAppError(c, apperrors.NewValidationError("invalid format: only wav, aiff, flac allowed"))
return
}
h.logger.Error("Upload stem failed", zap.Error(err))
RespondWithAppError(c, apperrors.NewInternalError("failed to upload stem"))
return
}
RespondSuccess(c, http.StatusCreated, gin.H{
"stem": gin.H{
"id": stem.ID,
"track_id": stem.TrackID,
"name": stem.Name,
"format": stem.Format,
"size_bytes": stem.SizeBytes,
"created_at": stem.CreatedAt,
},
})
}
// ListStems GET /tracks/:id/stems
func (h *TrackStemHandler) ListStems(c *gin.Context) {
trackIDStr := c.Param("id")
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("invalid track id"))
return
}
stems, err := h.stemService.ListStems(c.Request.Context(), trackID)
if err != nil {
h.logger.Error("List stems failed", zap.Error(err))
RespondWithAppError(c, apperrors.NewInternalError("failed to list stems"))
return
}
list := make([]gin.H, len(stems))
for i, s := range stems {
list[i] = gin.H{
"id": s.ID,
"track_id": s.TrackID,
"name": s.Name,
"format": s.Format,
"size_bytes": s.SizeBytes,
"created_at": s.CreatedAt,
}
}
RespondSuccess(c, http.StatusOK, gin.H{"stems": list})
}
// DownloadStem GET /tracks/:id/stems/:name/download
func (h *TrackStemHandler) DownloadStem(c *gin.Context) {
userID, ok := h.getUserID(c)
if !ok {
return
}
trackIDStr := c.Param("id")
trackID, err := uuid.Parse(trackIDStr)
if err != nil {
RespondWithAppError(c, apperrors.NewValidationError("invalid track id"))
return
}
name := c.Param("name")
if name == "" {
RespondWithAppError(c, apperrors.NewValidationError("stem name is required"))
return
}
stem, err := h.stemService.GetStem(c.Request.Context(), trackID, name)
if err != nil {
if err == services.ErrTrackStemNotFound {
RespondWithAppError(c, apperrors.NewNotFoundError("stem"))
return
}
RespondWithAppError(c, apperrors.NewInternalError("failed to get stem"))
return
}
track, err := h.trackLoader.GetTrackByID(c.Request.Context(), trackID)
if err != nil || track == nil {
RespondWithAppError(c, apperrors.NewNotFoundError("track"))
return
}
if !h.stemService.CanAccessStem(c.Request.Context(), track, userID) {
RespondWithAppError(c, apperrors.NewForbiddenError("forbidden"))
return
}
if _, err := os.Stat(stem.FilePath); os.IsNotExist(err) {
RespondWithAppError(c, apperrors.NewNotFoundError("stem file not found"))
return
}
ct := stemContentType(stem.Format)
c.Header("Content-Type", ct)
c.Header("Content-Disposition", `attachment; filename="`+stem.Name+"."+strings.ToLower(stem.Format)+`"`)
c.File(stem.FilePath)
}
func stemContentType(format string) string {
switch strings.ToUpper(format) {
case "WAV":
return "audio/wav"
case "AIFF":
return "audio/aiff"
case "FLAC":
return "audio/flac"
default:
return "application/octet-stream"
}
}