// WHAT ONE VIDEO'S OPERATIONS SAY ABOUT IT, read once, from the registry. // // The per-video page used to know about exactly one derived-data feature: it // re-derived the digest's freshness itself, from a resolver it called with its // own lane argument, and folded the per-section rule a second time beside the // registry's own. Diarization and attribution had no per-video surface at all // — their records existed on disk with nowhere for a human to look at them. // // So this is the reader that makes a fifth operation need no page: it walks // OPERATIONS in registry order, resolves each entry's target ONCE and asks the // entry itself for the video's state. Nothing here re-derives a classification, // and nothing here filters on `enabled` — shownOnVideoPage is that rule, and it // is deliberately separate so a sidecar captured before its feature was // switched off is still something the operator can see. // // THE EXTERNAL OPERATIONS ARE DELIBERATELY ABSENT. Download and transcription // already have a per-video surface — the pipeline stage cards // inside the editor's VideoPanel, which carry their own per-video actions. This // reader is over OPERATIONS, the backfill/derived-data registry, and that // asymmetry is intentional rather than an omission. Sync is absent for a // stronger reason still: it is `scope: "channel"`, so a per-video reader has // nothing to ask it. // // ONE readVideoFiles PER PAGE. It is a readdir, and the registry's state() // takes the listing as its probe rather than re-reading it, so the whole page // costs one listing plus whatever sidecar reads the individual entries decide // are worth paying for. import path from "node:path"; import type { Paths } from "../lib/paths"; import type { SiteSettings } from "../lib/settings"; import { readVideoFiles } from "../lib/videoStatus"; import { OPERATIONS, OPERATION_GROUP_ORDER, operationLabel, type OperationClassification, type OperationGroup, type OperationSettingsBlock, } from "../lib/operations"; export type VideoOperationView = { id: string; label: string; group: OperationGroup; settingsBlock?: OperationSettingsBlock; // Operation.state(probe), NEVER re-derived. The whole point of the reader. state: OperationClassification; // op.enabled(settings) — the feature's own gate, kept beside the state // rather than folded into it: a switched-off operation still has a state. enabled: boolean; // op.outputs against the listing this page already read. outputs: { name: string; present: boolean }[]; // The kind's own words for what a `deferred` video is waiting for. A plural // fragment completing "N videos are …" — kept VERBATIM rather than given a // singular twin, because one copy of a sentence cannot drift from itself and // operations.test.ts pins the digest one. deferredHint?: string; dependsOn: { id: string; label: string }[]; // The resolved target, erased exactly as Operation.resolveTarget erases it. // The one consumer that narrows it is the digest body (it needs // {target, sections} for its per-section rows). Never crosses to a client. target: unknown; }; export async function inspectVideoOperations(opts: { paths: Paths; channelSlug: string; videoId: string; settings: SiteSettings; }): Promise { const { paths, channelSlug, videoId, settings } = opts; const videoDir = path.join(paths.channelsDir, channelSlug, "data", videoId); // checkUntranscribable, because several entries classify an untranscribable // video as not-applicable and would otherwise report it as work. const files = await readVideoFiles(videoDir, { checkUntranscribable: true }); const views: VideoOperationView[] = []; for (const op of OPERATIONS) { const target = await op.resolveTarget({ settings, paths, channelSlug }); const state = await op.state({ videoDir, videoId, files, target, settings, }); views.push({ id: op.id, label: op.label, group: op.group, ...(op.settingsBlock ? { settingsBlock: op.settingsBlock } : {}), state, enabled: op.enabled(settings), outputs: op.outputs.map((name) => ({ name, present: files.entries.includes(name), })), ...(op.deferredHint ? { deferredHint: op.deferredHint } : {}), dependsOn: (op.dependsOn ?? []).map((id) => ({ id, label: operationLabel(id), })), target, }); } return views; } // Shown when the feature is on OR its output is on disk: a sidecar captured // before the feature was switched off is still a thing the operator needs to // see, and hiding it is how a record that cost audio nobody has any more // becomes invisible. export function shownOnVideoPage(view: VideoOperationView): boolean { return view.enabled || view.outputs.some((o) => o.present); } // Group order first (OPERATION_GROUP_ORDER — the order the channel stages and // the /channels columns already use, so a reader moving between pages sees one // sequence), registry order within a group. Pure, and a stable sort: the // registry's own order is what decides the three speaker operations. export function orderForVideoPage( views: VideoOperationView[], ): VideoOperationView[] { const groupRank = (g: OperationGroup): number => { const i = OPERATION_GROUP_ORDER.indexOf(g); return i === -1 ? OPERATION_GROUP_ORDER.length : i; }; return views .map((view, index) => ({ view, index })) .sort( (a, b) => groupRank(a.view.group) - groupRank(b.view.group) || a.index - b.index, ) .map((e) => e.view); }