import { z } from "zod"; import { db } from "@/lib/db"; import { Prisma } from "@prisma/client"; const exportManifestSchema = z.object({ shotCode: z.string().min(1), projectCode: z.string().min(1), aepPath: z.string().min(1), compName: z.string().min(1), rendererType: z.string().default("aerender"), outputDir: z.string().optional(), outputPattern: z.string().optional(), frameStart: z.number().int().positive(), frameEnd: z.number().int().positive(), fps: z.number().positive(), width: z.number().int().positive().optional(), height: z.number().int().positive().optional(), colorspace: z.string().optional(), }); export function validateExportManifest(input: unknown) { return exportManifestSchema.parse(input); } export function buildVersionString(versionNumber: number) { return `v${versionNumber.toString().padStart(3, "0")}`; } export function deriveOutputBaseName(outputPattern: string) { const match = outputPattern.match(/^(.*)\.(?:\[#+\]|\{#+\})\.([A-Za-z0-9]+)$/); return match ? match[1] : outputPattern; } export async function createRenderExport(input: { shotId: string; projectId: string; manifest: unknown; submittedById?: string | null; submittedByName?: string | null; taskId?: string | null; }) { const manifest = validateExportManifest(input.manifest); const shot = await db.shot.findUnique({ where: { id: input.shotId }, select: { id: true, shotVersion: true, exrOutput: true, projectId: true }, }); if (!shot) { throw new Error("Shot not found"); } if (shot.projectId !== input.projectId) { throw new Error("Shot does not belong to the supplied project"); } const versionNumber = parseInt((shot.shotVersion ?? "v001").replace(/^v/i, ""), 10) + 1; const versionString = buildVersionString(versionNumber); const outputPattern = manifest.outputPattern ?? `${manifest.compName}_TT_${versionString}.[####].exr`; const outputDir = manifest.outputDir ?? `${shot.exrOutput ?? manifest.compName}_${versionString}`; const result = await db.$transaction(async (tx) => { const activeExports = await tx.export.findMany({ where: { shotId: input.shotId, status: { in: ["QUEUED", "RENDERING", "VALIDATING", "GENERATING_PREVIEW", "READY_FOR_QC", "READY_FOR_DELIVERY"] }, }, select: { id: true }, }); const exportRecord = await tx.export.create({ data: { shotId: input.shotId, projectId: input.projectId, taskId: input.taskId ?? undefined, versionNumber, versionString, status: "QUEUED", aepPath: manifest.aepPath, compName: manifest.compName, rendererType: manifest.rendererType, outputDir, outputPattern, frameStart: manifest.frameStart, frameEnd: manifest.frameEnd, fps: manifest.fps, width: manifest.width ?? 1920, height: manifest.height ?? 1080, colorspace: manifest.colorspace, submittedById: input.submittedById ?? undefined, submittedByName: input.submittedByName ?? undefined, }, }); await tx.exportEvent.createMany({ data: [ { exportId: exportRecord.id, toStatus: "QUEUED", actorType: "USER", actorId: input.submittedById ?? undefined, note: "Queued from panel", }, ], }); await tx.renderJob.create({ data: { type: "AE_RENDER", exportId: exportRecord.id, attempt: 1, maxAttempts: 3, status: "QUEUED", manifest: manifest as Prisma.JsonObject, priority: 50, }, }); await tx.shot.update({ where: { id: input.shotId }, data: { shotVersion: versionString, exrOutput: deriveOutputBaseName(outputPattern), }, }); if (activeExports.length > 0) { await tx.export.updateMany({ where: { id: { in: activeExports.map((item) => item.id) } }, data: { status: "SUPERSEDED", supersededById: exportRecord.id, statusChangedAt: new Date(), }, }); await tx.exportEvent.createMany({ data: activeExports.map((item) => ({ exportId: item.id, fromStatus: "QUEUED", toStatus: "SUPERSEDED", actorType: "SYSTEM", note: "Superseded by newer export", })), }); } return { exportRecord, versionString }; }); return result; } export async function listExportsForQueue() { return db.export.findMany({ where: { status: { notIn: ["SUPERSEDED", "ARCHIVED", "CANCELLED", "DELIVERED"] } }, orderBy: [{ createdAt: "desc" }], include: { shot: { select: { shotCode: true, shotVersion: true, project: { select: { code: true } } } }, renderJobs: { orderBy: [{ createdAt: "desc" }], take: 1 }, }, }); } export async function getLatestExportForShot(shotId: string) { return db.export.findFirst({ where: { shotId }, orderBy: [{ createdAt: "desc" }], include: { shot: { select: { shotCode: true, project: { select: { code: true } } } }, renderJobs: { orderBy: [{ createdAt: "desc" }], take: 1 }, validations: true, }, }); }