OverviewFilesCommitsIssuesDocsReleasesPeers

stremio-vsembed / node_helper / vidcore_resolve.mjs

146 lines · javascript ref:
#!/usr/bin/env node
// vidcore resolver (additive, lazy). vidcore is flixlab's PRIMARY server, so we
// drive the vidcore player DIRECTLY and sniff the m3u8 — no server switching.
// imdb --TMDB find--> tmdb -> vidcore.net/{movie|tv}/<tmdb>[/s/e]?autoPlay=true
//   -> sniff moon.ironwallnet.net .../playlist.m3u8 (referer https://vidcore.net/)
//
// 2026-07-27: was driving flixlab's SPA (`flixlab.org/#/watch/tv/<tmdb>/<s>/<e>`).
// That route DOES NOT EXIST for series — flixlab redirects it to `#/`, the home
// page, so the vidcore iframe never mounted and EVERY tv episode reported
// "title may be unavailable on vidcore". (Movies worked: `#/watch/movie/<id>` is
// a real route.) flixlab itself embeds `vidcore.net/{movie|tv}/<tmdb>[/s/e]`, so
// we now open that iframe URL directly with Referer: flixlab.org — correct for
// both types, parameterized by season/episode, and ~3s instead of a 60s timeout.
// Usage: node vidcore_resolve.mjs <movie|series> <tt-id|tmdb:NNN> [season] [episode]
//   vidcore is TMDB-keyed, so a `tmdb:NNN` id is passed straight through and the
//   imdb->tmdb find is skipped. That is what the flixlab-native catalog (fxl:
//   ids) and the fmovies-catalog bridge use — neither has an imdb id to offer.
// Output (last stdout line): {streams:[{url,referer,quality,name,title}]}
//                          | {error, reason:'absent'|'timeout'|'tmdb-error'|'usage'}
// `reason` exists because the caller has to tell "vidcore does not have this"
// apart from "we gave up first" -- it records the former and hides the row for
// a week. Only 'absent' is a positive finding; everything else stays retryable.
import { chromium } from 'patchright';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
const TMDB_KEY = 'b70756b7083d9ee60f849d82d94a0d80';
const log = (...a) => console.error('[vc]', ...a);

// Phase timings, reported on EVERY outcome. Measured 2026-08-10: launch and page
// load are steady (~0.7s / ~1.2s) and it is vidcore's player that varies, 5.3s to
// 15s+. Reporting them is what lets the caller tell a slow SITE from a slow US --
// and comparing total_ms against the caller's own wall clock isolates anything
// spent outside node (xvfb-run, process spawn), which is where a 57s resolve that
// should have aborted at ~24s must have gone.
const T0 = Date.now();
let mLaunch = null, mGoto = null;
const timing = () => {
  const total = Date.now() - T0;
  return {
    launch_ms: mLaunch,
    goto_ms: mLaunch != null && mGoto != null ? mGoto - mLaunch : null,
    poll_ms: mGoto != null ? total - mGoto : null,
    total_ms: total,
  };
};
const MEDIA = /\.m3u8(\?|$)|\/playlist|\/master/i;
const NOISE = /youtube|googlevideo|ytimg|chaturbate|mmcdn|canAutoplay/i;

const TYPE = process.argv[2] === 'series' ? 'series' : 'movie';
const IDENT = process.argv[3];
const SEASON = process.argv[4] ? parseInt(process.argv[4],10) : null;
const EPISODE = process.argv[5] ? parseInt(process.argv[5],10) : null;
const die = (e,c=1,reason='usage') => { console.log(JSON.stringify({error:e,reason,timing:timing()})); process.exit(c); };
if (!IDENT || !/^(tt\d+|tmdb:\d+)$/.test(IDENT)) die('usage: <movie|series> <tt|tmdb:NNN> [s] [e]');
if (TYPE==='series' && (!SEASON||!EPISODE)) die('series requires season and episode');

let tmdb;
if (IDENT.startsWith('tmdb:')) {
  tmdb = IDENT.slice(5);                     // already TMDB-keyed — nothing to look up
} else try {
  const r = await fetch(`https://api.themoviedb.org/3/find/${IDENT}?external_source=imdb_id&api_key=${TMDB_KEY}`);
  const d = await r.json();
  const hit = TYPE==='series' ? (d.tv_results||[])[0] : (d.movie_results||[])[0];
  tmdb = (hit || (d.movie_results||[])[0] || (d.tv_results||[])[0])?.id;
  if (!tmdb) die('no tmdb match for '+IDENT, 3, 'absent');   // positively nothing to play
} catch (e) { die('tmdb lookup failed: '+e.message, 2, 'tmdb-error'); }  // our side; retry
const WATCH = TYPE==='series'
  ? `https://vidcore.net/tv/${tmdb}/${SEASON}/${EPISODE}?autoPlay=true&nextButton=false`
  : `https://vidcore.net/movie/${tmdb}?autoPlay=true&nextButton=false`;

// A FIXED profile dir made concurrent resolves fatal: Kodi issues GET+HEAD+GET
// for one stream url, each spawning this helper, and every launch after the
// first died with "Opening in existing browser session ... profile is already in
// use", exiting before any JSON was printed. Give each run its own profile.
const PROFILE = fs.mkdtempSync(path.join(os.tmpdir(), 'vc_profile_'));
// die() and any crash exit before the explicit cleanup below, so hook exit
// too -- otherwise every unavailable title leaks a profile directory.
process.on('exit', () => { try { fs.rmSync(PROFILE, { recursive:true, force:true }); } catch {} });
const ctx = await chromium.launchPersistentContext(PROFILE, { headless:false, args:['--no-sandbox'], viewport:{width:1280,height:720}, locale:'en-US' });
mLaunch = Date.now() - T0;
ctx.on('page', p => setTimeout(()=>{ if(!p.isClosed() && !/vidcore/.test(p.url())) p.close().catch(()=>{}); },500));
const media = new Map();
ctx.on('request', r => { const u=r.url(); if (MEDIA.test(u) && !NOISE.test(u)) media.set(u,(r.headers()||{}).referer||null); });
const page = ctx.pages()[0] || await ctx.newPage();
// vidcore is normally framed BY flixlab; present that as the referer.
await page.setExtraHTTPHeaders({ Referer:'https://flixlab.org/' });
log('tmdb', tmdb, '->', WATCH);
await page.goto(WATCH, { waitUntil:'domcontentloaded', timeout:60000 }).catch(e=>log('warn',e.message));
mGoto = Date.now() - T0;
log(`launch=${mLaunch}ms goto=${mGoto - mLaunch}ms`);
// How long to wait for a stream to surface before calling it unavailable.
// 20 rounds (60s) meant every title vidcore LACKS cost 65s, which wedged Kodi's
// plugin queue -- the UI sits on "updating in progress" behind it. Nobody is
// listening that long anyway: Jacktook's stremio_timeout is 25s, so it has
// already given up. Every successful resolve measured has landed in 3-8.3s, so
// 6 rounds (18s, plus the 2s tail below) keeps a ~2.5x margin over the slowest
// observed hit while capping a miss at ~20s, under the client timeout.
const POLL_ROUNDS = 6;
// The ONLY sound evidence that vidcore lacks a title: it says so. Measured
// 2026-08-10 on X-Men '97 S9E99 (a season/episode that does not exist) --
// vidcore holds "FETCHING YOUR CONTENT..." for ~18s and only then renders
// "(404) OOPS! SOMETHING WENT WRONG. PLEASE ...". A title it HAS flips to
// "Now Playing S2:E8 ..." by t=6s.
//
// Note what is NOT evidence: still sitting on the placeholder at the deadline.
// That looks identical to a slow cold start (S2E7 took 57s that morning, and
// resolved fine 9s later), and treating it as absence is exactly what hid a
// playable S2E8 for a week. Anything short of the explicit error is a timeout.
const NOT_FOUND = /\(404\)|SOMETHING WENT WRONG/i;
const bodyText = () => page.evaluate(
  ()=>(document.body?.innerText||'').replace(/\s+/g,' ').slice(0,40)).catch(()=>'');
let sawNotFound = false;
for (let i=0;i<POLL_ROUNDS && media.size===0;i++){
  await page.waitForTimeout(3000);
  // autoPlay usually suffices; nudge the player if it is waiting on a gesture.
  const v = await page.$('video').catch(()=>null);
  if (v) await v.click({timeout:1200,force:true}).catch(()=>{});
  const st = await bodyText();
  if (NOT_FOUND.test(st)) sawNotFound = true;
  log(`t=${(i+1)*3}s "${st}" media=${media.size}`);
}
await page.waitForTimeout(2000);
// vidcore's 404 lands at ~18s, i.e. right ON the poll ceiling, so read the page
// once more after the tail. Costs no extra wall time and is usually the read
// that catches it -- without it a genuine absence almost always reads as a
// timeout and never stops being re-probed.
if (!media.size && !sawNotFound) {
  const st = await bodyText();
  if (NOT_FOUND.test(st)) sawNotFound = true;
  log(`t=20s "${st}" media=${media.size}`);
}
await ctx.close();
// Each run gets its own profile, so each run must remove it or /tmp grows
// without bound (a Chromium profile is a few MB per resolve).
fs.rmSync(PROFILE, { recursive: true, force: true });
if (!media.size) {
  if (sawNotFound) die('vidcore reports 404 for this title', 5, 'absent');
  // No stream AND no 404 => we gave up first. Reporting that as absence is what
  // hid a perfectly playable X-Men '97 S2E8 for a week (2026-08-10).
  die('no m3u8 within the poll window', 5, 'timeout');
}
const url = [...media.keys()].find(u=>/master|playlist/i.test(u)) || [...media.keys()][0];
const labelTag = TYPE==='series' ? `S${String(SEASON).padStart(2,'0')}E${String(EPISODE).padStart(2,'0')} ` : '';
console.log(JSON.stringify({ streams:[{ url, referer:'https://vidcore.net/', quality:'1080p', name:'vidcore', title:`${labelTag}1080p\nvidcore (flixlab)`, behaviorHints:{notWebReady:true} }], debug:{tmdb}, timing:timing() }));