- 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
199 lines
5.2 KiB
Go
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"
|
|
}
|
|
}
|