commit 6d04f7ed3f1917924607080667accf845e954931
parent 9a61334a986324dae398051625c8f92aed5d8122
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Thu, 24 Sep 2026 21:55:48 -0400
common: one yt-dlp arg builder with a platform table — rumble gets --impersonate chrome everywhere
Four builders had drifted (configArgs, channelExtraArgs, the metadata scan's
and the quick availability check's inline copies) and the new-channel probe had
none. channelArgs.ts now holds PLATFORM_ARGS (rumble: --impersonate chrome
--sleep-requests 1, yt-dlp #17496) and every spawn goes through it: cookies,
then platform args, then the channel's own ytdlpExtraArgs so an override still
wins. probeChannelMeta gets platformArgs(detectPlatform(url)), so a Rumble
channel can be created again.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
5 files changed, 112 insertions(+), 14 deletions(-)
diff --git a/common/controller/checkAvailability.ts b/common/controller/checkAvailability.ts
@@ -25,6 +25,7 @@ import { getSettings } from "../lib/settings";
// digest registry's claude-code lane, which has the same problem.
import { parseStdoutJson } from "../lib/parseStdoutJson";
import { readChannelConfig } from "./channels";
+import { channelExtraArgs } from "../ytdlp/channelArgs";
import { resolveShardItems } from "./shard";
import type { Paths } from "../lib/paths";
@@ -117,7 +118,6 @@ export async function runAvailabilityCheck({
const channelDir = path.join(paths.channelsDir, channelSlug);
const dataDir = path.join(channelDir, "data");
const config = await readChannelConfig(paths, channelSlug);
- const extraArgs = config?.ytdlpExtraArgs ?? [];
// Probe cookies in "always" mode ONLY. when-required/defer probes stay
// cookie-free deliberately, so auth gating keeps being OBSERVED as
// needs_auth — defer mode's exclusion + Needs-cookies bucket depend on that
@@ -214,8 +214,9 @@ export async function runAvailabilityCheck({
"--dump-json",
"--skip-download",
"--no-warnings",
- ...cookieArgs(probeCookies),
- ...extraArgs,
+ ...(config
+ ? channelExtraArgs(config, probeCookies)
+ : cookieArgs(probeCookies)),
"--",
url,
];
diff --git a/common/ytdlp/channelArgs.test.ts b/common/ytdlp/channelArgs.test.ts
@@ -0,0 +1,63 @@
+import { test } from "node:test";
+import assert from "node:assert/strict";
+import { channelExtraArgs, platformArgs, PLATFORM_ARGS } from "./channelArgs";
+import type { ChannelConfig } from "../lib/channelConfig";
+
+// Run with:
+// pnpm --filter yt-dlp-transcript-common exec tsx --test ytdlp/channelArgs.test.ts
+
+const RUMBLE = ["--impersonate", "chrome", "--sleep-requests", "1"];
+
+function cfg(over: Partial<ChannelConfig>): ChannelConfig {
+ return { handling: "youtube", ...over } as ChannelConfig;
+}
+
+test("a rumble URL gets the four platform args", () => {
+ assert.deepEqual(
+ channelExtraArgs(cfg({ url: "https://rumble.com/c/TheQuartering" })),
+ RUMBLE,
+ );
+});
+
+test("youtube gets no platform args", () => {
+ assert.deepEqual(
+ channelExtraArgs(cfg({ url: "https://www.youtube.com/@x" })),
+ [],
+ );
+ assert.deepEqual(platformArgs("youtube"), []);
+ assert.deepEqual(platformArgs(null), []);
+});
+
+test("an explicit config.platform wins over the URL's host", () => {
+ assert.deepEqual(
+ channelExtraArgs(
+ cfg({ url: "https://example.com/feed", platform: "rumble" }),
+ ),
+ RUMBLE,
+ );
+});
+
+test("order: cookies, then platform args, then the channel's own (so an override wins)", () => {
+ const args = channelExtraArgs(
+ cfg({
+ url: "https://rumble.com/c/x",
+ ytdlpExtraArgs: ["--sleep-requests", "3"],
+ }),
+ "firefox",
+ );
+ assert.deepEqual(args, [
+ "--cookies-from-browser",
+ "firefox",
+ ...RUMBLE,
+ "--sleep-requests",
+ "3",
+ ]);
+ // yt-dlp is last-flag-wins: the channel's 3 is the one that applies.
+ assert.equal(args.lastIndexOf("--sleep-requests"), args.length - 2);
+});
+
+test("platformArgs returns a copy, not the table's array", () => {
+ const a = platformArgs("rumble");
+ a.push("--mutated");
+ assert.deepEqual(PLATFORM_ARGS.rumble, RUMBLE);
+});
diff --git a/common/ytdlp/channelArgs.ts b/common/ytdlp/channelArgs.ts
@@ -1,21 +1,54 @@
// The per-channel argv every yt-dlp invocation in this repo appends: the cookie
-// source when the resolved policy calls for one, then the channel's own
-// `ytdlpExtraArgs` verbatim.
+// source when the resolved policy calls for one, then the platform's fixed
+// args (PLATFORM_ARGS), then the channel's own `ytdlpExtraArgs` verbatim.
//
// Lifted out of ytdlp/downloadOneManaged.ts (where it was `channelConfigArgs`,
// which still delegates here) because the clip-window fetch has to honour the
// same two things and must not grow its own idea of them: an operator who set
// `--limit-rate` on a channel meant it for every byte that channel costs, not
// only for the bytes a full download costs.
+//
+// This is the ONE builder. `configArgs` (runYtdlp.ts), the metadata scan, the
+// quick availability check and the new-channel probe all go through it (the
+// probe has no config yet, so it calls `platformArgs` directly). Before
+// release 5 there were four copies and the probe had none, so a Rumble
+// channel could not even be created.
import type { ChannelConfig } from "../lib/channelConfig";
+import { cookieArgs } from "../lib/cookiePolicy";
+import { detectPlatform, type Platform } from "../lib/platform";
+
+// Args a platform needs on EVERY yt-dlp spawn, whatever the job. Code, not a
+// setting: per-channel `ytdlpExtraArgs` is the tweak surface, and it comes
+// AFTER these, so a channel override wins (yt-dlp is last-flag-wins for
+// `--sleep-requests` and `--impersonate`).
+//
+// rumble: every request 403s at Cloudflare without a browser TLS fingerprint
+// (yt-dlp #17496); a probe on 2026-09-24 got 200 with `--impersonate chrome`
+// and 403 without. `--sleep-requests 1` paces the listing walk — the
+// the-quartering-rumble full sweep 429'd at page 155 unpaced the same day.
+export const PLATFORM_ARGS: Partial<Record<Platform, readonly string[]>> = {
+ rumble: ["--impersonate", "chrome", "--sleep-requests", "1"],
+};
+
+export function platformArgs(platform: Platform | null | undefined): string[] {
+ if (!platform) return [];
+ return [...(PLATFORM_ARGS[platform] ?? [])];
+}
+
+export function channelPlatform(
+ config: Pick<ChannelConfig, "platform" | "url">,
+): Platform | null {
+ return config.platform ?? detectPlatform(config.url);
+}
export function channelExtraArgs(
config: ChannelConfig,
cookies?: string,
): string[] {
const args: string[] = [];
- if (cookies) args.push("--cookies-from-browser", cookies);
+ args.push(...cookieArgs(cookies));
+ args.push(...platformArgs(channelPlatform(config)));
if (config.ytdlpExtraArgs?.length) args.push(...config.ytdlpExtraArgs);
return args;
}
diff --git a/common/ytdlp/metadataScan.ts b/common/ytdlp/metadataScan.ts
@@ -28,9 +28,9 @@ import { compileDownloadFilter, titleFilterRejects } from "../lib/downloadFilter
import {
alwaysCookies,
authRetryCookies,
- cookieArgs,
resolveCookiePolicy,
} from "../lib/cookiePolicy";
+import { channelExtraArgs } from "./channelArgs";
import type { Paths } from "../lib/paths";
import { getSettings } from "../lib/settings";
import { extractVideoId } from "../lib/videoId";
@@ -408,8 +408,7 @@ export async function runMetadataScan(
PRINT_TEMPLATE,
"-a",
batchFile,
- ...cookieArgs(cookies),
- ...(channelConfig.ytdlpExtraArgs ?? []),
+ ...channelExtraArgs(channelConfig, cookies),
];
opts.onLog(`$ ${paths.ytdlpBin} ${args.join(" ")}\n`);
diff --git a/common/ytdlp/runYtdlp.ts b/common/ytdlp/runYtdlp.ts
@@ -13,6 +13,7 @@ import { getSettings } from "../lib/settings";
import { patchChannelConfig } from "../controller/channels";
import { diskGate } from "../lib/diskSpace";
import { detectPlatform } from "../lib/platform";
+import { channelExtraArgs, platformArgs } from "./channelArgs";
import { isRealAudioFile } from "../lib/videoStatus";
import { readVttProvenance } from "../lib/subtitleProvenance";
import { extractVideoId } from "../lib/videoId";
@@ -24,7 +25,6 @@ import {
} from "../lib/availability";
import {
alwaysCookies,
- cookieArgs,
resolveCookiePolicy,
type ResolvedCookiePolicy,
} from "../lib/cookiePolicy";
@@ -219,11 +219,10 @@ function abortableSleep(ms: number, signal: AbortSignal): Promise<void> {
});
}
+// Cookies, then the platform's fixed args, then the channel's own — the one
+// builder in channelArgs.ts.
function configArgs(config: ChannelConfig, cookies?: string): string[] {
- const args: string[] = [];
- args.push(...cookieArgs(cookies));
- if (config.ytdlpExtraArgs?.length) args.push(...config.ytdlpExtraArgs);
- return args;
+ return channelExtraArgs(config, cookies);
}
// The run-level cookie policy: settings + channel overrides, with the
@@ -418,6 +417,9 @@ export async function probeChannelMeta(opts: {
"1",
"--print",
"%(channel,uploader,playlist_title,playlist,uploader_id,title)s",
+ // No channel config exists yet; the platform's fixed args still apply
+ // (a Rumble probe 403s without `--impersonate`).
+ ...platformArgs(detectPlatform(url)),
url,
];
log(`$ ${paths.ytdlpBin} ${args.join(" ")}\n`);