import { getPaths, type Paths } from "yt-dlp-transcript-common/lib/paths"; import { getSettings } from "yt-dlp-transcript-common/lib/settings"; import { listSites, siteChannelIndex } from "yt-dlp-transcript-common/lib/site"; import { listChannelConfigs } from "yt-dlp-transcript-common/controller/channels"; import { type SiteChannelIndex, isDefaultChannelPriority, resolveFocusSlugs, } from "yt-dlp-transcript-common/lib/channelPriority"; import type { PriorityView } from "yt-dlp-transcript-common/views/channelPriority"; // THE STATUS PAYLOAD'S HALF OF CHANNEL PRIORITY — the tree the lane actually // dispatches from, and the focus banner's numbers. // // WHY THIS EXISTS AT ALL. The payload used to ship `getSettings().autoQueue[kind]` // verbatim, which is the STORED policy. While a priority model exists the runner // does not dispatch from the stored tree — it compiles one from the model per // tick (S1, `laneDispatchRoot` in controller/autoRunner.ts) — so a ladder drawn // from the stored root would name leaves the runner does not have and attribute // zero pending to every one of them. The console's whole contract is that its // rungs ARE the dispatch order, so it has to compile the same tree. // // COMPILED BY THE RUNNER'S OWN FUNCTION, not by a second one. S4 shipped a // `laneRootFor` here and a copy of the default-model gate beside it, flagged in // this header as the one fold S5 owed the file; S5 exported `laneDispatchRoot` // and `isDefaultChannelPriority` from controller/autoRunner.ts and deleted both // copies. There is now ONE definition of "which tree does this lane dispatch // from", and the console asks the runner for it. // // THE ABSENT DOCUMENT COSTS NOTHING. No focus and no channel entry means the // compiler never runs, the stored tree is shipped byte for byte, and neither // `listChannelConfigs` nor `listSites` is read — which is what keeps a corpus // with no priorities set on exactly today's payload and today's cost. // The pure half — the `PriorityView` type and the two folds the payload applies // to it — is `common/views/channelPriority.ts`, which the status view imports // directly. What is left here is the READ that resolves one, which is the half // that cannot move: it lists channels and, for a site focus, the sites // directory. // "a, b and c" for a short channel focus; "a, b and 4 more" past three, because // this lands mid-sentence in a banner and a 30-slug list is not a name. function channelFocusName(slugs: readonly string[]): string { if (slugs.length <= 3) { if (slugs.length <= 1) return slugs[0] ?? ""; return `${slugs.slice(0, -1).join(", ")} and ${slugs[slugs.length - 1]}`; } return `${slugs.slice(0, 2).join(", ")} and ${slugs.length - 2} more`; } export async function readPriorityView( paths: Paths = getPaths(), ): Promise { const model = getSettings().channelPriority; if (isDefaultChannelPriority(model)) { return { model, compiled: false, slugs: [], focusSlugs: [], name: null }; } // NOT CACHED, DELIBERATELY. The runner holds its channel list on a 30 s TTL // because it ticks every three seconds forever; this runs on the status // poll, which is what an e2e spec (and an operator) reads immediately after // creating, renaming or deleting a channel. A TTL here would make the // console lag the corpus by up to half a minute for a listing that costs // nothing at all while the model is default — the early return above never // reaches it. const slugs = (await listChannelConfigs(paths)).map((row) => row.slug); // ONLY A SITE FOCUS READS THE SITES DIRECTORY, as in the runner: the other // two kinds resolve from the document alone. `siteChannelIndex` is the one // spelling of that read (lib/site.ts); the site TITLE is this surface's // extra, because the banner names the focus and the model stores an id. let index: SiteChannelIndex = {}; let name: string | null = null; if (model.focus.kind === "site") { index = siteChannelIndex(paths); for (const site of listSites(paths)) { if (site.siteId === model.focus.siteId) name = site.siteTitle; } name = name ?? model.focus.siteId; } const focusSlugs = resolveFocusSlugs(model, index, slugs); if (model.focus.kind === "channels") name = channelFocusName(focusSlugs); return { model, compiled: true, slugs, focusSlugs, name }; }