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 = { "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 = 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] }; }, };