File size: 4,389 Bytes
3e8e34c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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] };
  },
};