import { NextRequest, NextResponse } from "next/server"; import { Privacy } from "@prisma/client"; import { requirePaidApiUser } from "@/lib/api-auth"; import { createVideoJob, saveUploadedFile } from "@/lib/jobs/create-job"; import { applyCreatePlaylistToItems, parseCreatePlaylistInput, } from "@/lib/jobs/resolve-playlist"; import { filenameWithoutExtension } from "@/lib/constants"; import { mapWithConcurrency } from "@/lib/fs-utils"; import { getPlanLimits } from "@/lib/plans"; import { checkQuota } from "@/lib/quota"; import type { CreateJobPayload, CreatePlaylistRequest, ItemMetadata } from "@/lib/types"; export const maxDuration = 300; type BatchItemInput = Partial & { filename?: string; title?: string; }; function defaultMetadata(title: string): ItemMetadata { return { title, description: "", tags: "", privacy: "PUBLIC", categoryId: "10", resolution: "1920x1080", notifySubscribers: true, madeForKids: false, embeddable: true, creativeCommons: false, includeWatermark: false, playlistId: null, }; } export async function POST(req: NextRequest) { try { const { error, user } = await requirePaidApiUser(req); if (error || !user) return error!; const formData = await req.formData(); const image = formData.get("image") as File | null; const audioFiles = formData.getAll("audio").filter((f): f is File => f instanceof File); const metadataRaw = formData.get("metadata"); if (!image || audioFiles.length === 0) { return NextResponse.json( { error: "Provide multipart fields: image (file), audio (one or more files)" }, { status: 400 }, ); } const limits = getPlanLimits(user.plan); if (audioFiles.length > limits.maxBatchSize) { return NextResponse.json( { error: `Batch limit exceeded. Your plan allows up to ${limits.maxBatchSize} files per batch.` }, { status: 400 }, ); } const quotaCheck = await checkQuota(user.id, audioFiles.length); if (!quotaCheck.ok) { return NextResponse.json({ error: quotaCheck.error }, { status: 403 }); } let itemMeta: BatchItemInput[] = []; let defaults: Partial = {}; let createPlaylist: CreatePlaylistRequest | null = null; if (metadataRaw) { try { const parsed = JSON.parse(String(metadataRaw)) as { items?: BatchItemInput[]; defaults?: Partial; createPlaylist?: unknown; }; itemMeta = parsed.items ?? []; defaults = parsed.defaults ?? {}; createPlaylist = parseCreatePlaylistInput(parsed.createPlaylist); if (parsed.createPlaylist && !createPlaylist) { return NextResponse.json( { error: "createPlaylist requires a title. Optional: description, privacy (public|unlisted|private)", }, { status: 400 }, ); } } catch { return NextResponse.json({ error: "metadata must be valid JSON" }, { status: 400 }); } } const sessionKey = Date.now().toString(); const imageUpload = await saveUploadedFile(user.id, image, "image", user.plan, { sessionKey }); const uploads = await mapWithConcurrency(audioFiles, 4, (audio) => saveUploadedFile(user.id, audio, "audio", user.plan, { sessionKey }), ); const builtItems: CreateJobPayload["items"] = uploads.map((upload, i) => { const audio = audioFiles[i]; const metaInput = itemMeta[i] ?? itemMeta.find((m) => m.filename === audio.name) ?? {}; const base = defaultMetadata( metaInput.title?.trim() || filenameWithoutExtension(audio.name), ); const metadata: ItemMetadata = { ...base, ...defaults, ...metaInput, title: (metaInput.title ?? defaults.title ?? base.title).trim(), privacy: (metaInput.privacy ?? defaults.privacy ?? base.privacy) as Privacy, }; return { audioPath: upload.path, audioFilename: upload.filename, metadata, }; }); const { items, playlist } = await applyCreatePlaylistToItems(user, builtItems, createPlaylist); const job = await createVideoJob(user, { imagePath: imageUpload.path, items, }); return NextResponse.json({ jobId: job.id, itemCount: job.items.length, status: job.status, playlist: playlist ? { id: playlist.id, title: playlist.title, privacy: playlist.privacy } : null, }); } catch (err) { console.error("Batch upload failed:", err); const message = err instanceof Error ? err.message : "Batch upload failed"; const status = message.includes("Quota exceeded") || message.includes("Batch limit exceeded") ? 403 : 500; return NextResponse.json({ error: message }, { status }); } }