Files
ytplayer/server/ytdlp-resilience.js

40 lines
2.4 KiB
JavaScript

// Bounded client fallbacks shared by metadata probes and actual downloads.
export const BOT_CHECK_RE = /Sign in to confirm|not a bot|confirm you.{0,3}re not a bot/i;
export const DEFAULT_FALLBACK_CLIENTS = ['web_embedded', 'web_safari', 'tv_simply', 'android_vr', 'mweb'];
export function canTryAnotherClient(error) {
const message = String(error?.message || error || '');
// Explicit access restrictions are not fixed by selecting a different client.
if (/private video|members.only|join this channel|not available in your country|geo.?restricted|removed by|video has been removed|copyright claim|age.restricted|confirm your age/i.test(message)) return false;
return BOT_CHECK_RE.test(message) || /video unavailable|requested format (?:is )?not available|no video formats found/i.test(message);
}
export function createResilientYtdlp({ run, withCookies = args => args, clients = DEFAULT_FALLBACK_CLIENTS, log = console }) {
return async function runResilient(args, opts = {}) {
const explicitClient = args.some(arg => String(arg).includes('player_client=') || String(arg).includes('player-client='));
if (opts.signal?.aborted) throw opts.signal.reason || new Error('save cancelled');
try {
return await run(withCookies(args), opts);
} catch (initial) {
if (explicitClient || opts.signal?.aborted || !canTryAnotherClient(initial)) throw initial;
const failures = [{ client: 'default', error: initial }];
for (const client of [...new Set(clients)]) {
if (opts.signal?.aborted) throw opts.signal.reason || initial;
try {
const out = await run(withCookies(['--extractor-args', `youtube:player_client=${client}`, ...args]), opts);
log.warn?.(`[ytplayer] YouTube fallback succeeded via player_client=${client}`);
return out;
} catch (error) {
failures.push({ client, error });
if (opts.signal?.aborted || !canTryAnotherClient(error)) throw error;
}
}
// Keep the original useful diagnosis, not the final client's weaker
// "no formats" error. Retain the bounded attempt trail for diagnostics.
const error = new Error(`${initial.message}\nYouTube clients tried: ${failures.map(f => f.client).join(', ')}`, { cause: initial });
error.attempts = failures.map(({ client, error }) => ({ client, detail: String(error.message || error) }));
throw error;
}
};
}