Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

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:
Mcommon/controller/checkAvailability.ts | 7++++---
Acommon/ytdlp/channelArgs.test.ts | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/ytdlp/channelArgs.ts | 39++++++++++++++++++++++++++++++++++++---
Mcommon/ytdlp/metadataScan.ts | 5++---
Mcommon/ytdlp/runYtdlp.ts | 12+++++++-----
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`);