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

@@ -0,0 +1,15 @@
CREATE TABLE "stems" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"track_id" uuid NOT NULL,
"name" varchar(255) NOT NULL,
"original_file_name" varchar(500) NOT NULL,
"mime_type" varchar(100) NOT NULL,
"file_size" bigint NOT NULL,
"file_key" text NOT NULL,
"sort_order" integer DEFAULT 0 NOT NULL,
"created_by_id" uuid NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "stems" ADD CONSTRAINT "stems_track_id_tracks_id_fk" FOREIGN KEY ("track_id") REFERENCES "public"."tracks"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "stems" ADD CONSTRAINT "stems_created_by_id_users_id_fk" FOREIGN KEY ("created_by_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;

File diff suppressed because it is too large Load Diff

View File

@@ -43,6 +43,13 @@
"when": 1776012912970,
"tag": "0005_rare_triathlon",
"breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1776094119472,
"tag": "0006_brown_lily_hollister",
"breakpoints": true
}
]
}

View File

@@ -77,3 +77,20 @@ export const versions = pgTable('versions', {
.notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const stems = pgTable('stems', {
id: uuid('id').defaultRandom().primaryKey(),
trackId: uuid('track_id')
.references(() => tracks.id, { onDelete: 'cascade' })
.notNull(),
name: varchar('name', { length: 255 }).notNull(),
originalFileName: varchar('original_file_name', { length: 500 }).notNull(),
mimeType: varchar('mime_type', { length: 100 }).notNull(),
fileSize: bigint('file_size', { mode: 'number' }).notNull(),
fileKey: text('file_key').notNull(),
sortOrder: integer('sort_order').default(0).notNull(),
createdById: uuid('created_by_id')
.references(() => users.id)
.notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});