commit dae7febb3f553f307ead7a3f42e656d99f236ee2
parent 3879dac23914fa1b5628effe60ec921e891da555
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Fri, 25 Sep 2026 19:24:13 -0400
common: only a filter-declined video skips the between-downloads sleep (review fix)
Review MED: a per_video failure that never got past the prefetch skipped
the sleep, but YouTube's soft block ("This content isn't available, try
again later") classifies as deleted -> per_video, so a soft-blocked run
would fire prefetches back to back into the block. declinedWithoutMediaFetch
now covers skipped-filtered only; classifyDownloadFailure is unchanged.
managedDownloadsSleep.test.ts: the per-video-at-prefetch case now sleeps.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
2 files changed, 30 insertions(+), 35 deletions(-)
diff --git a/common/ytdlp/managedDownloadsSleep.test.ts b/common/ytdlp/managedDownloadsSleep.test.ts
@@ -114,8 +114,12 @@ test("a video the download filter declined does not sleep", async () => {
assert.equal(await sleepsFor([FILTERED, FILTERED, FILTERED]), 0);
});
-test("a per-video failure that never reached a media request does not sleep", async () => {
- assert.equal(await sleepsFor([MEMBERS_ONLY, FETCHED]), 0);
+// Release 9 review: a per-video failure KEEPS the pace, even one that never
+// got past the prefetch. YouTube's soft block ("This content isn't available,
+// try again later") classifies as deleted → per_video; skipping the sleep there
+// would fire prefetches back to back into the block.
+test("a per-video failure at the prefetch still sleeps", async () => {
+ assert.equal(await sleepsFor([MEMBERS_ONLY, FETCHED]), 1);
});
test("the last video never sleeps, whatever it was", async () => {
@@ -145,24 +149,17 @@ test("declinedWithoutMediaFetch: a chat-only pass is a fetch", () => {
assert.equal(
declinedWithoutMediaFetch(
outcome("skipped-filtered", [PREFETCH, chatPass]),
- undefined,
),
false,
);
assert.equal(
- declinedWithoutMediaFetch(
- outcome("chat-only", [PREFETCH, chatPass]),
- undefined,
- ),
+ declinedWithoutMediaFetch(outcome("chat-only", [PREFETCH, chatPass])),
false,
);
- assert.equal(declinedWithoutMediaFetch(FILTERED, undefined), true);
- assert.equal(declinedWithoutMediaFetch(MEMBERS_ONLY, "per_video"), true);
+ assert.equal(declinedWithoutMediaFetch(FILTERED), true);
+ assert.equal(declinedWithoutMediaFetch(MEMBERS_ONLY), false);
assert.equal(
- declinedWithoutMediaFetch(
- outcome("failed", [PREFETCH], "rate_limit"),
- "rate_limit",
- ),
+ declinedWithoutMediaFetch(outcome("failed", [PREFETCH], "rate_limit")),
false,
);
});
diff --git a/common/ytdlp/runYtdlp.ts b/common/ytdlp/runYtdlp.ts
@@ -22,7 +22,6 @@ import {
EXCLUDED_FROM_DOWNLOAD,
classifyDownloadFailure,
type Availability,
- type DownloadFailureClass,
} from "../lib/availability";
import type { DownloadOutcomeRecord } from "../lib/downloadOutcome";
import {
@@ -895,19 +894,21 @@ export type ManagedDownloadsDeps = {
sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
};
-// True when the video ended without a single media request: every attempt it
-// made was the n: 0 metadata prefetch (or its cookie retry), and it ended
-// either declined by an app-level filter or failed for a reason that belongs
-// to the video (members-only, private, removed). Attempts n >= 1 are the real
-// fetches — the download attempts and the chat-only pass — and any of them
-// means the source was asked for media.
+// True when the download filter declined the video before any media request:
+// every attempt it made was the n: 0 metadata prefetch (or its cookie retry)
+// and it ended `skipped-filtered`. Attempts n >= 1 are the real fetches — the
+// download attempts and the chat-only pass (a failed chat pass leaves the
+// status skipped-filtered, and still asked).
+//
+// A per_video FAILURE is deliberately NOT here, even one that never got past
+// the prefetch (release 9 review): YouTube's soft block ("This content isn't
+// available, try again later") classifies as `deleted` → per_video, so
+// skipping the sleep there would fire prefetches back to back into the block.
export function declinedWithoutMediaFetch(
outcome: DownloadOutcomeRecord,
- failureClass: DownloadFailureClass | undefined,
): boolean {
if (outcome.attempts.some((a) => a.n >= 1)) return false;
- if (outcome.status === "skipped-filtered") return true;
- return outcome.status === "failed" && failureClass === "per_video";
+ return outcome.status === "skipped-filtered";
}
type ManagedRunResult = {
@@ -1050,14 +1051,12 @@ export async function runManagedDownloads(
// kept on disk, but not a usable download. Count as skipped (not ok,
// not a retryable failure) so the batch summary stays honest.
skippedCount++;
- }
- let failureClass: DownloadFailureClass | undefined;
- if (outcome.status === "failed") {
+ } else if (outcome.status === "failed") {
failedCount++;
// downloadOneManaged classifies against the full stderr tail; fall
// back to the truncated attempt error only if it's somehow absent.
const lastAttempt = outcome.attempts[outcome.attempts.length - 1];
- failureClass =
+ const failureClass =
outcome.failureClass ??
classifyDownloadFailure(
lastAttempt?.error ?? "",
@@ -1086,19 +1085,18 @@ export async function runManagedDownloads(
processedCount++;
const isLast = processedCount >= urls.length;
const willAbortLoop = firstFailure !== null && abortOnError;
- // THE SLEEP PACES MEDIA FETCHES, so a video that fetched none does not
- // pay for one (release 9, B2). On 2026-09-25 paramount-tactical's
- // download-missing slept 193 times for 14 archives: every video the
- // download filter declined slept 30 s after nothing but its metadata
- // prefetch. A real fetch — success or failure, or a chat-only pass —
- // still sleeps, and so does every failure that is not per-video (a
- // rate limit above all).
+ // A VIDEO THE FILTER DECLINED DOES NOT PAY FOR A FETCH IT NEVER MADE
+ // (release 9, B2). On 2026-09-25 paramount-tactical's download-missing
+ // slept 193 times for 14 archives: every video the download filter
+ // declined slept 30 s after nothing but its metadata prefetch. Every
+ // other outcome still sleeps — a real fetch, success or failure, and
+ // every failure, per-video ones included (see declinedWithoutMediaFetch).
if (
sleepSeconds > 0 &&
!isLast &&
!willAbortLoop &&
!opts.signal.aborted &&
- !declinedWithoutMediaFetch(outcome, failureClass)
+ !declinedWithoutMediaFetch(outcome)
) {
opts.onLog(`Sleeping ${sleepSeconds}s before next download...\n`);
await sleep(sleepSeconds * 1000, opts.signal);