// rcp_patch_probe.mjs — does the rcp/prorcp chain surface an m3u8 under
// patchright (headed, xvfb) the way it fails under headless playwright?
// Usage: xvfb-run -a node node_helper/rcp_patch_probe.mjs <embedUrl>
import { chromium } from 'patchright';
const log = (...a) => console.error('[pp]', ...a);
const AD = /devilyquondam|dtscout|histats|\/cuid\/|popunder|propeller|adsco|doubleclick|onclick\.|popads|\/rum\?/i;
const M3U8 = /\.m3u8(\?|$)/i;
const EMBED = process.argv[2];
if (!EMBED) { console.error('usage: <embedUrl>'); process.exit(1); }
const ctx = await chromium.launchPersistentContext('/tmp/rcp_profile1', {
headless: false, args: ['--no-sandbox'], viewport: { width: 1280, height: 720 }, locale: 'en-US',
});
// auto-close popunders, never touch flixlab/cloud* docs
ctx.on('page', p => setTimeout(() => {
if (!p.isClosed() && !/cloudorchestranova|cloudnestra|vsembed/.test(p.url())) p.close().catch(() => {});
}, 500));
const media = new Map();
ctx.on('request', r => { const u = r.url(); if (M3U8.test(u)) media.set(u, (r.headers() || {}).referer || null); });
await ctx.route('**/*', r => AD.test(r.request().url()) ? r.abort() : r.continue());
// step 1: embed -> rcp
const p1 = ctx.pages()[0] || await ctx.newPage();
let rcp = null;
p1.on('framenavigated', f => { const u = f.url(); if (/\/rcp\//i.test(u) && !/prorcp/i.test(u) && !rcp) rcp = u; });
await p1.setExtraHTTPHeaders({ Referer: 'https://netoda.tech/' });
await p1.goto(EMBED, { waitUntil: 'domcontentloaded', timeout: 45000 }).catch(e => log('embed warn', e.message));
for (let i = 0; i < 18 && !rcp; i++) await p1.waitForTimeout(1000);
log('rcp:', rcp ? rcp.slice(0, 70) + '…' : null);
if (!rcp) { await ctx.close(); console.log(JSON.stringify({ error: 'no rcp' })); process.exit(3); }
// step 2: drive rcp directly
const p = await ctx.newPage();
p.on('response', r => { if (/prorcp|m3u8|base64\.js/i.test(r.url())) log('resp', r.status(), r.url().slice(0, 90)); });
await p.setExtraHTTPHeaders({ Referer: EMBED });
await p.goto(rcp, { waitUntil: 'domcontentloaded', timeout: 45000 }).catch(e => log('rcp warn', e.message));
await p.waitForTimeout(3000);
const clicked = await p.evaluate(() => {
const el = document.querySelector('#pl_but') || document.querySelector('.play') || document.querySelector('[onclick]');
if (el) { el.click(); return el.id || el.className || 'onclick'; }
if (document.body) { document.body.click(); return 'body'; }
return null;
}).catch(e => 'evalErr:' + e.message);
log('clicked:', clicked);
for (let i = 0; i < 12 && media.size === 0; i++) {
await p.waitForTimeout(3000);
const pf = p.frames().map(f => f.url()).filter(u => /prorcp/i.test(u));
let inner = '';
if (pf.length) {
const f = p.frames().find(x => /prorcp/i.test(x.url()));
inner = await f.evaluate(() => ({
text: ((document.body && document.body.innerText) || '').replace(/\s+/g, ' ').slice(0, 90),
video: !!document.querySelector('video'),
turnstile: !!document.querySelector('[class*=cf-],iframe[src*=challenges.cloudflare]'),
})).then(JSON.stringify).catch(e => 'evalErr:' + e.message.slice(0, 60));
// try clicking inside the player
await f.evaluate(() => { const v = document.querySelector('video') || document.querySelector('#pl_but') || document.body; if (v) v.click(); }).catch(() => {});
}
log(`t=${(i + 1) * 3}s prorcpFrame=${pf.length ? 'Y' : 'n'} inner=${inner} media=${media.size}`);
}
await ctx.close().catch(() => {});
if (!media.size) { console.log(JSON.stringify({ error: 'no m3u8 under patchright either' })); process.exit(5); }
const url = [...media.keys()].find(u => /master/i.test(u)) || [...media.keys()][0];
console.log(JSON.stringify({ url, all: [...media.keys()] }));