// The server half of the video page's operation panels: the registry reader, // plus the ONE record each panel's body needs, projected down to what crosses // to the client. // // SERVER-ONLY, no directive — it is imported by the page, which is a server // component. It reaches the sidecar loaders (node:fs) and it strips `target`, // the erased `unknown` the registry narrows per entry, before anything is // handed to a client component. // // ONE loadAttribution FOR BOTH ATTRIBUTION PANELS: the two operations share // attribution.json (lib/operations.ts's two-kinds-one-file rule), so reading it // twice would be two reads of one file that must not be allowed to disagree. import path from "node:path"; import type { Paths } from "yt-dlp-transcript-common/lib/paths"; import type { SiteSettings } from "yt-dlp-transcript-common/lib/settings"; import { inspectVideoOperations, orderForVideoPage, shownOnVideoPage, type VideoOperationView, } from "yt-dlp-transcript-common/controller/videoOperations"; import { loadDigest, loadDigestOverrides, } from "yt-dlp-transcript-common/lib/digest-server"; import { digestSectionStates, effectiveDigest, type DigestFreshnessTarget, type DigestSectionKind, } from "yt-dlp-transcript-common/lib/digest"; import { loadDiarization } from "yt-dlp-transcript-common/lib/diarization-server"; import { loadAttribution } from "yt-dlp-transcript-common/lib/attribution-server"; import type { AttributionRecord } from "yt-dlp-transcript-common/lib/attribution"; import type { OperationPanelBody } from "../components/OperationPanel"; import type { OperationPanelView } from "../components/operationState"; export type VideoOperationPanel = { view: OperationPanelView; body: OperationPanelBody; }; export async function loadVideoOperationPanels(opts: { paths: Paths; channelSlug: string; videoId: string; settings: SiteSettings; }): Promise { const views = orderForVideoPage( (await inspectVideoOperations(opts)).filter(shownOnVideoPage), ); const videoDir = path.join( opts.paths.channelsDir, opts.channelSlug, "data", opts.videoId, ); // Read once, lazily, and shared by both attribution panels. let attribution: AttributionRecord | null | undefined; const readAttribution = async (): Promise => { if (attribution === undefined) attribution = await loadAttribution(videoDir); return attribution; }; const panels: VideoOperationPanel[] = []; for (const view of views) { panels.push({ view: toPanelView(view), body: await loadBody(view, videoDir, opts.settings, readAttribution), }); } return panels; } // Drops `target`. Everything else on VideoOperationView is already plain data. function toPanelView(view: VideoOperationView): OperationPanelView { const { target: _target, ...rest } = view; return rest; } async function loadBody( view: VideoOperationView, videoDir: string, settings: SiteSettings, // Memoized PER CALL of loadVideoOperationPanels, never at module scope: a // module-level cache would outlive the request and hand a re-render the // record from before the run that just rewrote it. readAttribution: () => Promise, ): Promise { switch (view.settingsBlock) { case "digest": { const [record, overrides] = await Promise.all([ loadDigest(videoDir), loadDigestOverrides(videoDir), ]); // The digest is the one body that narrows the erased target: its // per-section rows need the identity a regeneration would produce and // the section list the settings ask for. Same shape the registry entry's // own resolveTarget returns. const { target, sections } = view.target as { target: DigestFreshnessTarget; sections: DigestSectionKind[]; }; return { block: "digest", data: { digest: effectiveDigest(record, overrides), hasMachineDigest: record !== null, // THE SAME FOLD state() counts. The page used to compute its own. sections: digestSectionStates(record, sections, target), target, hasOverrides: overrides !== null, note: overrides?.note ?? "", remoteEnabled: settings.digest.remoteEnabled, }, }; } case "diarization": { const record = await loadDiarization(videoDir); return { block: "diarization", data: { record: record ? { generatedAt: record.generatedAt, ...(record.audioSeconds !== undefined ? { audioSeconds: record.audioSeconds } : {}), ...(record.durationMs !== undefined ? { durationMs: record.durationMs } : {}), speakers: record.speakers, // A count: a long recording has thousands of turns and none of // them is a row on this page. turns: record.turns.length, engine: record.engine, ...(record.windowing ? { windowing: record.windowing } : {}), } : null, }, }; } case "attribution": { const record = await readAttribution(); return { block: "attribution", data: { record: record ? { generatedAt: record.generatedAt, speakers: record.speakers, segments: record.segments.length, warnings: record.warnings?.length ?? 0, provenance: record.provenance, } : null, }, }; } // NEVER REACHED, and the switch has to say so anyway. `views` comes from // videoOperations.ts, which walks the per-video registry; sync is // `scope: "channel"` and has no per-video state at all, so its block can // never arrive here. The arm exists because the union is closed and // exhaustive — which is the property that makes a NEW per-video block a // compile error rather than a blank panel. case "syncScheduler": case undefined: return { block: undefined }; } }