// Walk every channel/video and ensure each transcript.live_chat.json has a // sibling live_chat.cues.json. Mirrors normalizeAllTranscripts; cheap on // rebuilds because normalizeLiveChat short-circuits when the sidecar is // already at least as new as the metadata + raw payload. import path from "node:path"; import { readdir } from "node:fs/promises"; import pLimit from "p-limit"; import { listChannelStatsFromDisk } from "./channels"; import { normalizeLiveChat } from "./normalizeLiveChat"; import type { Paths } from "../lib/paths"; import { assertChannelMediaReachable } from "../lib/channelMedia"; export type NormalizeAllLiveChatOptions = { paths: Paths; onLog?: (msg: string) => void; signal?: AbortSignal; concurrency?: number; }; export type NormalizeAllLiveChatResult = { wrote: number; fresh: number; skipped: number; failed: number; }; export async function normalizeAllLiveChat( opts: NormalizeAllLiveChatOptions, ): Promise { const log = opts.onLog ?? ((m: string) => console.log(m)); const limit = pLimit(opts.concurrency ?? 8); const channels = await listChannelStatsFromDisk(opts.paths); const result: NormalizeAllLiveChatResult = { wrote: 0, fresh: 0, skipped: 0, failed: 0, }; for (const ch of channels) { if (opts.signal?.aborted) break; // THE MEDIA GUARD (release 17): the raw replay is media — on a tiered // channel a link into channels//media, possibly on another drive — // and a stale cues file is re-derived by READING it. A channel whose media // is not reachable (unmounted, stalled, moving, legacy) is skipped and // counted, never read as a clean pass over zero videos. try { await assertChannelMediaReachable(opts.paths, ch.slug, ch.config); } catch (err) { log(`Normalize live chat ${ch.slug}: SKIPPED — ${(err as Error).message}`); result.failed++; continue; } const dataDir = path.join(opts.paths.channelsDir, ch.slug, "data"); const videoIds = await readdir(dataDir).catch(() => [] as string[]); log(`Normalize live chat ${ch.slug}: ${videoIds.length} videos`); let wrote = 0; let fresh = 0; let skipped = 0; let failed = 0; await Promise.all( videoIds.map((id) => limit(async () => { if (opts.signal?.aborted) return; try { const outcome = await normalizeLiveChat({ videoDir: path.join(dataDir, id), channelSlug: ch.slug, configName: ch.config.name, }); if (outcome.status === "wrote") wrote++; else if (outcome.status === "fresh") fresh++; else skipped++; } catch (err) { failed++; log(` ! ${ch.slug}/${id}: ${(err as Error).message}`); } }), ), ); log( ` ${ch.slug}: wrote=${wrote} fresh=${fresh} skipped=${skipped} failed=${failed}`, ); result.wrote += wrote; result.fresh += fresh; result.skipped += skipped; result.failed += failed; } log( `Done. wrote=${result.wrote} fresh=${result.fresh} skipped=${result.skipped} failed=${result.failed}`, ); return result; }