commit f93f22e334e3ac63aeae3e4bd4add19c1dd7910b
parent 5a5201465b7834370cffeb64bf2420dfb6358eb6
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Wed, 7 Oct 2026 20:31:32 -0400
Merge fetch-window-rumble-picky-first (one Rumble page load per window)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
3 files changed, 56 insertions(+), 11 deletions(-)
diff --git a/common/ytdlp/fetchWindowManaged.test.ts b/common/ytdlp/fetchWindowManaged.test.ts
@@ -1,6 +1,7 @@
// fetchWindowManaged against a fake yt-dlp: Rumble's `.tar` HLS segments are
// refused by ffmpeg until the one retry adds -extension_picky 0, and the option
-// is never passed on a first try (against a progressive URL it is an error).
+// is never passed on a first try (against a progressive URL it is an error) —
+// except to a rumble.com URL, which gets it first and loses it on a retry.
//
// Run with: pnpm --filter yt-dlp-transcript-common exec tsx --test ytdlp/fetchWindowManaged.test.ts
@@ -98,3 +99,26 @@ test("any other failure is not retried with the HLS option", async () => {
const mine = (await runs()).slice(before);
assert.equal(mine.length, 1);
});
+
+const atRumble = (videoDir: string) => ({ ...opts(videoDir), videoUrl: "https://rumble.com/v1-x.html" });
+
+test("a rumble.com window sends -extension_picky 0 on the first try: one page load, not two", async () => {
+ process.env.FAKE_MODE = "rumble";
+ const before = (await runs()).length;
+ const res = await fetchWindowManaged(atRumble(await setup("rumble-first")));
+ const mine = (await runs()).slice(before);
+ assert.equal(mine.length, 1);
+ assert.equal(hasPicky(mine[0]), true);
+ assert.equal(await readFile(res.file, "utf8"), "mp4");
+});
+
+test("a rumble.com upload served progressive is retried once without the HLS option", async () => {
+ process.env.FAKE_MODE = "progressive";
+ const before = (await runs()).length;
+ const res = await fetchWindowManaged(atRumble(await setup("rumble-progressive")));
+ const mine = (await runs()).slice(before);
+ assert.equal(mine.length, 2);
+ assert.equal(hasPicky(mine[0]), true);
+ assert.equal(hasPicky(mine[1]), false);
+ assert.equal(await readFile(res.file, "utf8"), "mp4");
+});
diff --git a/common/ytdlp/fetchWindowManaged.ts b/common/ytdlp/fetchWindowManaged.ts
@@ -28,6 +28,7 @@ import {
type ResolvedCookiePolicy,
} from "../lib/cookiePolicy";
import type { ChannelConfig } from "../lib/channelConfig";
+import { detectPlatform } from "../lib/platform";
import type { Paths } from "../lib/paths";
import {
clipsDirFor,
@@ -56,9 +57,13 @@ export const DEFAULT_CLIP_MAX_HEIGHT = 720;
// failed. `-extension_picky 0` lets them through, but it is an option of the HLS
// DEMUXER: against a progressive URL (YouTube's googlevideo mp4) ffmpeg aborts
// with "Option extension_picky not found". So it is a RETRY on exactly that
-// refusal, never a default — umtool's build-video.mjs does the same
-// (umtool/docs/quirks.md).
+// refusal — umtool's build-video.mjs does the same (umtool/docs/quirks.md) —
+// EXCEPT on a rumble.com URL, where it goes on the first try: every attempt
+// loads the Rumble page again, and Cloudflare 403s some share of those loads,
+// so a retry every Rumble window needs doubled the refusals. An old Rumble
+// upload served progressive is the mirror case, retried once without it.
export const HLS_EXTENSION_REFUSED = /allowed_segment_extensions|allowed_extensions/;
+export const HLS_PICKY_REFUSED = /Option extension_picky not found/;
export const HLS_PICKY_RETRY_ARGS = ["--downloader-args", "ffmpeg_i:-extension_picky 0"];
// The clip format selector lives with the download presets (the "video_720"
@@ -253,8 +258,9 @@ export async function fetchWindowManaged(
);
let cookiesUsed = alwaysCookies(policy);
- let args = argsWith(cookiesUsed);
- let outcome = await run(cookiesUsed);
+ let picky = detectPlatform(opts.videoUrl) === "rumble" ? HLS_PICKY_RETRY_ARGS : [];
+ let args = argsWith(cookiesUsed, picky);
+ let outcome = await run(cookiesUsed, picky);
let backedOff = false;
if (outcome.exitCode !== 0) {
@@ -273,19 +279,28 @@ export async function fetchWindowManaged(
`Window fetch failed with ${availability}; retrying once with cookies.\n`,
);
cookiesUsed = retryCookies;
- args = argsWith(retryCookies);
- outcome = await run(retryCookies);
+ args = argsWith(retryCookies, picky);
+ outcome = await run(retryCookies, picky);
}
}
}
- if (outcome.exitCode !== 0 && HLS_EXTENSION_REFUSED.test(outcome.stderrTail)) {
+ if (outcome.exitCode !== 0 && !picky.length && HLS_EXTENSION_REFUSED.test(outcome.stderrTail)) {
opts.onLog(
`Window fetch: ffmpeg refused the HLS segment extension; retrying once with -extension_picky 0.\n`,
);
await rm(part, { force: true });
- args = argsWith(cookiesUsed, HLS_PICKY_RETRY_ARGS);
- outcome = await run(cookiesUsed, HLS_PICKY_RETRY_ARGS);
+ picky = HLS_PICKY_RETRY_ARGS;
+ args = argsWith(cookiesUsed, picky);
+ outcome = await run(cookiesUsed, picky);
+ } else if (outcome.exitCode !== 0 && picky.length && HLS_PICKY_REFUSED.test(outcome.stderrTail)) {
+ opts.onLog(
+ `Window fetch: the source is not HLS after all; retrying once without -extension_picky.\n`,
+ );
+ await rm(part, { force: true });
+ picky = [];
+ args = argsWith(cookiesUsed, picky);
+ outcome = await run(cookiesUsed, picky);
}
if (outcome.exitCode !== 0) {
diff --git a/umtool/docs/quirks.md b/umtool/docs/quirks.md
@@ -26,7 +26,13 @@ and Rumble ships no progressive fallback, so every Rumble clip is unbuildable
without it. But the option lives on the **HLS demuxer**: pass it against a
progressive URL (YouTube's googlevideo mp4) and ffmpeg aborts with "Option
extension_picky not found". Adding it unconditionally trades a Rumble failure for
-a YouTube one.
+a YouTube one. The editor's window fetch keys it on the URL instead: a rumble.com
+URL gets it on the FIRST try (a retry loads the Rumble page again, and Cloudflare
+403s a share of page loads), any other URL only as the retry.
+
+**A Rumble video gets Rumble's args from its URL, not its channel.** A channel
+with no platform of its own (community-notes) holds Rumble videos; fetched with
+that channel's args they went without `--impersonate` and Cloudflare 403'd them.
**A 403 backs the platform off in a batch.** `fetch-via-editor.mjs --all` (the
editor's `fetch-windows` job) treats one 403 as that clip's failure — a removed