"use server"; import path from "node:path"; import { safeRevalidate } from "../../lib/safeRevalidate"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import { channelQueueKey } from "yt-dlp-transcript-common/lib/queueKeys"; import { readChannelConfig } from "yt-dlp-transcript-common/controller/channels"; import { attachMedia, summaryOf, } from "yt-dlp-transcript-common/controller/attachMedia"; import { runManagedFunction, type StreamActionResult, } from "yt-dlp-transcript-common/jobs/streamCommand"; // LOCAL MEDIA ATTACHED TO A CHANNEL'S HELD VIDEOS (release 21 D1): the // `attach-media` job, as `pnpm ops attach-media` posts it // (`/api/ops/attach-media`). The work and its rules are // common/controller/attachMedia.ts; this is the enqueue and the refusals a // caller gets before any job exists. // // On the channel's own queue: nothing is fetched, so no platform queue is owed // a turn, and a second attach on the same channel waits behind the first // rather than racing it for the same pointers. The kind's `needsMedia` is what // keeps it off a channel whose media is moving or unreachable. export async function attachMediaAction( slug: string, opts: { source: string; items?: { id: string; path: string }[]; match?: string; createRecords?: boolean; replace?: boolean; dryRun?: boolean; }, ): Promise { const paths = getPaths(); const channelConfig = await readChannelConfig(paths, slug); if (!channelConfig) { return { ok: false, error: `Channel "${slug}" not found` }; } const source = opts.source.trim(); if (!path.isAbsolute(source)) { return { ok: false, error: `"source" must be an absolute path (a directory, a .zip or a .7z)` }; } if (opts.items !== undefined && opts.match !== undefined) { return { ok: false, error: 'Send "items" or "match", not both: items name the files exactly' }; } if (opts.match !== undefined) { try { new RegExp(opts.match, "i"); } catch (e) { return { ok: false, error: `"match" is not a valid regex: ${(e as Error).message}` }; } } return runManagedFunction({ kind: "attach-media", queueKey: channelQueueKey(slug), paths, channelSlug: slug, fn: async (onLog, signal, _setProgress, ctx) => { const result = await attachMedia({ paths, slug, channelConfig, source, ...(opts.items ? { items: opts.items } : {}), ...(opts.match !== undefined ? { match: opts.match } : {}), ...(opts.createRecords ? { createRecords: true } : {}), ...(opts.replace ? { replace: true } : {}), ...(opts.dryRun ? { dryRun: true } : {}), onLog, signal, drainSignal: ctx.drainSignal, }); if (!opts.dryRun) { safeRevalidate([ `/channels/${slug}`, "/channels", ...result.attached.map((id) => `/channels/${slug}/videos/${id}`), ]); } if (result.failed.length > 0 && result.attached.length === 0) { throw new Error( `${result.failed.length} failed, none attached: ${JSON.stringify(summaryOf(result).failed)}`, ); } if (result.stopped && result.stopped !== "drained" && result.stopped !== "cancelled") { throw new Error(`stopped: ${result.stopped}`); } }, }); }