fix(transcript): pipeline URLs signees yt-dlp + fetch durci anti-429
CI / build-and-test (push) Successful in 14m1s
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:
@@ -319,6 +319,60 @@ export function looksLikeHtmlError(text) {
|
||||
return s.startsWith('<!doctype') || s.startsWith('<html');
|
||||
}
|
||||
|
||||
/**
|
||||
* Append `fmt=xx` to a timedtext URL only when it has no `fmt` param yet.
|
||||
* Re-signing a signed yt-dlp URL by appending a duplicate `fmt` breaks its
|
||||
* `signature` and YouTube answers 429/Sorry: always reuse the URL as-is.
|
||||
* Pure and testable offline.
|
||||
*/
|
||||
export function ensureFmtParam(url, fmt = 'json3') {
|
||||
const raw = String(url || '');
|
||||
if (!raw) return raw;
|
||||
try {
|
||||
const u = new URL(raw);
|
||||
if (u.searchParams.has('fmt')) return u.toString();
|
||||
u.searchParams.set('fmt', fmt);
|
||||
return u.toString();
|
||||
} catch {
|
||||
if (/[?&]fmt=/i.test(raw)) return raw;
|
||||
return `${raw}${raw.includes('?') ? '&' : '?'}fmt=${encodeURIComponent(fmt)}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* True when a timedtext HTTP 200 body carries no usable caption payload.
|
||||
* YouTube throttles datacenter egress IPs with `200 + empty body` (instead
|
||||
* of an explicit 429): callers must treat it as transient (same as 429),
|
||||
* never as "no subtitles".
|
||||
*/
|
||||
export function isEmptyTimedTextBody(text) {
|
||||
const s = String(text ?? '');
|
||||
if (!s) return true;
|
||||
if (looksLikeHtmlError(s)) return true;
|
||||
return s.trim().length < 32;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge fetch candidates, signed yt-dlp URLs first (they carry a fresh
|
||||
* `signature`/`expire` and are the only ones plain fetch can still read),
|
||||
* then InnerTube base URLs as a quick probe. De-duplicated by URL, bounded.
|
||||
* Pure and testable offline.
|
||||
* @param {{ signed?: Array<{track:any,lang:string|null}>, innertube?: Array<{track:any,lang:string|null}>, max?: number }} opts
|
||||
*/
|
||||
export function mergeTranscriptCandidates({ signed = [], innertube = [], max = 5 } = {}) {
|
||||
const n = Math.max(1, Number(max || 5));
|
||||
const seen = new Set();
|
||||
const out = [];
|
||||
for (const c of [...(signed || []), ...(innertube || [])]) {
|
||||
const u = String(c?.track?.url || '');
|
||||
if (!u || seen.has(u)) continue;
|
||||
seen.add(u);
|
||||
out.push(c);
|
||||
if (out.length >= n) break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function vttTimestampToSeconds(h, m, s, ms) {
|
||||
return Number(h) * 3600 + Number(m) * 60 + Number(s) + Number(ms) / 1000;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user