import { NextResponse } from "next/server"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import { readChannelConfig } from "yt-dlp-transcript-common/controller/channels"; import { bulkRelocateChannelMediaAction } from "../../../channels/bulkStorageActions"; import { previewRelocationAction } from "../../../channels/[slug]/storageActions"; import { OpsInputError, ops, optBool, optString, queueResponse, reqSlugs, } from "../_lib"; export const dynamic = "force-dynamic"; // POST { slugs: string[], locationId?: string, root?: string, dryRun?: boolean } // -> { ok: true, queued, skipped } // // THE DESTINATION IS A LOCATION ID WHEREVER POSSIBLE — the root is resolved on // the server from settings.storage.locations, so a caller holding a stale root // cannot aim a batch somewhere a re-point has moved. `root` is the one-off // escape hatch the panel also offers. A skip is not a failure: every slug that // did not queue comes back with the same sentence the bulk bar shows. // // `dryRun: true` (release 19, A4) is the Storage panel's preview per channel // (previewRelocationAction) and starts nothing: { ok, dryRun, previews: // [{slug, preview} | {slug, error}] } — the bytes to copy and both sides' // free space. As on the panel, a preview of a CLASSIC channel (its big files // still real files in data/) first tiers it in place — same-disk renames into // its own media/, the step every move's preflight takes (release 17's ruling); // no byte leaves the corpus disk. export async function POST(request: Request) { return ops(request, ["slugs", "locationId", "root", "dryRun"], async (body) => { const slugs = reqSlugs(body, "slugs"); const locationId = optString(body, "locationId"); const root = optString(body, "root"); const dryRun = optBool(body, "dryRun") === true; if ((locationId ? 1 : 0) + (root ? 1 : 0) !== 1) { throw new OpsInputError( 'send exactly one of "locationId" (a location configured on /storage) or "root" (an absolute path)', ); } const dest = locationId ? ({ kind: "location", locationId } as const) : ({ kind: "custom", root: root as string } as const); if (dryRun) { const previews = []; for (const slug of slugs) { // The bulk move's own skip for a slug with no channel: the panel's // preview is only ever asked about a channel that exists, and a // zero-byte preview of a typo is the silent success this layer refuses. if (!(await readChannelConfig(getPaths(), slug))) { previews.push({ slug, error: "channel not found" }); continue; } const res = await previewRelocationAction(slug, dest); previews.push(res.ok ? { slug, preview: res.preview } : { slug, error: res.error }); } return NextResponse.json({ ok: true, dryRun: true, previews }); } return queueResponse(await bulkRelocateChannelMediaAction(slugs, dest)); }); }