200 lines
4.6 KiB
JavaScript
200 lines
4.6 KiB
JavaScript
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' });
|
|
}
|
|
}
|
|
};
|