const { check, done, req } = require('./_setup'); const plan = req('src/youtube/plan'); const W = 'http://warp:1080'; const ROUTES = { 'warp+direct': [W, null], 'direct': [null], 'warp seul': [W] }; const FB = 'web_safari,tv_downgraded'; const label = (a) => `${a.proxy ? 'W' : 'D'}/${a.identity === 'cookies' ? 'C' : 'A'}${a.clients ? '/fb' : ''}${a.seek ? '/' + a.seek : ''}`; /** * Simule getStream : chaque tentative échoue avec la classe donnée (tableau, ou fonction (attempt, i, history)) * okAt : index de la tentative qui réussit ; rotateAt : la tentative i invalide les cookies */ function simulate({ routes, mode = 'fallback', cookiesValid = true, cookiesOnProxy = false, fail, okAt = -1, rotateAt = -1, seekMode = null, fallbackClients = FB }) { const history = []; const seq = []; let valid = cookiesValid; for (let i = 0; i < 10; i++) { const a = plan.planNext({ routes, mode, cookiesValid: valid, cookiesOnProxy, clients: '', fallbackClients, seekMode, history, maxAttempts: 4 }); if (!a) break; seq.push(label(a)); if (i === okAt) { seq.push('OK'); break; } const cls = typeof fail === 'function' ? fail(a, i, history) : (Array.isArray(fail) ? fail[Math.min(i, fail.length - 1)] : fail); if (i === rotateAt) valid = false; history.push({ ...a, cls }); } return seq.join(' > '); } function expect(name, input, expected) { const got = simulate(input); check(`${name.padEnd(62)} ${got}`, got === expected, { expected }); } console.log('# Mode never'); expect('never, warp+direct, UNKNOWN', { routes: ROUTES['warp+direct'], mode: 'never', fail: 'UNKNOWN' }, 'W/A > D/A'); expect('never, warp+direct, BOT_CHECK (jamais de cookies)', { routes: ROUTES['warp+direct'], mode: 'never', fail: 'BOT_CHECK' }, 'W/A > D/A'); expect('never, warp+direct, HTTP_403 (clients de repli une fois)', { routes: ROUTES['warp+direct'], mode: 'never', fail: 'HTTP_403' }, 'W/A > W/A/fb > D/A'); expect('never, direct, LOGIN_REQUIRED (arrêt)', { routes: ROUTES.direct, mode: 'never', fail: 'LOGIN_REQUIRED' }, 'D/A'); expect('never, warp seul, TIMEOUT', { routes: ROUTES['warp seul'], mode: 'never', fail: 'TIMEOUT' }, 'W/A'); expect('never, warp+direct, PRIVATE (fatal)', { routes: ROUTES['warp+direct'], mode: 'never', fail: 'PRIVATE' }, 'W/A'); expect('never, warp+direct, UNAVAILABLE (fatal)', { routes: ROUTES['warp+direct'], mode: 'never', fail: 'UNAVAILABLE' }, 'W/A'); expect('never, warp+direct, RATE_LIMITED (autre route uniquement)', { routes: ROUTES['warp+direct'], mode: 'never', fail: 'RATE_LIMITED' }, 'W/A > D/A'); expect('never, warp+direct, BINARY_MISSING (arrêt)', { routes: ROUTES['warp+direct'], mode: 'never', fail: 'BINARY_MISSING' }, 'W/A'); console.log('# Mode fallback (cookies valides)'); expect('fallback, warp+direct, BOT_CHECK', { routes: ROUTES['warp+direct'], fail: 'BOT_CHECK' }, 'W/A > D/A > D/C'); expect('fallback, warp+direct, IP_BLOCKED, cookiesOnProxy', { routes: ROUTES['warp+direct'], cookiesOnProxy: true, fail: 'IP_BLOCKED' }, 'W/A > D/A > W/C > D/C'); expect('fallback, direct, CAPTCHA', { routes: ROUTES.direct, fail: 'CAPTCHA' }, 'D/A > D/C'); expect('fallback, warp seul, BOT_CHECK (proxy = seule route)', { routes: ROUTES['warp seul'], fail: 'BOT_CHECK' }, 'W/A > W/C'); expect('fallback, warp+direct, AGE_RESTRICTED (compte directement)', { routes: ROUTES['warp+direct'], fail: 'AGE_RESTRICTED' }, 'W/A > D/C'); expect('fallback, warp+direct, MEMBERS_ONLY', { routes: ROUTES['warp+direct'], fail: 'MEMBERS_ONLY' }, 'W/A > D/C'); expect('fallback, warp+direct, LOGIN_REQUIRED, cookiesOnProxy (refus avec compte = arrêt)', { routes: ROUTES['warp+direct'], cookiesOnProxy: true, fail: 'LOGIN_REQUIRED' }, 'W/A > W/C'); expect('fallback, warp+direct, UNKNOWN (pas de cookies)', { routes: ROUTES['warp+direct'], fail: 'UNKNOWN' }, 'W/A > D/A'); expect('fallback, warp+direct, GEO_BLOCKED (autre route seulement)', { routes: ROUTES['warp+direct'], fail: 'GEO_BLOCKED' }, 'W/A > D/A'); expect('fallback, direct, GEO_BLOCKED', { routes: ROUTES.direct, fail: 'GEO_BLOCKED' }, 'D/A'); expect('fallback, direct, HTTP_403', { routes: ROUTES.direct, fail: 'HTTP_403' }, 'D/A > D/A/fb'); expect('fallback, direct, SABR', { routes: ROUTES.direct, fail: 'SABR' }, 'D/A > D/A/fb'); expect('fallback, direct, POT_MISSING', { routes: ROUTES.direct, fail: 'POT_MISSING' }, 'D/A > D/A/fb'); expect('fallback, direct, EMPTY_DOWNLOAD', { routes: ROUTES.direct, fail: 'EMPTY_DOWNLOAD' }, 'D/A > D/A/fb'); expect('fallback, direct, FORMAT_UNAVAILABLE, repli désactivé', { routes: ROUTES.direct, fail: 'FORMAT_UNAVAILABLE', fallbackClients: '' }, 'D/A'); expect('fallback, warp+direct, 403 puis BOT_CHECK', { routes: ROUTES['warp+direct'], fail: ['HTTP_403', 'BOT_CHECK'] }, 'W/A > W/A/fb > D/A > D/C'); expect('fallback, warp+direct, BOT puis 403 (max 4)', { routes: ROUTES['warp+direct'], fail: ['BOT_CHECK', 'BOT_CHECK', 'HTTP_403'] }, 'W/A > D/A > D/C > D/C/fb'); expect('fallback, warp+direct, réussite à la 2e', { routes: ROUTES['warp+direct'], fail: 'BOT_CHECK', okAt: 1 }, 'W/A > D/A > OK'); expect('fallback, cookies tournés pendant la tentative avec compte', { routes: ROUTES['warp+direct'], fail: 'BOT_CHECK', rotateAt: 2 }, 'W/A > D/A > D/C'); expect('fallback, cookies invalides, BOT_CHECK', { routes: ROUTES['warp+direct'], cookiesValid: false, fail: 'BOT_CHECK' }, 'W/A > D/A'); expect('fallback, cookies invalides, AGE_RESTRICTED (arrêt)', { routes: ROUTES['warp+direct'], cookiesValid: false, fail: 'AGE_RESTRICTED' }, 'W/A'); expect('fallback, compte refusé (AGE avec cookies) -> arrêt', { routes: ROUTES.direct, fail: 'AGE_RESTRICTED' }, 'D/A > D/C'); console.log('# Mode always'); expect('always, warp+direct (pas de compte via WARP)', { routes: ROUTES['warp+direct'], mode: 'always', fail: 'UNKNOWN' }, 'W/A > D/C'); expect('always, warp+direct, cookiesOnProxy', { routes: ROUTES['warp+direct'], mode: 'always', cookiesOnProxy: true, fail: 'UNKNOWN' }, 'W/C > D/C'); expect('always, direct, BOT_CHECK', { routes: ROUTES.direct, mode: 'always', fail: 'BOT_CHECK' }, 'D/C'); expect('always, warp seul (compte autorisé)', { routes: ROUTES['warp seul'], mode: 'always', fail: 'UNKNOWN' }, 'W/C'); expect('always, direct, rotation -> anonyme', { routes: ROUTES.direct, mode: 'always', fail: 'COOKIES_ROTATED', rotateAt: 0 }, 'D/C > D/A'); expect('always, warp+direct, AGE sur WARP anonyme -> compte', { routes: ROUTES['warp+direct'], mode: 'always', fail: 'AGE_RESTRICTED' }, 'W/A > D/C'); expect('always, direct, HTTP_403 -> clients de repli avec compte', { routes: ROUTES.direct, mode: 'always', fail: 'HTTP_403' }, 'D/C > D/C/fb'); expect('always sans cookies valides = never', { routes: ROUTES['warp+direct'], mode: 'always', cookiesValid: false, fail: 'BOT_CHECK' }, 'W/A > D/A'); expect('always, PRIVATE', { routes: ROUTES['warp+direct'], mode: 'always', fail: 'PRIVATE' }, 'W/A'); console.log('# Seek'); check('seekModeFor auto 30 -> native', plan.seekModeFor('auto', 30) === 'native'); check('seekModeFor auto 1200 -> native', plan.seekModeFor('auto', 1200) === 'native'); check('seekModeFor auto 1201 -> sections', plan.seekModeFor('auto', 1201) === 'sections'); check('seekModeFor ffmpeg 10 -> sections', plan.seekModeFor('ffmpeg', 10) === 'sections'); check('seekModeFor native 5000 -> native', plan.seekModeFor('native', 5000) === 'native'); check('seekModeFor sans seek -> null', plan.seekModeFor('auto', 0) === null); expect('sections + HTTP_403 -> natif puis clients de repli', { routes: ROUTES.direct, seekMode: 'sections', fail: 'HTTP_403' }, 'D/A/sections > D/A/native > D/A/fb/native'); expect('sections + 403 sur warp+direct -> la suite en natif', { routes: ROUTES['warp+direct'], seekMode: 'sections', fail: ['HTTP_403', 'BOT_CHECK'] }, 'W/A/sections > W/A/native > D/A/native > D/C/native'); expect('natif + UNKNOWN', { routes: ROUTES['warp+direct'], seekMode: 'native', fail: 'UNKNOWN' }, 'W/A/native > D/A/native'); console.log('# Matrice (modes x routes x classes)'); const classes = ['BOT_CHECK', 'IP_BLOCKED', 'CAPTCHA', 'RATE_LIMITED', 'HTTP_403', 'HTTP_429', 'AGE_RESTRICTED', 'LOGIN_REQUIRED', 'MEMBERS_ONLY', 'PRIVATE', 'UNAVAILABLE', 'GEO_BLOCKED', 'POT_MISSING', 'SABR', 'JS_CHALLENGE', 'FORMAT_UNAVAILABLE', 'PAGE_RELOAD', 'COOKIES_ROTATED', 'EMPTY_DOWNLOAD', 'BINARY_MISSING', 'TIMEOUT', 'UNKNOWN']; let maxLen = 0, dup = 0; for (const mode of ['never', 'always', 'fallback']) { for (const [rname, routes] of Object.entries(ROUTES)) { const row = []; for (const cls of classes) { const seq = simulate({ routes, mode, fail: cls }); const parts = seq.split(' > '); maxLen = Math.max(maxLen, parts.length); if (new Set(parts).size !== parts.length) dup++; row.push(`${cls}: ${seq}`); } console.log(` [${mode} | ${rname}]`); for (const r of row) console.log(` ${r}`); } } check('jamais plus de 4 tentatives', maxLen <= 4, maxLen); check('jamais deux fois la même tentative', dup === 0, dup); check('pas de compte en mode never', !['warp+direct', 'direct', 'warp seul'].some(r => classes.some(c => simulate({ routes: ROUTES[r], mode: 'never', fail: c }).includes('/C')))); done();