Archilyzer · Source

archilyzer

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

commit 639abe30a9563367759168705e9155103eed9121
parent 9cbed498b846120abc958645fef892d22b30f332
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Fri, 25 Sep 2026 11:00:29 -0400

common: per-video deferral helpers + persisted videoDeferrals on the auto-queue state

VideoDeferral {until, channelSlug}, VIDEO_RATE_LIMIT_DEFER_MS = 6 h,
deferVideo / isVideoDeferred / pruneDeferred / coerceVideoDeferrals beside
the platform backoff (nextBackoff untouched). AutoQueueKindState gains
videoDeferrals: a file without the key coerces to {}, an older build ignores
it. platformBackoff.test +5, autoQueueState.test +2.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Mcommon/jobs/autoQueueState.test.ts | 41+++++++++++++++++++++++++++++++++++++++++
Mcommon/jobs/autoQueueState.ts | 16+++++++++++++++-
Mcommon/jobs/platformBackoff.test.ts | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/jobs/platformBackoff.ts | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 182 insertions(+), 1 deletion(-)

diff --git a/common/jobs/autoQueueState.test.ts b/common/jobs/autoQueueState.test.ts @@ -101,6 +101,7 @@ test("every lane is keyed, and a state file written before a lane existed coerce runtime: { currentWeights: {} }, picks: [], platformBackoff: {}, + videoDeferrals: {}, }); assert.deepEqual(back.backfill.picks, []); // And the lanes it does name are untouched. @@ -127,6 +128,46 @@ test("a lane missing from an in-memory state object still writes", async () => { }); }); +test("videoDeferrals round-trips through write/read; other lanes stay {}", async () => { + await withPaths(async (paths) => { + const state = emptyAutoQueueState(); + state.download.videoDeferrals = { + a1: { until: 123456, channelSlug: "alpha" }, + }; + await writeAutoQueueState(paths, state); + + const back = await readAutoQueueState(paths); + assert.deepEqual(back.download.videoDeferrals, { + a1: { until: 123456, channelSlug: "alpha" }, + }); + assert.deepEqual(back.transcription.videoDeferrals, {}); + }); +}); + +test("a state file written before videoDeferrals existed coerces it to {}; corrupt entries drop", async () => { + await withPaths(async (paths) => { + await writeAutoQueueState(paths, emptyAutoQueueState()); + await writeFile( + paths.autoQueueStateFile, + JSON.stringify({ + download: { + runtime: { currentWeights: {} }, + picks: [], + platformBackoff: { youtube: { until: 5, fails: 2 } }, + }, + transcription: { videoDeferrals: { x: { until: "nope" }, y: 7 } }, + }), + ); + + const back = await readAutoQueueState(paths); + assert.deepEqual(back.download.videoDeferrals, {}); + assert.deepEqual(back.download.platformBackoff, { + youtube: { until: 5, fails: 2 }, + }); + assert.deepEqual(back.transcription.videoDeferrals, {}); + }); +}); + // --- write safety ------------------------------------------------------------ test("overlapping writes do not collide on the tmp file", async () => { diff --git a/common/jobs/autoQueueState.ts b/common/jobs/autoQueueState.ts @@ -7,7 +7,9 @@ import { } from "./autoQueuePolicy"; import { type PlatformBackoffState, + type VideoDeferralState, coercePlatformBackoff, + coerceVideoDeferrals, } from "./platformBackoff"; // Persistent fairness state for the auto-queue runners. Unlike in-flight worker @@ -41,6 +43,11 @@ export type AutoQueueKindState = { // the "download" kind; empty for transcription. Persisted so an Odysee 429 // cooldown survives a server restart instead of re-storming on boot. platformBackoff: PlatformBackoffState; + // Per-video deferrals (a rate-limited video is skipped by the auto-download + // pick for VIDEO_RATE_LIMIT_DEFER_MS). Download kind only; `{}` elsewhere. + // Persisted so a restart does not re-hit the same video at fails+1. A file + // written before the key existed coerces to `{}`; an older build ignores it. + videoDeferrals: VideoDeferralState; }; // KEYED BY EVERY LANE, including the two with no executor yet. A lane whose @@ -53,7 +60,12 @@ export type AutoQueueState = Record<AutoQueueKind, AutoQueueKindState>; export const AUTO_QUEUE_PICK_LOG_LIMIT = 50; export function emptyAutoQueueKindState(): AutoQueueKindState { - return { runtime: emptyAutoQueueRuntime(), picks: [], platformBackoff: {} }; + return { + runtime: emptyAutoQueueRuntime(), + picks: [], + platformBackoff: {}, + videoDeferrals: {}, + }; } export function emptyAutoQueueState(): AutoQueueState { @@ -109,6 +121,7 @@ function coerceKindState(value: unknown): AutoQueueKindState { runtime: coerceRuntime(r.runtime), picks, platformBackoff: coercePlatformBackoff(r.platformBackoff), + videoDeferrals: coerceVideoDeferrals(r.videoDeferrals), }; } @@ -148,6 +161,7 @@ export async function writeAutoQueueState( runtime: k.runtime, picks: k.picks.slice(0, AUTO_QUEUE_PICK_LOG_LIMIT), platformBackoff: k.platformBackoff ?? {}, + videoDeferrals: k.videoDeferrals ?? {}, }); const out = Object.fromEntries( LANES.map((lane) => [lane, trim(state[lane] ?? emptyAutoQueueKindState())]), diff --git a/common/jobs/platformBackoff.test.ts b/common/jobs/platformBackoff.test.ts @@ -9,6 +9,12 @@ import { isCoolingDown, nextBackoff, pruneExpired, + VIDEO_RATE_LIMIT_DEFER_MS, + type VideoDeferralState, + coerceVideoDeferrals, + deferVideo, + isVideoDeferred, + pruneDeferred, } from "./platformBackoff"; // Run with: pnpm --filter yt-dlp-transcript-common exec tsx --test common/jobs/platformBackoff.test.ts @@ -91,3 +97,51 @@ test("coercePlatformBackoff tolerates corrupt/missing shapes", () => { { odysee: { until: 5, fails: 2 }, youtube: { until: 7, fails: 1 } }, ); }); + +test("deferVideo defers a video for 6 h by default, keyed by id", () => { + const state: VideoDeferralState = {}; + deferVideo(state, "v1", "alpha", 1_000); + assert.deepEqual(state, { + v1: { until: 1_000 + VIDEO_RATE_LIMIT_DEFER_MS, channelSlug: "alpha" }, + }); + assert.equal(VIDEO_RATE_LIMIT_DEFER_MS, 6 * 60 * 60_000); +}); + +test("deferVideo re-defers from now (a later 429 extends the window)", () => { + const state: VideoDeferralState = {}; + deferVideo(state, "v1", "alpha", 1_000, 10); + deferVideo(state, "v1", "alpha", 5_000, 10); + assert.equal(state.v1.until, 5_010); +}); + +test("isVideoDeferred reflects the until window, per video", () => { + const state: VideoDeferralState = { v1: { until: 2_000, channelSlug: "a" } }; + assert.equal(isVideoDeferred(state, "v1", 1_999), true); + assert.equal(isVideoDeferred(state, "v1", 2_000), false); + assert.equal(isVideoDeferred(state, "v2", 1_000), false); +}); + +test("pruneDeferred drops lapsed deferrals only", () => { + const state: VideoDeferralState = { + old: { until: 1_000, channelSlug: "a" }, + edge: { until: 2_000, channelSlug: "a" }, + live: { until: 3_000, channelSlug: "b" }, + }; + pruneDeferred(state, 2_000); + assert.deepEqual(Object.keys(state), ["live"]); +}); + +test("coerceVideoDeferrals tolerates corrupt/missing shapes", () => { + assert.deepEqual(coerceVideoDeferrals(undefined), {}); + assert.deepEqual(coerceVideoDeferrals("nope"), {}); + assert.deepEqual( + coerceVideoDeferrals({ + ok: { until: 5, channelSlug: "a" }, + noSlug: { until: 5 }, + badUntil: { until: "5", channelSlug: "a" }, + inf: { until: Infinity, channelSlug: "a" }, + nul: null, + }), + { ok: { until: 5, channelSlug: "a" } }, + ); +}); diff --git a/common/jobs/platformBackoff.ts b/common/jobs/platformBackoff.ts @@ -92,3 +92,75 @@ export function coercePlatformBackoff(value: unknown): PlatformBackoffState { } return out; } + +// --------------------------------------------------------------------------- +// Per-video deferral (release 7, YouTube lane pacing). +// +// A platform cooldown alone does not pace a lane whose queue is ordered: with +// `order: "listed"` the runner re-picks the SAME video the moment the cooldown +// lapses, so one video whose subtitle fetch keeps answering 429 climbs `fails` +// to the 30-minute cap and holds the whole platform (2026-09-24: one Short +// retried 12x). A rate-limited video is therefore also DEFERRED for +// VIDEO_RATE_LIMIT_DEFER_MS: the auto-download pick skips it, so after the +// cooldown the runner moves on to the next video. Persisted beside +// `platformBackoff` (a restart must not re-hit the same video at fails+1). +// Manual Sync / download-missing do not consult it. + +export type VideoDeferral = { + // Epoch ms: the auto-download pick skips this video until this instant. + until: number; + // The channel the video was picked from — for the status view's link. + channelSlug: string; +}; + +// Keyed by video id. +export type VideoDeferralState = Record<string, VideoDeferral>; + +export const VIDEO_RATE_LIMIT_DEFER_MS = 6 * 60 * 60_000; // 6 hours + +// Defer a video from `now` for `ms`. Mutates in place. +export function deferVideo( + state: VideoDeferralState, + videoId: string, + channelSlug: string, + now: number, + ms: number = VIDEO_RATE_LIMIT_DEFER_MS, +): void { + state[videoId] = { until: now + ms, channelSlug }; +} + +// True while the video's deferral window is open. +export function isVideoDeferred( + state: VideoDeferralState, + videoId: string, + now: number, +): boolean { + const entry = state[videoId]; + return entry !== undefined && entry.until > now; +} + +// Drop deferrals whose window has lapsed. Mutates in place — keeps the map +// bounded (unlike platform backoff there is no escalation memory to retain). +export function pruneDeferred(state: VideoDeferralState, now: number): void { + for (const [videoId, entry] of Object.entries(state)) { + if (entry.until <= now) delete state[videoId]; + } +} + +// Defensive coercion for the persisted shape; mirrors coercePlatformBackoff. +export function coerceVideoDeferrals(value: unknown): VideoDeferralState { + const out: VideoDeferralState = {}; + if (!value || typeof value !== "object") return out; + for (const [videoId, raw] of Object.entries(value as Record<string, unknown>)) { + if (!raw || typeof raw !== "object") continue; + const r = raw as Record<string, unknown>; + if ( + typeof r.until === "number" && + Number.isFinite(r.until) && + typeof r.channelSlug === "string" + ) { + out[videoId] = { until: r.until, channelSlug: r.channelSlug }; + } + } + return out; +}