feat: add STEM file support per track

- DB: stems table with trackId FK, fileKey, sortOrder, createdById
- API: GET/POST/DELETE stems, presigned upload URL, ZIP download via fflate
- Web: StemUploadDropzone (multi-file, batch upload, progress bars)
- Web: StemList with download-all-ZIP and per-stem delete
- Web: STEMs tab in track detail view
- Icon: add 'music' icon to inline set
- Auto-migration runs stems table creation on boot

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Robin Choice
2026-04-13 18:13:01 +02:00
parent df54fde710
commit 9530add1ff
15 changed files with 1812 additions and 4 deletions

View File

@@ -53,6 +53,20 @@ export const updateVersionSchema = z.object({
branchLabel: z.string().max(100).nullable().optional(),
});
export const requestStemUploadUrlSchema = z.object({
fileName: z.string().min(1),
mimeType: z.string().min(1),
fileSize: z.number().int().positive().max(MAX_FILE_SIZE),
});
export const createStemSchema = z.object({
fileKey: z.string().min(1),
name: z.string().min(1).max(255),
originalFileName: z.string().min(1),
mimeType: z.string().min(1),
fileSize: z.number().int().positive(),
});
export const createShareLinkSchema = z.object({
expiresAt: z.string().datetime().optional(),
allowComments: z.boolean().optional(),
@@ -75,3 +89,5 @@ export type UpdateVersionInput = z.infer<typeof updateVersionSchema>;
export type CreateShareLinkInput = z.infer<typeof createShareLinkSchema>;
export type CoverUploadInput = z.infer<typeof coverUploadSchema>;
export type GuestCommentInput = z.infer<typeof guestCommentSchema>;
export type RequestStemUploadUrlInput = z.infer<typeof requestStemUploadUrlSchema>;
export type CreateStemInput = z.infer<typeof createStemSchema>;