61 lines
1.8 KiB
TypeScript
61 lines
1.8 KiB
TypeScript
// 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 });
|
||
}
|
||
}
|