diff --git a/src/routes/admin/audio/+page.server.js b/src/routes/admin/audio/+page.server.js index 18b00be..ff50bbf 100644 --- a/src/routes/admin/audio/+page.server.js +++ b/src/routes/admin/audio/+page.server.js @@ -3,6 +3,46 @@ 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({ @@ -43,16 +83,19 @@ export const actions = { 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 (without blob data) + // Then save metadata to database with calculated duration await db.insert(audioFile).values({ id, filename: file.name, contentType: file.type, s3Key, - duration: null, // Will be updated by client-side after upload + duration, fileSize: file.size, createdAt: new Date() }); diff --git a/src/routes/admin/audio/+page.svelte b/src/routes/admin/audio/+page.svelte index 5246abc..6f3b772 100644 --- a/src/routes/admin/audio/+page.svelte +++ b/src/routes/admin/audio/+page.svelte @@ -91,9 +91,6 @@ if (response.ok && !responseText.includes('missing') && !responseText.includes('error')) { uploadProgress[index].status = 'completed'; uploadProgress[index].progress = 100; - - // Extract duration from uploaded file - extractAndSaveDuration(file); } else { uploadProgress[index].status = 'error'; uploadProgress[index].error = 'Upload failed'; @@ -140,48 +137,6 @@ uploadProgress = newProgress; } - async function extractAndSaveDuration(file) { - try { - const audio = new Audio(); - const url = URL.createObjectURL(file); - - audio.src = url; - - await new Promise((resolve, reject) => { - audio.addEventListener('loadedmetadata', () => { - URL.revokeObjectURL(url); - resolve(); - }); - audio.addEventListener('error', reject); - }); - - if (audio.duration && !isNaN(audio.duration)) { - // Find the file ID from the most recent upload response - // For now, we'll need to match by filename - this is a limitation - const matchingFile = data.audioFiles.find(af => af.filename === file.name); - if (matchingFile) { - await updateFileDuration(matchingFile.id, audio.duration); - } - } - } catch (error) { - console.error('Error extracting duration:', error); - } - } - - async function updateFileDuration(fileId, duration) { - try { - const formData = new FormData(); - formData.append('fileId', fileId); - formData.append('duration', duration.toString()); - - await fetch('?/updateDuration', { - method: 'POST', - body: formData - }); - } catch (error) { - console.error('Error updating duration:', error); - } - } function startEdit(audioFile) { editingAudioId = audioFile.id; @@ -456,7 +411,7 @@ {formatFileSize(audio.fileSize || 0)}
View and analyze participant ratings data with timeseries visualization.
+View participant ratings data with timeseries visualization.