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" } }