import { attachMediaAction } from "../../../channels/[slug]/attachMediaActions"; import { OpsInputError, jobResponse, ops, optBool, optString, reqSlug, reqString, reqVideoId, type OpsBody, } from "../_lib"; export const dynamic = "force-dynamic"; // POST { slug, source, items?: [{id, path}], match?, createRecords?, replace?, dryRun? } // -> { ok: true, jobId } // // Local media attached to a channel's held videos (release 21 D1): each // video's file copied out of `source` — an absolute path to a directory, a // .zip (read in place) or a .7z — into the saved-video store, as one // `attach-media` job on the channel's queue (controller/attachMedia.ts). The // id comes from the folder's trailing "()", else the file's yt-dlp // suffix; `items` names exact files instead (each `path` is the entry's path // inside the source), `match` narrows the folders by a case-insensitive regex. // `createRecords` writes a record for a video the channel does not hold; // `replace` re-attaches over an existing saved container. `dryRun` logs what // each folder is (attach, not held, already attached, [LOST], no media, // unmatched, ambiguous) and the held videos the archive has no media for, and // writes nothing. The job's log ends with a `summary: {…}` line. function optItems(body: OpsBody): { id: string; path: string }[] | undefined { const v = body.items; if (v === undefined) return undefined; if (!Array.isArray(v) || v.length === 0) { throw new OpsInputError('"items" must be a non-empty array of { "id", "path" }'); } return v.map((entry, i) => { if (typeof entry !== "object" || entry === null || Array.isArray(entry)) { throw new OpsInputError(`"items[${i}]" must be an object { "id", "path" }`); } const extra = Object.keys(entry).filter((k) => k !== "id" && k !== "path"); if (extra.length) { throw new OpsInputError( `"items[${i}]" has unknown key(s): ${extra.join(", ")} — an item is { "id", "path" }`, ); } return { id: reqVideoId(entry as OpsBody, "id"), path: reqString(entry as OpsBody, "path"), }; }); } export async function POST(request: Request) { return ops( request, ["slug", "source", "items", "match", "createRecords", "replace", "dryRun"], async (body) => jobResponse( await attachMediaAction(reqSlug(body, "slug"), { source: reqString(body, "source"), items: optItems(body), match: optString(body, "match"), createRecords: optBool(body, "createRecords"), replace: optBool(body, "replace"), dryRun: optBool(body, "dryRun"), }), ), ); }