miruro / lib /scraper /vixsrc.ts
mwask's picture
Upload 45 files
3e8e34c verified
Raw
History Blame Contribute Delete
4.39 kB
import type { SourceModule, SourceStream } from "./types";
// Server-only VixSrc resolver (vixsrc.to). Hits the JSON API for a TMDB title to
// get the embed iframe path, scrapes a short-lived token/expires/playlist out of
// the embed HTML, then builds the signed master playlist URL. The master playlist
// is parsed for the best resolution and its CDN needs a Referer, so it's wrapped
// in our /api/stream proxy for playback.
const BASE_URL = "https://vixsrc.to";
const HEADERS: Record<string, string> = {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150 Safari/537.36",
Accept: "application/json, text/javascript, */*; q=0.01",
"Accept-Language": "en-US,en;q=0.9",
Referer: BASE_URL,
Origin: BASE_URL,
};
interface TokenData {
token: string;
expires: string;
playlist: string;
}
// JS Date.now() works in ms; VixSrc's `expires` is a unix-seconds string.
function isTokenExpired(expires: string): boolean {
const expiration = parseInt(expires, 10);
if (!Number.isFinite(expiration)) return true;
return expiration * 1000 - 60000 < Date.now();
}
function extractTokenData(html: string): TokenData | null {
const token = html.match(/token["']\s*:\s*["']([^"']+)/)?.[1];
const expires = html.match(/expires["']\s*:\s*["']([^"']+)/)?.[1];
const playlist = html.match(/url\s*:\s*["']([^"']+)/)?.[1];
if (!token || !expires || !playlist) return null;
if (isTokenExpired(expires)) return null;
return { token, expires, playlist };
}
// Pull the highest RESOLUTION=NxH variant out of the HLS master manifest.
function bestResolution(manifest: string): number | null {
let best: number | null = null;
const re = /#EXT-X-STREAM-INF:[^\n]*RESOLUTION=\d+x(\d+)/g;
let m: RegExpExecArray | null;
while ((m = re.exec(manifest))) {
const h = parseInt(m[1], 10);
if (Number.isFinite(h) && (best === null || h > best)) best = h;
}
return best;
}
async function resolveMaster(
tmdbId: number,
season: number,
episode: number
): Promise<{ masterUrl: string; referer: string; height: number | null } | null> {
const isMovie = !season || !episode;
const apiUrl = isMovie
? `${BASE_URL}/api/movie/${tmdbId}`
: `${BASE_URL}/api/tv/${tmdbId}/${season}/${episode}`;
const get = (url: string, headers: Record<string, string> = HEADERS) =>
fetch(url, {
headers,
cache: "no-store",
signal: AbortSignal.timeout(8000),
});
// 1. API → embed iframe path.
const apiRes = await get(apiUrl);
if (!apiRes.ok) return null;
const apiData = await apiRes.json().catch(() => null);
const sublink: string | undefined = apiData?.src;
if (!sublink) return null;
// 2. Embed HTML → token/expires/playlist.
const embedUrl = BASE_URL + sublink;
const htmlRes = await get(embedUrl);
if (!htmlRes.ok) return null;
const tokenData = extractTokenData(await htmlRes.text());
if (!tokenData) return null;
// 3. Build the signed master playlist URL.
const sep = tokenData.playlist.includes("?") ? "&" : "?";
const masterUrl = `${tokenData.playlist}${sep}token=${tokenData.token}&expires=${tokenData.expires}&h=1`;
// 4. Fetch the master to confirm it resolves + pick the best resolution.
const playlistRes = await get(masterUrl, { ...HEADERS, Referer: apiUrl });
if (!playlistRes.ok) return null;
const height = bestResolution(await playlistRes.text());
if (height === null) return null;
return { masterUrl, referer: apiUrl, height };
}
// VixSrc's CDN requires the Referer used to mint the playlist, so playback goes
// through our /api/stream proxy. Subtitles are broken upstream, so we lean on the
// route's default Wyzie/OpenSubtitles layer instead.
export const vixsrc: SourceModule = {
id: "vixsrc",
name: "Noor",
label: "HLS · proxied",
active: true,
rank: 8,
async fetch(ctx) {
const resolved = await resolveMaster(
ctx.tmdbId,
ctx.season,
ctx.episode
).catch(() => null);
if (!resolved) return null;
const stream: SourceStream = {
file: ctx.proxyStream(resolved.masterUrl, resolved.referer),
label: resolved.height ? String(resolved.height) : "auto",
type: "hls",
};
return { streams: [stream] };
},
};