// IS ANYTHING WRITING INTO THIS CHANNEL'S MEDIA RIGHT NOW? // // One question, one answer, three callers: the per-channel Storage panel's // actions, the bulk move from /channels, and the channel page that greys the // buttons out with a reason. They each used to ask the job registry and only // the job registry — and the registry is HALF the truth. // // THE OMNIMIRROR INCIDENT (2026-09-13) is the half that was missing. The // auto-queue lanes run their units IN-PROCESS: "the auto-queue runner's // per-video units are IN-PROCESS and make no job record" (operationBatch.ts's // header says so, and that is deliberate — 119,600 job records would evict the // registry). So a digest unit was writing a sidecar into `data/` while the // relocate job's rsync was already past that directory; nothing in the registry // named the channel, the move was allowed, and the copy's verify refused at the // last step with 131 GB on the far side. // // `getAutoRunnerStatus(kind).inFlight[]` is the in-process half: one entry per // reserved unit, each carrying the `channelSlug` it belongs to. Asking every // lane is four map reads — no disk, no await — so this is cheap enough to call // on a render. // // SERVER-SIDE ONLY. It reaches into the controller (which imports execa, // transitively), so nothing reachable from a `"use client"` file may import it; // `next build` is what proves that. import { channelWriters, describeChannelWriter, } from "yt-dlp-transcript-common/controller/channelWriters"; import { channelMediaWriters } from "yt-dlp-transcript-common/controller/relocateChannelMedia"; // A sentence naming what is holding the channel, or null when nothing is. The // caller supplies the verb (`"moving its media"`) so the same reason reads as // an instruction in every panel it appears in. // // THE COUNTS, THEN THE FIRST WRITER BY NAME (release 16 slice RM): "Finish or // cancel 1 running/queued job(s) for this channel before moving its media. // Now: a transcription of v50t5yt is running (Transcribe all, job 01M…)." The // list is common/controller/channelWriters.ts's — the one the move's preview // and its job's first step ask — with queued jobs counted here, because this is // the courtesy before anything is enqueued. A lane's download unit is also a // registry job; it is counted once, as the unit. // // `mediaOnly` (release 17): a MOVE of the media tier is held only by the // channel's media writers — a digest may run during it (`channelMediaWriters`, // the same list the move's preview and its first step ask). A rename or a // delete moves or removes the whole channel and asks every writer. export function channelMediaBusyReason( slug: string, what?: string, opts: { mediaOnly?: boolean } = {}, ): string | null { const writers = opts.mediaOnly ? channelMediaWriters(slug, { includeQueued: true }) : channelWriters(slug, { includeQueued: true }); if (writers.length === 0) return null; const jobs = writers.filter((w) => w.source === "job").length; const units = writers.length - jobs; const parts: string[] = []; if (jobs > 0) parts.push(`${jobs} running/queued job(s)`); if (units > 0) parts.push(`${units} auto-queue unit(s) in flight`); const subject = `${parts.join(" and ")} for this channel`; const now = `Now: ${describeChannelWriter(writers[0])}.`; return what ? `Finish or cancel ${subject} before ${what}. ${now}` : `${subject}. ${now}`; }