import { NextRequest, NextResponse } from "next/server"; import fs from "fs/promises"; import path from "path"; import { Privacy } from "@prisma/client"; import { FREE_PLAN, isAllowedResolution } from "@/lib/constants"; import { prisma } from "@/lib/db"; import { enqueueVideoJob } from "@/lib/queue/client"; import { checkQuota } from "@/lib/quota"; import { requireAuth } from "@/lib/session"; import { getJobDir } from "@/lib/storage"; import type { CreateJobPayload } from "@/lib/types"; function validateItemMetadata(metadata: CreateJobPayload["items"][0]["metadata"]) { if (!metadata.title?.trim()) return "Each video must have a title"; if (!isAllowedResolution(metadata.resolution)) return "Invalid resolution"; if (!["PUBLIC", "PRIVATE", "UNLISTED"].includes(metadata.privacy)) { return "Invalid privacy setting"; } return null; } export async function POST(req: NextRequest) { const { error, user } = await requireAuth(); if (error || !user) return error!; const body = (await req.json()) as CreateJobPayload; if (!body.imagePath || !body.items?.length) { return NextResponse.json({ error: "Image and at least one audio file required" }, { status: 400 }); } for (const item of body.items) { const metaError = validateItemMetadata(item.metadata); if (metaError) { return NextResponse.json({ error: metaError }, { status: 400 }); } if (user.plan === "FREE" && !item.metadata.includeWatermark) { return NextResponse.json({ error: "Watermark is required on the free plan" }, { status: 400 }); } } const quotaCheck = await checkQuota(user.id, body.items.length); if (!quotaCheck.ok) { return NextResponse.json({ error: quotaCheck.error }, { status: 403 }); } try { await fs.access(body.imagePath); for (const item of body.items) { await fs.access(item.audioPath); const stat = await fs.stat(item.audioPath); if (stat.size > FREE_PLAN.maxFileSizeBytes) { return NextResponse.json({ error: `Audio file ${item.audioFilename} exceeds size limit` }, { status: 400 }); } } const imageStat = await fs.stat(body.imagePath); if (imageStat.size > FREE_PLAN.maxFileSizeBytes) { return NextResponse.json({ error: "Image exceeds size limit" }, { status: 400 }); } } catch { return NextResponse.json({ error: "One or more uploaded files not found" }, { status: 400 }); } const job = await prisma.job.create({ data: { userId: user.id, imagePath: body.imagePath, items: { create: body.items.map((item) => ({ audioPath: item.audioPath, audioFilename: item.audioFilename, title: item.metadata.title.trim(), description: item.metadata.description || "", tags: item.metadata.tags || "", privacy: item.metadata.privacy as Privacy, categoryId: item.metadata.categoryId || "10", resolution: item.metadata.resolution, notifySubscribers: item.metadata.notifySubscribers, madeForKids: item.metadata.madeForKids, embeddable: item.metadata.embeddable, creativeCommons: item.metadata.creativeCommons, includeWatermark: user.plan === "FREE" ? true : item.metadata.includeWatermark, })), }, }, include: { items: true }, }); const jobDir = getJobDir(user.id, job.id); await fs.mkdir(jobDir, { recursive: true }); const imageExt = path.extname(body.imagePath); const newImagePath = path.join(jobDir, `image${imageExt}`); await fs.copyFile(body.imagePath, newImagePath); await prisma.job.update({ where: { id: job.id }, data: { imagePath: newImagePath } }); for (const item of job.items) { const audioExt = path.extname(item.audioPath); const newAudioPath = path.join(jobDir, `${item.id}${audioExt}`); await fs.copyFile(item.audioPath, newAudioPath); await prisma.jobItem.update({ where: { id: item.id }, data: { audioPath: newAudioPath }, }); await enqueueVideoJob({ jobItemId: item.id, userId: user.id, jobId: job.id, }); } return NextResponse.json({ jobId: job.id }); } export async function GET() { const { error, user } = await requireAuth(); if (error || !user) return error!; const jobs = await prisma.job.findMany({ where: { userId: user.id }, orderBy: { createdAt: "desc" }, take: 20, include: { items: { select: { id: true, audioFilename: true, title: true, status: true, youtubeVideoId: true, error: true, }, }, }, }); return NextResponse.json({ jobs }); }