import { NextResponse } from "next/server"; import { SOURCE_VIDEO_QUALITIES } from "yt-dlp-transcript-common/ytdlp/downloadFormat"; import { PERSIST_REPLACE_POLICIES } from "yt-dlp-transcript-common/controller/persistVideos"; import { persistVideosAction } from "../../../channels/[slug]/persistActions"; import { oneOf, ops, opsFail, optBool, optNonNegativeInt, optString, reqVideoItems, } from "../_lib"; export const dynamic = "force-dynamic"; // POST { items: [{ slug, id }, …], format?, replace?, gapMs?, minFreeMemMb?, // dryRun?, queueKey? } // dryRun -> { ok: true, dryRun: true, plan } // else -> { ok: true, dryRun: false, plan, jobs: [{ slug, jobId }], jobIds, // jobId (one job only), skipped: [{ slug, reason }] } // // Persist specific videos, across channels, to the saved-video store: the // video page's "Persist source video" over a list. `plan` buckets every item — // saved, wrongHeight, toFetch, noUrl, unknown — with counts and ids, and // `willFetch` is how many downloads a run makes. A real run starts one job per // channel with something to fetch, on that channel's download queue. // // `format` ("original" | "video_720") defaults to each channel's own quality. // `replace: "above-height"` also re-fetches a saved container whose height is // unknown or above that quality's ceiling (default "never"). `gapMs` is the // pause between downloads (default the batch downloads' gap), `minFreeMemMb` // holds each download until that much memory is available (default 0, off). // Re-running the same body is the resume: saved videos are skipped. // // A big list goes in a file: `pnpm ops persist-videos --file list.json`. export async function POST(request: Request) { return ops( request, ["items", "format", "replace", "gapMs", "minFreeMemMb", "dryRun", "queueKey"], async (body) => { const result = await persistVideosAction({ items: reqVideoItems(body, "items"), format: body.format === undefined ? undefined : oneOf(body, "format", SOURCE_VIDEO_QUALITIES), replace: body.replace === undefined ? undefined : oneOf(body, "replace", PERSIST_REPLACE_POLICIES), gapMs: optNonNegativeInt(body, "gapMs"), minFreeMemMb: optNonNegativeInt(body, "minFreeMemMb"), dryRun: optBool(body, "dryRun"), queueKey: optString(body, "queueKey"), }); if (!result.ok) return opsFail(result.error); if (result.dryRun) return NextResponse.json(result); return NextResponse.json({ ...result, ...(result.jobIds.length === 1 ? { jobId: result.jobIds[0] } : {}), }); }, ); }