import path from "node:path"; import { stat } from "node:fs/promises"; import { NextResponse } from "next/server"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import { authorizeWorkerRequest } from "yt-dlp-transcript-common/lib/workerToken"; import { getRegistry } from "yt-dlp-transcript-common/jobs/registry"; import { readJobMeta } from "yt-dlp-transcript-common/jobs/jobMeta"; import { getJobEntry, readLogChunk } from "yt-dlp-transcript-common/jobs/listJobs"; import { clipWindowPath } from "yt-dlp-transcript-common/lib/clipWindow-server"; import { loadSavedVideo } from "yt-dlp-transcript-common/lib/savedVideo-server"; import { savedVideoPath } from "yt-dlp-transcript-common/lib/savedVideo"; export const dynamic = "force-dynamic"; // POLL ONE FETCH. The POST that started it returned a jobId; this says whether // it finished and, when it did, what file came out. // // The file is recomputed from the job's own SPEC, never echoed back from the // caller: the spec is what the editor itself queued, so a poll cannot be talked // into naming a path the fetch never wrote. const ERROR_TAIL_BYTES = 2000; export async function GET( request: Request, { params }: { params: Promise<{ jobId: string }> }, ) { const auth = authorizeWorkerRequest(request.headers.get("authorization")); if (!auth.ok) { return NextResponse.json({ error: auth.error }, { status: auth.status }); } const { jobId } = await params; if (!/^[A-Za-z0-9_-]+$/.test(jobId)) { return NextResponse.json({ error: "Invalid job id" }, { status: 400 }); } const paths = getPaths(); // The live record first (it has the spec and the true status); then the // sidecar, which survives eviction and a restart and also carries the spec; // then the log-only fallback, which can still answer "what happened" for a // job whose sidecar is gone. const live = getRegistry().get(jobId); const meta = live ? null : await readJobMeta(paths, jobId); const entry = live || meta ? null : await getJobEntry(paths, jobId); if (!live && !meta && !entry) { return NextResponse.json({ error: "no such job" }, { status: 404 }); } const kind = live?.kind ?? meta?.kind ?? entry?.kind; // A fetch-windows batch answers with its status and, on failure, its log's // tail: it names no one file, so `file` stays absent. if (kind !== "fetch-window" && kind !== "redownload-archive" && kind !== "fetch-windows") { return NextResponse.json( { error: `job ${jobId} is a ${kind ?? "?"} job, not a media fetch` }, { status: 400 }, ); } const status = live?.status ?? meta?.status ?? entry?.status ?? "queued"; const exitCode = live?.exitCode ?? meta?.exitCode ?? entry?.exitCode; const spec = live?.spec ?? meta?.spec ?? null; const out: Record = { status, jobId }; if (exitCode !== undefined) out.exitCode = exitCode; if (status === "done" && kind !== "fetch-windows") { const slug = live?.channelSlug ?? meta?.channelSlug ?? spec?.slug; const videoId = live?.videoId ?? meta?.videoId; const p = spec?.params ?? {}; if (slug && videoId) { const videoDir = path.join(paths.channelsDir, slug, "data", videoId); let file: string | null = null; if (kind === "fetch-window") { const from = Number(p.from); const to = Number(p.to); if (Number.isFinite(from) && Number.isFinite(to)) { file = clipWindowPath(videoDir, from, to); out.from = from; out.to = to; } } else { const pointer = await loadSavedVideo(videoDir); file = pointer ? savedVideoPath(pointer) : null; // How tall the saved file is, as the persist recorded it: a 720p // persist can fall through to its last-resort rung, and the caller // asked with a height in mind. if (pointer?.format?.height !== undefined) { out.height = pointer.format.height; } } if (file) { const st = await stat(file).catch(() => null); if (st?.isFile()) { out.file = file; out.bytes = st.size; } } } } if (status === "failed" || status === "cancelled") { // The tail of the job's own log. A caller that cannot see this server's // /jobs page still gets the yt-dlp line that explains the refusal. const logPath = live?.logPath ?? entry?.logPath ?? path.join(paths.jobsDir, `${jobId}.log`); const { content } = await readLogChunk(logPath, 0); out.error = content.slice(-ERROR_TAIL_BYTES).trim(); } return NextResponse.json(out, { headers: { "cache-control": "no-store" } }); }