refactor: simplify stem code per review

- async zip() instead of zipSync (non-blocking for large files)
- null check for track in DELETE endpoint (was non-null assertion)
- formatFileSize extracted to format.ts, imported in StemList
- Stem type exported from StemList, removed duplicate in +page.svelte
- files[idx].progress = p direct Svelte 5 mutation (no spread)
- remove narrative comments from stems.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Robin Choice
2026-04-13 18:44:12 +02:00
parent e63dc30a7f
commit ccd7ed3a93
5 changed files with 23 additions and 28 deletions

View File

@@ -5,13 +5,12 @@ import { requestStemUploadUrlSchema, createStemSchema } from '@music-hub/shared'
import { tracks, stems, projectMembers } from '@music-hub/db';
import { requireAuth } from '../middleware/auth.js';
import { createUploadUrl, getObjectBuffer, deleteObject } from '../storage/s3.js';
import { zipSync } from 'fflate';
import { zip } from 'fflate';
import type { AppEnv } from '../types.js';
export const stemRoutes = new Hono<AppEnv>()
.use('*', requireAuth)
// List stems for a track
.get('/track/:trackId', async (c) => {
const db = c.get('db');
const userId = c.get('userId');
@@ -36,7 +35,6 @@ export const stemRoutes = new Hono<AppEnv>()
return c.json({ stems: trackStems });
})
// Request presigned upload URL
.post('/track/:trackId/upload-url', zValidator('json', requestStemUploadUrlSchema), async (c) => {
const db = c.get('db');
const userId = c.get('userId');
@@ -60,7 +58,6 @@ export const stemRoutes = new Hono<AppEnv>()
return c.json({ uploadUrl, fileKey, stemId });
})
// Register stem after upload
.post('/track/:trackId', zValidator('json', createStemSchema), async (c) => {
const db = c.get('db');
const userId = c.get('userId');
@@ -93,7 +90,6 @@ export const stemRoutes = new Hono<AppEnv>()
return c.json({ stem }, 201);
})
// Delete stem
.delete('/:id', async (c) => {
const db = c.get('db');
const userId = c.get('userId');
@@ -103,10 +99,12 @@ export const stemRoutes = new Hono<AppEnv>()
if (!stem) return c.json({ error: 'Not found' }, 404);
const [track] = await db.select().from(tracks).where(eq(tracks.id, stem.trackId)).limit(1);
if (!track) return c.json({ error: 'Not found' }, 404);
const [membership] = await db
.select()
.from(projectMembers)
.where(and(eq(projectMembers.projectId, track!.projectId), eq(projectMembers.userId, userId)))
.where(and(eq(projectMembers.projectId, track.projectId), eq(projectMembers.userId, userId)))
.limit(1);
if (!membership || (membership.role !== 'owner' && stem.createdById !== userId)) {
@@ -118,7 +116,6 @@ export const stemRoutes = new Hono<AppEnv>()
return c.json({ message: 'Stem deleted' });
})
// Download all stems as ZIP
.get('/track/:trackId/download-zip', async (c) => {
const db = c.get('db');
const userId = c.get('userId');
@@ -142,23 +139,21 @@ export const stemRoutes = new Hono<AppEnv>()
if (trackStems.length === 0) return c.json({ error: 'No stems found' }, 404);
// Download all files and build ZIP
const files: Record<string, Uint8Array> = {};
await Promise.all(
trackStems.map(async (stem) => {
const buf = await getObjectBuffer(stem.fileKey);
files[stem.originalFileName] = buf;
}),
const fileEntries = await Promise.all(
trackStems.map(async (stem) => [stem.originalFileName, await getObjectBuffer(stem.fileKey)] as const),
);
const files = Object.fromEntries(fileEntries) as Record<string, Uint8Array>;
const zipped = zipSync(files);
const zipped = await new Promise<Uint8Array>((resolve, reject) => {
zip(files, (err, data) => (err ? reject(err) : resolve(data)));
});
const zipName = `${track.name.replace(/[^a-z0-9]/gi, '-').toLowerCase()}-stems.zip`;
const body = zipped.buffer.slice(zipped.byteOffset, zipped.byteOffset + zipped.byteLength) as ArrayBuffer;
return new Response(zipped, {
return new Response(new Blob([body], { type: 'application/zip' }), {
headers: {
'Content-Type': 'application/zip',
'Content-Disposition': `attachment; filename="${zipName}"`,
'Content-Length': String(zipped.length),
'Content-Length': String(zipped.byteLength),
},
});
});