Initial commit
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { assertLocalRequest } from "@/lib/api/localOnly";
|
||||
import { cancelJob } from "@/lib/whisperjav/queue";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
|
||||
const blocked = assertLocalRequest(req);
|
||||
if (blocked) return blocked;
|
||||
|
||||
const { id } = await ctx.params;
|
||||
const ok = cancelJob(id);
|
||||
if (!ok) return NextResponse.json({ error: "Not found or not cancellable" }, { status: 404 });
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import fs from "node:fs/promises";
|
||||
import { assertLocalRequest } from "@/lib/api/localOnly";
|
||||
import { getJob, estimateRealtimeMultiplier } from "@/lib/whisperjav/db";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const LOG_TAIL_LINES = 50;
|
||||
|
||||
export async function GET(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
|
||||
const blocked = assertLocalRequest(req);
|
||||
if (blocked) return blocked;
|
||||
|
||||
const { id } = await ctx.params;
|
||||
const job = getJob(id);
|
||||
if (!job) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
|
||||
let logTail: string[] = [];
|
||||
try {
|
||||
const raw = await fs.readFile(job.logPath, "utf8");
|
||||
const lines = raw.split(/\r?\n/);
|
||||
logTail = lines.slice(-LOG_TAIL_LINES - 1).filter(Boolean);
|
||||
} catch { /* log may not exist yet */ }
|
||||
|
||||
// ETA: per-mode multiplier from history × video duration − elapsed.
|
||||
// Returns null when we can't compute (no duration / not running yet).
|
||||
let etaSec: number | null = null;
|
||||
if (
|
||||
(job.status === "queued" || job.status === "running") &&
|
||||
job.videoDurationSec && job.videoDurationSec > 0 &&
|
||||
job.mode
|
||||
) {
|
||||
const multiplier = estimateRealtimeMultiplier(job.mode);
|
||||
const totalProjected = job.videoDurationSec * multiplier;
|
||||
const start = job.startedAt ?? job.enqueuedAt;
|
||||
const elapsedSec = (Date.now() - start) / 1000;
|
||||
etaSec = Math.max(0, totalProjected - elapsedSec);
|
||||
}
|
||||
|
||||
return NextResponse.json({ ...job, logTail, etaSec });
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { assertLocalRequest } from "@/lib/api/localOnly";
|
||||
import { enqueueJob, cancelAllQueued } from "@/lib/whisperjav/queue";
|
||||
import { rawDb } from "@/lib/db/client";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/** Enqueue WhisperJAV for many codes at once. Each code becomes a
|
||||
* separate row in whisperjav_jobs; the single-worker loop processes
|
||||
* them sequentially. Codes that already have a generated subtitle
|
||||
* are skipped (alreadyExists), not failed. */
|
||||
export async function POST(req: NextRequest) {
|
||||
const blocked = assertLocalRequest(req);
|
||||
if (blocked) return blocked;
|
||||
|
||||
const body = await req.json().catch(() => ({}));
|
||||
const rawCodes = Array.isArray(body.codes) ? body.codes : [];
|
||||
const codes = rawCodes
|
||||
.filter((c: unknown): c is string => typeof c === "string" && c.trim().length > 0)
|
||||
.map((c: string) => c.trim());
|
||||
if (codes.length === 0) {
|
||||
return NextResponse.json({ enqueued: 0, skipped: 0, errors: [] });
|
||||
}
|
||||
|
||||
let enqueued = 0;
|
||||
let skipped = 0;
|
||||
const errors: Array<{ code: string; error: string }> = [];
|
||||
|
||||
for (const code of codes) {
|
||||
try {
|
||||
// Always part 0 for batch — multi-part videos are uncommon and
|
||||
// the user can hit individual codes via the player picker for
|
||||
// those edge cases.
|
||||
const result = await enqueueJob({ code, partIdx: 0, overwrite: false });
|
||||
if ("alreadyExists" in result) skipped++;
|
||||
else enqueued++;
|
||||
} catch (e) {
|
||||
errors.push({ code, error: (e as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ enqueued, skipped, errors });
|
||||
}
|
||||
|
||||
/** Cancel every queued (not-yet-running) job. Useful when the user
|
||||
* wants to stop a batch mid-flight. The currently-running job is
|
||||
* left alone — kill it via the per-job cancel endpoint. */
|
||||
export async function DELETE(req: NextRequest) {
|
||||
const blocked = assertLocalRequest(req);
|
||||
if (blocked) return blocked;
|
||||
const cancelled = cancelAllQueued();
|
||||
return NextResponse.json({ cancelled });
|
||||
}
|
||||
|
||||
/** Lightweight queue-state probe used by the batch UI: how many jobs
|
||||
* are queued/running right now, plus the active row's id. */
|
||||
export async function GET(req: NextRequest) {
|
||||
const blocked = assertLocalRequest(req);
|
||||
if (blocked) return blocked;
|
||||
const queued = (rawDb
|
||||
.prepare(`SELECT COUNT(*) AS n FROM whisperjav_jobs WHERE status = 'queued'`)
|
||||
.get() as { n: number }).n;
|
||||
const running = rawDb
|
||||
.prepare(`SELECT id, code, started_at, stage, stage_index, stage_total FROM whisperjav_jobs WHERE status = 'running' ORDER BY started_at DESC LIMIT 1`)
|
||||
.get() as { id: string; code: string; started_at: number | null; stage: string | null; stage_index: number | null; stage_total: number | null } | undefined;
|
||||
return NextResponse.json({ queued, running: running ?? null });
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { assertLocalRequest } from "@/lib/api/localOnly";
|
||||
import { enqueueJob, clearAllJobHistory, runRetentionSweep } from "@/lib/whisperjav/queue";
|
||||
import { listJobsForCode } from "@/lib/whisperjav/db";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const blocked = assertLocalRequest(req);
|
||||
if (blocked) return blocked;
|
||||
|
||||
const body = await req.json().catch(() => ({}));
|
||||
const code = typeof body.code === "string" ? body.code.trim() : "";
|
||||
const rawPartIdx = typeof body.partIdx === "number" && Number.isFinite(body.partIdx) ? body.partIdx : 0;
|
||||
const partIdx = Math.max(0, Math.floor(rawPartIdx));
|
||||
const overwrite = body.overwrite === true;
|
||||
if (!code) return NextResponse.json({ error: "Missing code" }, { status: 400 });
|
||||
|
||||
try {
|
||||
const result = await enqueueJob({ code, partIdx, overwrite });
|
||||
if ("alreadyExists" in result) {
|
||||
return NextResponse.json(result, { status: 409 });
|
||||
}
|
||||
return NextResponse.json(result, { status: 202 });
|
||||
} catch (e) {
|
||||
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const blocked = assertLocalRequest(req);
|
||||
if (blocked) return blocked;
|
||||
|
||||
const code = req.nextUrl.searchParams.get("code") ?? "";
|
||||
if (!code) return NextResponse.json({ jobs: [] });
|
||||
const jobs = listJobsForCode(code, 5);
|
||||
return NextResponse.json({ jobs });
|
||||
}
|
||||
|
||||
/** Clear-all-history. Wipes every non-running row + every temp dir. */
|
||||
export async function DELETE(req: NextRequest) {
|
||||
const blocked = assertLocalRequest(req);
|
||||
if (blocked) return blocked;
|
||||
const result = await clearAllJobHistory();
|
||||
return NextResponse.json(result);
|
||||
}
|
||||
|
||||
/** Manual retention sweep trigger. */
|
||||
export async function PATCH(req: NextRequest) {
|
||||
const blocked = assertLocalRequest(req);
|
||||
if (blocked) return blocked;
|
||||
const result = await runRetentionSweep();
|
||||
return NextResponse.json(result);
|
||||
}
|
||||
Reference in New Issue
Block a user