// 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; } }; }