fix(transcript): pipeline URLs signees yt-dlp + fetch durci anti-429
CI / build-and-test (push) Successful in 14m1s

Les URLs timedtext InnerTube repondent 200-vide (throttle YouTube) :
sonde rapide max 2 sans retry, puis dump yt-dlp -> URLs signees en
direct (max 4), puis --write-sub. 200-vide traite comme 429
(retryAfterSec), fmt= plus jamais duplique (signature), cookies
YT_COOKIES_FILE reutilises par le fetch direct. Tests 26/26.
This commit is contained in:
2026-09-27 20:47:28 -04:00
parent 148a00e60f
commit a79831f406
7 changed files with 214 additions and 62 deletions
+28
View File
@@ -18,6 +18,9 @@ import {
translatedFallbacks,
firstPerLanguage,
looksLikeHtmlError,
ensureFmtParam,
isEmptyTimedTextBody,
mergeTranscriptCandidates,
capLines,
normalizeTranscriptProvider,
} from '../transcript.mjs';
@@ -234,6 +237,31 @@ describe('dedupeTranscriptLines (rolling-window auto-captions)', () => {
});
});
describe('timedtext fetch guards (anti-429 datacenter)', () => {
it('ensureFmtParam never duplicates fmt on signed urls', () => {
const signed = 'https://www.youtube.com/api/timedtext?v=abc&fmt=json3&signature=XYZ';
assert.equal(ensureFmtParam(signed), signed);
const bare = 'https://www.youtube.com/api/timedtext?v=abc&caps=asr';
assert.match(ensureFmtParam(bare), /fmt=json3/);
assert.equal(ensureFmtParam('', 'json3'), '');
});
it('isEmptyTimedTextBody treats 200-empty as transient', () => {
assert.equal(isEmptyTimedTextBody(''), true);
assert.equal(isEmptyTimedTextBody(' '), true);
assert.equal(isEmptyTimedTextBody('<!DOCTYPE html> sorry'), true);
assert.equal(isEmptyTimedTextBody(JSON.stringify({ events: [{ tStartMs: 0, dDurationMs: 1, segs: [{ utf8: 'Hi' }] }] })), false);
assert.equal(isEmptyTimedTextBody('WEBVTT\n\n00:00:00.000 --> 00:00:01.000\nHello\n'), false);
});
it('mergeTranscriptCandidates prefers signed urls, dedupes, bounds', () => {
const s = { track: { url: 'https://x/signed?sig=1' }, lang: 'fr' };
const dup = { track: { url: 'https://x/signed?sig=1' }, lang: 'fr' };
const it = { track: { url: 'https://x/innertube?fmt=json3' }, lang: 'fr' };
const out = mergeTranscriptCandidates({ signed: [s, dup], innertube: [it], max: 5 });
assert.deepEqual(out.map((c) => c.track.url), ['https://x/signed?sig=1', 'https://x/innertube?fmt=json3']);
assert.equal(mergeTranscriptCandidates({ signed: [s], innertube: [it], max: 1 }).length, 1);
});
});
describe('capLines + normalizeTranscriptProvider', () => { it('truncates very long transcripts', () => {
const lines = Array.from({ length: 10 }, (_, i) => ({ t: i, dur: 1, text: `l${i}` }));
assert.equal(capLines(lines, 3).length, 3);