import { fail } from '@sveltejs/kit'; import { db } from '$lib/server/db/index.js'; import { audioFile } from '$lib/server/db/schema.js'; import { eq, isNull } from 'drizzle-orm'; import { uploadToS3, deleteFromS3, generateAudioS3Key } from '$lib/server/s3.js'; import { exec } from 'child_process'; import { promisify } from 'util'; import fs from 'fs/promises'; import path from 'path'; import os from 'os'; const execAsync = promisify(exec); async function getAudioDuration(buffer) { try { // Create temporary file const tempDir = os.tmpdir(); const tempFilePath = path.join(tempDir, `audio_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`); // Write buffer to temp file await fs.writeFile(tempFilePath, buffer); try { // Use ffprobe to get duration const { stdout } = await execAsync(`ffprobe -v quiet -show_entries format=duration -of csv=p=0 "${tempFilePath}"`); const duration = parseFloat(stdout.trim()); // Clean up temp file await fs.unlink(tempFilePath); return isNaN(duration) ? null : duration; } catch (error) { // Clean up temp file even if ffprobe fails try { await fs.unlink(tempFilePath); } catch {} console.error('Error extracting audio duration:', error); return null; } } catch (error) { console.error('Error creating temp file for duration extraction:', error); return null; } } export async function load() { const audioFiles = await db.select({ id: audioFile.id, filename: audioFile.filename, contentType: audioFile.contentType, duration: audioFile.duration, fileSize: audioFile.fileSize, createdAt: audioFile.createdAt }) .from(audioFile) .where(isNull(audioFile.deletedAt)); // Only show active audio files return { audioFiles }; } export const actions = { upload: async ({ request }) => { const data = await request.formData(); const file = data.get('audioFile'); if (!file || file.size === 0) { return fail(400, { missing: true }); } if (!file.type.startsWith('audio/')) { return fail(400, { invalidType: true }); } const id = crypto.randomUUID(); const buffer = Buffer.from(await file.arrayBuffer()); const s3Key = generateAudioS3Key(id, file.name); try { // Extract duration before uploading const duration = await getAudioDuration(buffer); // Upload to S3 first await uploadToS3(s3Key, buffer, file.type); // Then save metadata to database with calculated duration await db.insert(audioFile).values({ id, filename: file.name, contentType: file.type, s3Key, duration, fileSize: file.size, createdAt: new Date() }); return { success: true }; } catch (error) { console.error('Error uploading audio file:', error); return fail(500, { error: error.message || 'Upload failed' }); } }, delete: async ({ request }) => { const data = await request.formData(); const fileId = data.get('fileId'); if (!fileId) { return fail(400, { missing: true }); } try { // Soft delete from database (don't delete from S3 for recovery purposes) await db .update(audioFile) .set({ deletedAt: new Date() }) .where(eq(audioFile.id, fileId)); return { deleted: true }; } catch (error) { console.error('Error deleting audio file:', error); return fail(500, { error: true }); } }, updateDuration: async ({ request }) => { const data = await request.formData(); const fileId = data.get('fileId'); const duration = parseFloat(data.get('duration')); if (!fileId || isNaN(duration)) { return fail(400, { missing: true }); } try { await db.update(audioFile) .set({ duration }) .where(eq(audioFile.id, fileId)); return { success: true }; } catch (error) { console.error('Error updating duration:', error); return fail(500, { error: true }); } }, renameAudioFile: async ({ request }) => { const data = await request.formData(); const audioFileId = data.get('audioFileId'); const newFilename = data.get('newFilename'); if (!audioFileId || !newFilename) { return fail(400, { error: 'Invalid data provided' }); } // Validate filename const filename = newFilename.trim(); if (filename.length === 0) { return fail(400, { error: 'Filename cannot be empty' }); } if (filename.length > 255) { return fail(400, { error: 'Filename is too long' }); } try { await db .update(audioFile) .set({ filename }) .where(eq(audioFile.id, audioFileId)); return { renamed: true, filename }; } catch (error) { console.error('Error renaming audio file:', error); return fail(500, { error: 'Failed to rename audio file' }); } } };