diff --git a/app/api/sounds/route.ts b/app/api/sounds/route.ts index f6cb921..9910f75 100644 --- a/app/api/sounds/route.ts +++ b/app/api/sounds/route.ts @@ -6,9 +6,13 @@ import { writeFile, mkdir, unlink } from "fs/promises"; import path from "path"; import { v4 as uuidv4 } from "uuid"; import { eq } from "drizzle-orm"; -import { convertOggToMp3 } from "@/lib/convert"; +import { convertToCompressedMp3 } from "@/lib/convert"; + +export const maxDuration = 120; const UPLOAD_DIR = path.join(process.cwd(), "public", "uploads", "sounds"); +const ALLOWED_EXTENSIONS = /\.(mp3|ogg|wav|flac|aac|m4a|wma|opus|webm|mp4|avi|mov|mkv|webm|wmv|flv)$/i; +const MAX_SIZE = 50 * 1024 * 1024; // 50 MB export async function GET() { const session = await getServerSession(); @@ -43,37 +47,36 @@ export async function POST(req: NextRequest) { const results: { url: string; originalName: string; duration: number }[] = []; for (const file of files) { - if (!file.name.toLowerCase().endsWith(".ogg")) continue; - if (file.size > 2 * 1024 * 1024) continue; + if (!ALLOWED_EXTENSIONS.test(file.name)) continue; + if (file.size > MAX_SIZE) continue; const baseName = uuidv4(); - const oggPath = path.join(UPLOAD_DIR, `${baseName}.ogg`); + const inputPath = path.join(UPLOAD_DIR, `${baseName}.in`); const mp3Path = path.join(UPLOAD_DIR, `${baseName}.mp3`); const bytes = await file.arrayBuffer(); - await writeFile(oggPath, Buffer.from(bytes)); - - const duration = Math.round((file.size / (64 * 128)) * 10) / 10; + await writeFile(inputPath, Buffer.from(bytes)); let storedFilename: string; - if (convertOggToMp3(oggPath, mp3Path)) { - await unlink(oggPath).catch(() => {}); + if (convertToCompressedMp3(inputPath, mp3Path)) { + await unlink(inputPath).catch(() => {}); storedFilename = `${baseName}.mp3`; } else { - storedFilename = `${baseName}.ogg`; + await unlink(inputPath).catch(() => {}); + continue; } const id = uuidv4(); db.insert(sounds).values({ id, - originalName: file.name.replace(/\.ogg$/i, ".mp3"), + originalName: file.name, storedFilename, - duration: Math.min(duration, 6), + duration: 6, uploadedBy: session.id, createdAt: new Date().toISOString(), }).run(); - results.push({ url: `/uploads/sounds/${storedFilename}`, originalName: file.name.replace(/\.ogg$/i, ".mp3"), duration: Math.min(duration, 6) }); + results.push({ url: `/uploads/sounds/${storedFilename}`, originalName: file.name, duration: 6 }); } return NextResponse.json({ sounds: results }); diff --git a/app/api/upload/sound/route.ts b/app/api/upload/sound/route.ts index f512a8d..a992925 100644 --- a/app/api/upload/sound/route.ts +++ b/app/api/upload/sound/route.ts @@ -5,10 +5,13 @@ import { sounds } from "@/lib/db/schema"; import { writeFile, mkdir, unlink } from "fs/promises"; import path from "path"; import { v4 as uuidv4 } from "uuid"; -import { convertOggToMp3 } from "@/lib/convert"; -import fs from "fs"; +import { convertToCompressedMp3 } from "@/lib/convert"; + +export const maxDuration = 60; const UPLOAD_DIR = path.join(process.cwd(), "public", "uploads", "sounds"); +const ALLOWED_EXTENSIONS = /\.(mp3|ogg|wav|flac|aac|m4a|wma|opus|webm|mp4|avi|mov|mkv|webm|wmv|flv)$/i; +const MAX_SIZE = 50 * 1024 * 1024; // 50 MB export async function POST(req: NextRequest) { const session = await getServerSession(); @@ -21,47 +24,43 @@ export async function POST(req: NextRequest) { const file = formData.get("file") as File | null; if (!file) return NextResponse.json({ error: "No file" }, { status: 400 }); - if (!file.name.toLowerCase().endsWith(".ogg")) { - return NextResponse.json({ error: "Only OGG files allowed" }, { status: 400 }); + if (!ALLOWED_EXTENSIONS.test(file.name)) { + return NextResponse.json({ error: "Unsupported file format" }, { status: 400 }); } - if (file.size > 2 * 1024 * 1024) { - return NextResponse.json({ error: "File too large (max 2MB)" }, { status: 400 }); + if (file.size > MAX_SIZE) { + return NextResponse.json({ error: "File too large (max 50MB)" }, { status: 400 }); } await mkdir(UPLOAD_DIR, { recursive: true }); const baseName = uuidv4(); - const oggPath = path.join(UPLOAD_DIR, `${baseName}.ogg`); + const inputPath = path.join(UPLOAD_DIR, `${baseName}.in`); const mp3Path = path.join(UPLOAD_DIR, `${baseName}.mp3`); - // save temporary OGG const bytes = await file.arrayBuffer(); - await writeFile(oggPath, Buffer.from(bytes)); + await writeFile(inputPath, Buffer.from(bytes)); - // duration estimate - const duration = Math.round((file.size / (64 * 128)) * 10) / 10; - - // convert to MP3 let storedFilename: string; - if (convertOggToMp3(oggPath, mp3Path)) { - await unlink(oggPath).catch(() => {}); + if (convertToCompressedMp3(inputPath, mp3Path)) { + await unlink(inputPath).catch(() => {}); storedFilename = `${baseName}.mp3`; - } else { - storedFilename = `${baseName}.ogg`; + + const id = uuidv4(); + db.insert(sounds).values({ + id, + originalName: file.name, + storedFilename, + duration: 6, + uploadedBy: session.id, + createdAt: new Date().toISOString(), + }).run(); + + return NextResponse.json({ url: `/uploads/sounds/${storedFilename}` }); } - const id = uuidv4(); - db.insert(sounds).values({ - id, - originalName: file.name.replace(/\.ogg$/i, ".mp3"), - storedFilename, - duration: Math.min(duration, 6), - uploadedBy: session.id, - createdAt: new Date().toISOString(), - }).run(); - - return NextResponse.json({ url: `/uploads/sounds/${storedFilename}` }); + await unlink(inputPath).catch(() => {}); + return NextResponse.json({ error: "Conversion failed" }, { status: 500 }); } catch { return NextResponse.json({ error: "Upload failed" }, { status: 500 }); } diff --git a/components/ItemEditor.tsx b/components/ItemEditor.tsx index c39033c..888f88f 100644 --- a/components/ItemEditor.tsx +++ b/components/ItemEditor.tsx @@ -305,7 +305,7 @@ export function ItemEditor({ campaign }: { campaign: Campaign }) { { const f = e.target.files?.[0]; if (f) uploadSound(f); }} /> diff --git a/components/ResourceBrowser.tsx b/components/ResourceBrowser.tsx index f0aa095..41504b0 100644 --- a/components/ResourceBrowser.tsx +++ b/components/ResourceBrowser.tsx @@ -79,7 +79,7 @@ function SoundsTab() { } catch {} setUploading(false); }, - accept: { "audio/ogg": [".ogg"] }, + accept: { "audio/*": [".mp3", ".ogg", ".wav", ".flac", ".aac", ".m4a", ".wma", ".opus", ".webm"], "video/*": [".mp4", ".avi", ".mov", ".mkv", ".webm", ".wmv", ".flv"] }, multiple: true, }); @@ -133,7 +133,7 @@ function SoundsTab() {
-

Drop OGG files here or click to upload

+

Drop audio/video files here or click to upload

{/* Library */} diff --git a/lib/convert.ts b/lib/convert.ts index 48a0e2c..a34b869 100644 --- a/lib/convert.ts +++ b/lib/convert.ts @@ -1,23 +1,18 @@ import { execSync } from "child_process"; -import fs from "fs"; -export function convertOggToMp3(inputPath: string, outputPath: string): boolean { +/** + * Convert any audio/video file to a 6-second compressed MP3 via ffmpeg. + * Returns true on success, false on failure. + * The output file is always mp3Path (overwritten if exists). + */ +export function convertToCompressedMp3(inputPath: string, mp3Path: string): boolean { try { execSync( - `ffmpeg -i "${inputPath}" -codec:a libmp3lame -b:a 128k -y "${outputPath}" 2>/dev/null`, - { timeout: 15000 } + `ffmpeg -i "${inputPath}" -t 6 -vn -codec:a libmp3lame -b:a 64k -y "${mp3Path}" 2>/dev/null`, + { timeout: 30000 } ); return true; } catch { return false; } } - -export function convertOggToMp3OrCopy(inputPath: string, mp3Path: string): string { - if (convertOggToMp3(inputPath, mp3Path)) { - return mp3Path; - } - // fallback: keep original OGG - fs.copyFileSync(inputPath, mp3Path.replace(/\.mp3$/, ".ogg")); - return mp3Path.replace(/\.mp3$/, ".ogg"); -}