import { test } from "node:test"; import assert from "node:assert/strict"; import { channelExtraArgs, channelPaceSeconds, configForVideoUrl, pacedPlatformArgs, platformArgs, platformArgsForUrl, platformImportMinGapSeconds, PLATFORM_IMPORT_MIN_GAP_SECONDS, PLATFORM_ARGS, PLATFORM_MIN_GAP_SECONDS, platformMinGapSeconds, staticSleepRequestsSeconds, withSleepRequests, } 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 { 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 the request pace and nothing else (release 9, B3)", () => { assert.deepEqual( channelExtraArgs(cfg({ url: "https://www.youtube.com/@x" })), ["--sleep-requests", "1"], ); assert.deepEqual(platformArgs("youtube"), ["--sleep-requests", "1"]); assert.deepEqual(platformArgs(null), []); }); test("a platform with no entry gets no platform args", () => { assert.deepEqual(platformArgs("twitch"), []); assert.deepEqual( channelExtraArgs(cfg({ url: "https://example.com/feed" })), [], ); }); test("a youtube channel's own --sleep-requests comes after, and wins", () => { const args = channelExtraArgs( cfg({ url: "https://www.youtube.com/@x", ytdlpExtraArgs: ["--sleep-requests", "3"], }), ); assert.deepEqual(args, ["--sleep-requests", "1", "--sleep-requests", "3"]); assert.equal(args.lastIndexOf("--sleep-requests"), args.length - 2); }); 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); }); // ── release 17, slice RL: the adaptive pace ───────────────────────────────── test("the pace raises --sleep-requests in place, and a channel's own still wins", () => { assert.deepEqual( channelExtraArgs(cfg({ url: "https://www.youtube.com/@x" }), undefined, 4), ["--sleep-requests", "4"], ); assert.deepEqual( channelExtraArgs(cfg({ url: "https://rumble.com/c/x" }), undefined, 8), ["--impersonate", "chrome", "--sleep-requests", "8"], ); const own = channelExtraArgs( cfg({ url: "https://www.youtube.com/@x", ytdlpExtraArgs: ["--sleep-requests", "3"] }), undefined, 8, ); assert.deepEqual(own, ["--sleep-requests", "8", "--sleep-requests", "3"]); }); test("a platform with no static pace gets one appended; a pace never lowers one", () => { assert.deepEqual(withSleepRequests([], 2), ["--sleep-requests", "2"]); assert.deepEqual(withSleepRequests(["--sleep-requests", "1"], 0.5), ["--sleep-requests", "1"]); assert.deepEqual(withSleepRequests(["--sleep-requests", "1"], undefined), ["--sleep-requests", "1"]); assert.deepEqual(pacedPlatformArgs("odysee", 2), ["--sleep-requests", "2"]); assert.equal(staticSleepRequestsSeconds("youtube"), 1); assert.equal(staticSleepRequestsSeconds("odysee"), 0); assert.equal(staticSleepRequestsSeconds(null), 0); }); test("with no pace recorded, the static args are unchanged", () => { globalThis.__yttAutoQueueState__ = undefined; assert.equal(channelPaceSeconds(cfg({ url: "https://www.youtube.com/@x" })), 1); assert.deepEqual( channelExtraArgs(cfg({ url: "https://www.youtube.com/@x" })), ["--sleep-requests", "1"], ); }); test("the live pace (the shared state) reaches every channelExtraArgs call", () => { globalThis.__yttAutoQueueState__ = { state: null, stateFile: null, loading: null, loadingFile: null, lastPace: { youtube: { sleepRequestsSeconds: 4, baseSeconds: 1, cleanUnits: 0, steppedAt: Date.now() } }, }; try { assert.deepEqual( channelExtraArgs(cfg({ url: "https://www.youtube.com/@x" })), ["--sleep-requests", "4"], ); assert.equal(channelPaceSeconds(cfg({ url: "https://www.youtube.com/@x" })), 4); // Another platform is untouched. assert.deepEqual(channelExtraArgs(cfg({ url: "https://rumble.com/c/x" })), RUMBLE); } finally { globalThis.__yttAutoQueueState__ = undefined; } }); test("archive.org is paced and backs off, and keeps the fetched page as webpage_url", () => { const args = platformArgs("archiveorg"); assert.equal(staticSleepRequestsSeconds("archiveorg"), 2); assert.deepEqual(args.slice(0, 2), ["--sleep-requests", "2"]); assert.ok(args.includes("http:exp=2:120")); const i = args.indexOf("--parse-metadata"); const [field, re] = [args[i + 1].slice(0, args[i + 1].indexOf(":")), args[i + 1].slice(args[i + 1].indexOf(":") + 1)]; assert.equal(field, "original_url"); // The regex (Python's syntax, which this subset shares) takes an archive.org // page and nothing else. const js = new RegExp(`^${re.replace("(?P", "(?")}$`); assert.equal(js.exec("https://archive.org/details/example-item/a%20b.mp4")?.groups?.webpage_url, "https://archive.org/details/example-item/a%20b.mp4"); assert.equal(js.exec("https://www.youtube.com/watch?v=AbC123xyz_9"), null); // No parallel transfer is ever asked for. assert.equal(args.some((a) => /concurrent|downloader|^-N$/.test(a)), false); }); test("BitChute is paced hardest: 3 s between requests, exponential retry sleeps, a 60 s floor between videos", () => { const args = platformArgs("bitchute"); assert.equal(staticSleepRequestsSeconds("bitchute"), 3); assert.deepEqual(args.slice(0, 2), ["--sleep-requests", "3"]); assert.ok(args.includes("http:exp=2:120")); assert.ok(args.includes("extractor:exp=2:120")); assert.deepEqual(platformArgsForUrl("https://www.bitchute.com/video/Zq3xVb7Kp2Lm/"), args); // No impersonation, and no parallel transfer is ever asked for. assert.equal(args.includes("--impersonate"), false); assert.equal(args.some((a) => /concurrent|downloader|^-N$/.test(a)), false); assert.equal(platformMinGapSeconds("bitchute"), 60); assert.equal(PLATFORM_MIN_GAP_SECONDS.bitchute, 60); // Every other platform keeps the operator's gap. for (const p of ["youtube", "rumble", "odysee", "archiveorg", "unknown", null]) { assert.equal(platformMinGapSeconds(p), 0, String(p)); } }); test("one-off imports have their own floor: Odysee's 60 s, BitChute's batch floor, none elsewhere", () => { assert.equal(PLATFORM_IMPORT_MIN_GAP_SECONDS.odysee, 60); assert.equal(platformImportMinGapSeconds("odysee"), 60); assert.equal(platformImportMinGapSeconds("bitchute"), 60); // Odysee's batch gap is untouched: the import floor is imports only. assert.equal(platformMinGapSeconds("odysee"), 0); for (const p of ["youtube", "rumble", "archiveorg", "unknown", null]) { assert.equal(platformImportMinGapSeconds(p), 0, String(p)); } }); test("a video's own URL picks the platform args when the channel names none or another", () => { const notes = cfg({ handling: "transcribe" } as Partial); assert.deepEqual(channelExtraArgs(notes), []); assert.deepEqual( channelExtraArgs(configForVideoUrl(notes, "https://rumble.com/v7em13s-x.html")), RUMBLE, ); const yt = cfg({ url: "https://www.youtube.com/@x", ytdlpExtraArgs: ["--limit-rate", "1M"] }); assert.deepEqual( channelExtraArgs(configForVideoUrl(yt, "https://rumble.com/v7em13s-x.html")), [...RUMBLE, "--limit-rate", "1M"], "the channel's own args still apply", ); assert.equal(configForVideoUrl(yt, "https://www.youtube.com/watch?v=abc"), yt); assert.equal(configForVideoUrl(yt, "https://example.com/v.mp4"), yt); });