// THE METADATA SCAN AS A JOB — one definition, two dispatchers. // // The scan itself is `ytdlp/metadataScan.ts`, which is pure work: it takes a // channel and a config and reads titles. What it is NOT is a job — the kind, // the queue key, the replay spec and the platform cooldown that surrounds it // all lived in the editor's server action, which is the one place the auto // runner cannot reach. // // So the JOB moves here and the editor action keeps what is genuinely its // own: the cooldown pre-check it answers to a click with (a returned // `{ ok: false, info: true }` sentence, not a queued job nobody watches) and // its three `revalidatePath` calls. The runner passes neither. What both get // from this module is identical: the same `kind`, the same per-platform queue // key, the same replay spec, and the same `onPlatformBackoff` wiring into the // SHARED cooldown the download lane already honours — which is what stops the // runner and a manual sync from arguing about whether YouTube is angry. // // `needsMedia` comes from the kind (jobs/jobKinds.ts declares it TRUE for this // one, because the scan's target set is "listed, minus what is already on // disk"), so `runManagedFunction` refuses the job against an unmounted drive // for either caller with nothing said here. import { detectPlatform } from "../lib/platform"; import { downloadQueueKey, resolveQueueKey } from "../lib/queueKeys"; import { recordDownloadBackoff } from "../jobs/downloadBackoff"; import { runManagedFunction, type StreamActionResult, } from "../jobs/streamCommand"; import type { ChannelConfig } from "../lib/channelConfig"; import type { Paths } from "../lib/paths"; import { runMetadataScan } from "../ytdlp/metadataScan"; export const METADATA_SCAN_JOB_KIND = "metadata-scan"; export type MetadataScanJobOpts = { paths: Paths; slug: string; channelConfig: ChannelConfig; // Per-run override of the platform queue key (the editor threads one through // from the pipeline action's own queue plumbing). queueKey?: string; // Run in the background so a clicked action preempts it in the queue. The // runner passes true for the same reason its download units do. background?: boolean; // Called after a successful scan, inside the job. The editor revalidates its // three paths here; the runner asks for a snapshot regen instead. afterRun?: () => void | Promise; }; export async function runMetadataScanJob( opts: MetadataScanJobOpts, ): Promise { const { paths, slug, channelConfig } = opts; const platform = detectPlatform(channelConfig.url) ?? "unknown"; return runManagedFunction({ kind: METADATA_SCAN_JOB_KIND, queueKey: resolveQueueKey(downloadQueueKey(channelConfig), opts.queueKey), paths, channelSlug: slug, ...(opts.background ? { background: true } : {}), spec: { kind: METADATA_SCAN_JOB_KIND, slug, params: { queueKey: opts.queueKey }, }, fn: async (onLog, signal, setProgress) => { await runMetadataScan({ paths, channelSlug: slug, channelConfig, onLog, signal, setProgress, onPlatformBackoff: () => recordDownloadBackoff(platform, paths), }); await opts.afterRun?.(); }, }); }