#!/usr/bin/env node
// fmovies_resolve.mjs — resolves a title to an HLS master.m3u8.
//
// 2026-07-21 rewrite. fmovies' "Server 1" now just redirects (netoda.tech /get/)
// into the vsembed.ru embed keyed by the TMDB id, whose player is the vidsrc/
// cloudnestra "rcp" chain. Two entry modes, both ending in the same rcp sniff:
//
// TT MODE (Jacktook's normal TMDB menu):
// <movie|series> <tt-id> [s] [e]
// imdb --TMDB find--> tmdb -> vsembed.ru/embed/{movie|tv}/<tmdb>[/s/e]
//
// URL MODE (fmovies-native catalog, fmv: ids — stays fmovies-native so it
// handles TMDB-fragmented titles the tt path can't):
// url <fmoviesFilmUrl> <movie|series> [s] [e]
// drive the fmovies film page (pick episode, click Server 1) and capture the
// vsembed embed URL it redirects into (which carries fmovies' own tmdb id).
//
// Both then: capture the <rcp-host>/rcp/<hash> iframe -> drive it directly (ads
// blocked, Referer=embed) -> click #pl_but -> /prorcp/<hash> -> sniff master.m3u8.
// Crypto-agnostic (their JS mints the JWT/IP-locked token, we just sniff the wire)
// so it survives token/domain rotations. m3u8 is token+IP-CIDR locked (~4h), NOT
// referer-locked; playback works while the player shares the resolver's /24.
//
// Output (stdout, last line): {streams:[{name,title,url,referer,quality,behaviorHints}]}
// or {error:"..."}. Diagnostics on stderr.
import { chromium } from 'playwright';
const log = (...a) => console.error('[resolve]', ...a);
const TMDB_KEY = 'b70756b7083d9ee60f849d82d94a0d80'; // public Jacktook default key
const VSEMBED = 'https://vsembed.ru';
const UA = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
const AD = /devilyquondam|dtscout|histats|\/cuid\/|popunder|propeller|adsco|doubleclick|onclick\.|popads|\/rum\?/i;
const M3U8 = /\.m3u8(\?|$)/i;
const RCP = /\/rcp\//i;
const EMBED_RE = /\/embed\//i;
const RCP_WAIT_MS = 16000; // wait for the embed page to spawn its rcp iframe
const SNIFF_MS = 22000; // wait after clicking play for the m3u8 to surface
const FMV_WAIT_MS = 18000; // wait for the fmovies drive to redirect into the embed
// ---- argv: two modes -------------------------------------------------------
const ARGV = process.argv.slice(2);
const URL_MODE = ARGV[0] === 'url';
let TYPE, IMDB, FILM_URL, SEASON, EPISODE;
function die(error, code = 1) { console.log(JSON.stringify({ error })); process.exit(code); }
if (URL_MODE) {
FILM_URL = ARGV[1];
TYPE = ARGV[2] === 'series' ? 'series' : 'movie';
SEASON = ARGV[3] ? parseInt(ARGV[3], 10) : null;
EPISODE = ARGV[4] ? parseInt(ARGV[4], 10) : null;
if (!FILM_URL || !/^https?:\/\//.test(FILM_URL)) die('usage: url <filmUrl> <movie|series> [season] [episode]');
if (TYPE === 'series' && (!SEASON || !EPISODE)) die('series url mode requires season and episode');
} else {
TYPE = ARGV[0] === 'series' ? 'series' : 'movie';
IMDB = ARGV[1];
SEASON = ARGV[2] ? parseInt(ARGV[2], 10) : null;
EPISODE = ARGV[3] ? parseInt(ARGV[3], 10) : null;
if (!IMDB || !/^tt\d+$/.test(IMDB)) die('usage: <movie|series> <imdb_tt_id> [season] [episode]');
if (TYPE === 'series' && (!SEASON || !EPISODE)) die('series requires season and episode');
}
// ---- shared helpers --------------------------------------------------------
async function imdbToTmdb(type, imdb) {
const r = await fetch(`https://api.themoviedb.org/3/find/${imdb}?external_source=imdb_id&api_key=${TMDB_KEY}`);
if (!r.ok) throw new Error(`tmdb find ${r.status}`);
const d = await r.json();
const tv = (d.tv_results || [])[0];
const mv = (d.movie_results || [])[0];
if (type === 'series' && tv) return tv.id;
if (type === 'movie' && mv) return mv.id;
const any = mv || tv; // tolerate a type mismatch from cinemeta
if (any) return any.id;
throw new Error('no tmdb match for ' + imdb);
}
function embedUrl(type, tmdb) {
return type === 'series'
? `${VSEMBED}/embed/tv/${tmdb}/${SEASON}/${EPISODE}?autoplay=1`
: `${VSEMBED}/embed/movie/${tmdb}?autoplay=1`;
}
// Load the vsembed embed and capture the rcp iframe URL it spawns.
async function captureRcp(ctx, embed) {
const p = await ctx.newPage();
let rcp = null;
p.on('framenavigated', f => { const u = f.url(); if (RCP.test(u) && !rcp) rcp = u; });
await p.setExtraHTTPHeaders({ Referer: 'https://netoda.tech/' });
await p.goto(embed, { waitUntil: 'domcontentloaded', timeout: 30000 }).catch(e => log('embed goto warn:', e.message));
const deadline = Date.now() + RCP_WAIT_MS;
while (Date.now() < deadline && !rcp) await p.waitForTimeout(1000);
await p.close().catch(() => {});
return rcp;
}
// Drive the rcp page directly, click play, sniff the master.m3u8.
async function sniffM3u8(ctx, rcpUrl, referer) {
const p = await ctx.newPage();
const found = new Map(); // m3u8 url -> referer header seen on the request
p.on('request', r => { const u = r.url(); if (M3U8.test(u)) found.set(u, (r.headers() || {}).referer || null); });
await p.setExtraHTTPHeaders({ Referer: referer });
await p.goto(rcpUrl, { waitUntil: 'domcontentloaded', timeout: 30000 }).catch(e => log('rcp goto warn:', e.message));
await p.waitForTimeout(2500);
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(() => null);
log('play click:', clicked);
const deadline = Date.now() + SNIFF_MS;
while (Date.now() < deadline && found.size === 0) await p.waitForTimeout(1000);
if (found.size) await p.waitForTimeout(1500); // grace so we prefer master over a variant
await p.close().catch(() => {});
if (!found.size) return null;
const url = [...found.keys()].find(u => /master/i.test(u)) || [...found.keys()][0];
// The CDN's video segments (/content/.../page-N.html) 403 on a vsembed/netoda/
// m3u8-host Referer but 200 on the rcp PLAYER origin (or none). The captured
// request referer is masked (we force Referer=embed to load rcp), so ignore it
// and hand the player its own origin — rotation-agnostic (cloudnestra/cloudorchestra/…).
const rcpOrigin = (() => { try { return new URL(rcpUrl).origin + '/'; } catch { return 'https://cloudnestra.com/'; } })();
return { url, referer: rcpOrigin };
}
// URL MODE: drive the fmovies film page and capture the vsembed embed URL (and,
// as a fallback, the rcp URL directly). fmovies maps its own mid -> tmdb, so the
// embed URL it redirects into carries the correct tmdb id for this exact title
// (and, for series, the exact episode).
async function driveFmovies(ctx, filmUrl, type, season, episode) {
const p = await ctx.newPage();
let embed = null, rcp = null;
p.on('framenavigated', f => {
const u = f.url();
if (EMBED_RE.test(u) && !embed) embed = u;
if (RCP.test(u) && !rcp) rcp = u;
});
await p.goto(filmUrl, { waitUntil: 'domcontentloaded', timeout: 30000 }).catch(e => log('film goto warn:', e.message));
await p.waitForTimeout(2500);
// open the player (populates the internal mid; skipping it encrypts mid=undefined)
await p.evaluate(() => { const a = document.getElementById('play-now'); if (a) a.click(); }).catch(() => {});
await p.waitForTimeout(2000);
if (type === 'series') {
const tag = `s${String(season).padStart(2, '0')}e${String(episode).padStart(2, '0')}`;
const chose = await p.evaluate(({ tag, ep }) => {
const eps = [...document.querySelectorAll('.episode')];
const norm = (s) => (s || '').toLowerCase().replace(/[^a-z0-9]/g, '');
let t = eps.find(b => norm(b.id).includes(tag) || norm(b.textContent).includes(tag))
|| eps.find(b => (b.id || '').split('-').pop() === String(ep));
if (t) { t.click(); return t.id || t.textContent.trim().slice(0, 30); }
return null;
}, { tag, ep: episode }).catch(() => null);
log('episode pick:', chose);
await p.waitForTimeout(1500);
}
// click Server 1 (srv-1 -> netoda -> vsembed); fall through to alternates
for (const id of ['srv-1', 'srv-2', 'srv-5']) {
const ok = await p.evaluate((s) => { const b = document.getElementById(s); if (b) { b.click(); return true; } return false; }, id).catch(() => false);
if (ok) { log('clicked', id); break; }
}
const deadline = Date.now() + FMV_WAIT_MS;
while (Date.now() < deadline && !embed && !rcp) await p.waitForTimeout(1000);
await p.close().catch(() => {});
return { embed, rcp };
}
// ---- main ------------------------------------------------------------------
(async () => {
const browser = await chromium.launch({ headless: true, args: ['--no-sandbox'] });
const ctx = await browser.newContext({ userAgent: UA });
await ctx.route('**/*', route => { AD.test(route.request().url()) ? route.abort() : route.continue(); });
const fail = async (msg, code) => { await browser.close().catch(() => {}); die(msg, code); };
// Resolve to an rcp URL + the referer to drive it with.
let rcp = null, referer = null, dbg = {};
try {
if (URL_MODE) {
log('url mode:', FILM_URL);
const { embed, rcp: directRcp } = await driveFmovies(ctx, FILM_URL, TYPE, SEASON, EPISODE);
log('fmovies -> embed:', embed, 'rcp:', directRcp ? directRcp.slice(0, 60) + '...' : null);
if (embed) {
// Re-derive a fresh rcp from the embed (most reliable path).
rcp = await captureRcp(ctx, embed);
referer = embed;
dbg = { embed };
}
if (!rcp && directRcp) { rcp = directRcp; referer = embed || FILM_URL; dbg = { embed, viaDirectRcp: true }; }
if (!rcp) return fail('no embed/rcp from fmovies film page (title may be unavailable)', 3);
} else {
const tmdb = await imdbToTmdb(TYPE, IMDB);
log(`tmdb: ${IMDB} -> ${tmdb} (${TYPE})`);
const embed = embedUrl(TYPE, tmdb);
log('embed:', embed);
rcp = await captureRcp(ctx, embed);
referer = embed;
dbg = { tmdb, embed };
if (!rcp) return fail('no rcp iframe from vsembed (title may be unavailable)', 3);
}
} catch (e) {
return fail((URL_MODE ? 'fmovies drive failed: ' : 'lookup failed: ') + e.message, 2);
}
log('rcp:', rcp.slice(0, 90) + '...');
let stream = null;
try { stream = await sniffM3u8(ctx, rcp, referer); }
catch (e) { return fail('sniff failed: ' + e.message, 5); }
await browser.close().catch(() => {});
if (!stream) die('no playable stream (m3u8 did not surface)', 5);
const labelTag = TYPE === 'series'
? `S${String(SEASON).padStart(2, '0')}E${String(EPISODE).padStart(2, '0')} ` : '';
console.log(JSON.stringify({
streams: [{
name: 'vsembed',
title: `${labelTag}1080p\nvsembed (cloudnestra)`,
url: stream.url,
referer: stream.referer,
quality: '1080p',
behaviorHints: { notWebReady: true },
}],
debug: { ...dbg, referer: stream.referer },
}));
})();