// app/api/transcoder/claim/route.ts // Atomically claims the next available transcoding job. // Uses SELECT … FOR UPDATE SKIP LOCKED so multiple workers never race on the // same video. import { NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; import { Prisma } from "@prisma/client"; import { verifyTranscoderToken } from "@/lib/transcoder-auth"; export const dynamic = "force-dynamic"; export async function POST(request: Request) { if (!verifyTranscoderToken(request)) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } try { const video = await prisma.$transaction(async (tx) => { // Atomically lock the oldest uploaded video, skipping rows already // locked by concurrent workers. const rows = await tx.$queryRaw<{ id: string }[]>( Prisma.sql` SELECT id FROM "Video" WHERE "transcodingStatus" = 'uploaded' ORDER BY "createdAt" ASC LIMIT 1 FOR UPDATE SKIP LOCKED ` ); if (rows.length === 0) return null; return tx.video.update({ where: { id: rows[0].id }, data: { transcodingStatus: "processing" }, }); }); if (!video) { // No work available – return null body so the worker knows to stop. return NextResponse.json(null, { status: 200 }); } // Build the download URL from the public CMS base URL. const cmsBase = process.env.NEXTAUTH_URL?.replace(/\/$/, "") ?? process.env.CMS_PUBLIC_URL?.replace(/\/$/, "") ?? ""; return NextResponse.json({ videoId: video.id, downloadUrl: `${cmsBase}/api/transcoder/download/${video.id}`, }); } catch (err) { console.error("[Transcoder Claim] Error:", err); return NextResponse.json({ error: "Internal server error" }, { status: 500 }); } }