| import type { SourceModule, SourceStream } from "./types";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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;
|
| }
|
|
|
|
|
| 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 };
|
| }
|
|
|
|
|
| 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),
|
| });
|
|
|
|
|
| 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;
|
|
|
|
|
| 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;
|
|
|
|
|
| const sep = tokenData.playlist.includes("?") ? "&" : "?";
|
| const masterUrl = `${tokenData.playlist}${sep}token=${tokenData.token}&expires=${tokenData.expires}&h=1`;
|
|
|
|
|
| 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 };
|
| }
|
|
|
|
|
|
|
|
|
| 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] };
|
| },
|
| };
|
|
|