File size: 6,759 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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
// Server-only Insertunit source resolver (api.insertunit.ws). Resolves a TMDB
// id to an IMDB id, scrapes the embed HTML, and extracts the dash/hls streams,
// audio-track names and cc (subtitle) list. Streams live on interkh.com and
// require the insertunit Referer, so they must be proxied.

export interface InsertunitResult {
  dash: string | null;
  dasha: string | null;
  hls: string | null;
  audio: { names: string[]; order: number[] } | null;
  cc: { url: string; name: string }[] | null;
}

const HEADERS: Record<string, string> = {
  accept:
    "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
  "accept-language": "en-GB,en;q=0.9",
  "cache-control": "max-age=0",
  "sec-ch-ua":
    '"Chromium";v="148", "Brave";v="148", "Not/A)Brand";v="99"',
  "sec-ch-ua-mobile": "?0",
  "sec-ch-ua-platform": '"macOS"',
  "sec-fetch-dest": "document",
  "sec-fetch-mode": "navigate",
  "sec-fetch-site": "none",
  "sec-fetch-user": "?1",
  "upgrade-insecure-requests": "1",
};

export const INSERTUNIT_REFERER = "https://api.insertunit.ws/";

function safeJsonLoad<T = any>(s: string): T | null {
  try {
    return JSON.parse(s) as T;
  } catch {
    // strip trailing commas from sloppy JS objects, retry
    const clean = s.replace(/,\s*}/g, "}").replace(/,\s*\]/g, "]");
    try {
      return JSON.parse(clean) as T;
    } catch {
      return null;
    }
  }
}

export class InsertunitScraper {
  private tmdbKey =
    process.env.TMDB_API_KEY || "";

  private async getImdbId(tmdbId: number, isTv: boolean): Promise<string | null> {
    const mt = isTv ? "tv" : "movie";
    try {
      const r = await fetch(
        `https://api.themoviedb.org/3/${mt}/${tmdbId}/external_ids?api_key=${this.tmdbKey}`
      );
      if (!r.ok) return null;
      const d = await r.json();
      return d.imdb_id || null;
    } catch {
      return null;
    }
  }

  // Finds the dynamic token suffix appended to media URLs to bypass 403s.
  private extractDynamicToken(html: string): string {
    const varMatch = html.match(
      /o\[k\]\s*\+=\s*'&'\s*\+\s*([a-zA-Z0-9_]+);/
    );
    if (!varMatch) return "";
    const name = varMatch[1];
    const valMatch = html.match(
      new RegExp(`${name}\\s*=\\s*"([^"]+)"`)
    );
    if (!valMatch) return "";
    return `&${valMatch[1]}`;
  }

  async fetchSources(
    tmdbId: number,
    season = 0,
    episode = 0
  ): Promise<InsertunitResult | null> {
    const isTv = !!(season && episode);
    const imdb = await this.getImdbId(tmdbId, isTv);
    if (!imdb) return null;

    let html: string;
    try {
      const r = await fetch(`https://api.insertunit.ws/embed/imdb/${imdb}`, {
        headers: HEADERS,
        cache: "no-store", // fresh tokens each resolve
      });
      if (!r.ok) return null;
      html = await r.text();
    } catch {
      return null;
    }

    return isTv
      ? this.parseTv(html, season, episode)
      : this.parseMovie(html);
  }

  private parseMovie(html: string): InsertunitResult | null {
    const m = html.match(/source:\s*({[\s\S]*?})/);
    if (!m) return null;
    const src = m[1];
    const tok = this.extractDynamicToken(html);

    const dash = src.match(/dash:\s*"(.*?)"/);
    const dasha = src.match(/dasha:\s*"(.*?)"/);
    const hls = src.match(/hls:\s*"(.*?)"/);
    const audio = src.match(/audio:\s*({[\s\S]*?})/);
    const cc = src.match(/cc:\s*(\[[\s\S]*?\])/);

    return {
      dash: dash ? dash[1] + tok : null,
      dasha: dasha ? dasha[1] + tok : null,
      hls: hls ? hls[1] + tok : null,
      audio: audio ? safeJsonLoad(audio[1]) : null,
      cc: cc ? safeJsonLoad(cc[1]) : null,
    };
  }

  private parseTv(
    html: string,
    season: number,
    episode: number
  ): InsertunitResult | null {
    // Isolate the season block to avoid cross-season episode mismatches.
    const seasonRe = new RegExp(
      `"season":\\s*${season}\\s*,\\s*"blocked"[\\s\\S]*?(?="season":\\s*\\d+\\s*,|$)`
    );
    const sm = html.match(seasonRe);
    if (!sm) return null;
    const seasonHtml = sm[0];

    const epRe = new RegExp(
      `"episode":\\s*"${episode}"[\\s\\S]*?(?="episode":\\s*"\\d+"|$)`
    );
    const em = seasonHtml.match(epRe);
    if (!em) return null;
    const eh = em[0];
    const tok = this.extractDynamicToken(html);

    const dash = eh.match(/"dash":\s*"(.*?)"/);
    const dasha = eh.match(/"dasha":\s*"(.*?)"/);
    const hls = eh.match(/"hls":\s*"(.*?)"/);
    const audio = eh.match(/"audio":\s*({[\s\S]*?})/);
    const cc = eh.match(/"cc":\s*(\[[\s\S]*?\])/);

    return {
      dash: dash ? dash[1] + tok : null,
      dasha: dasha ? dasha[1] + tok : null,
      hls: hls ? hls[1] + tok : null,
      audio: audio ? safeJsonLoad(audio[1]) : null,
      cc: cc ? safeJsonLoad(cc[1]) : null,
    };
  }
}

import type { SourceModule } from "./types";

// Best-effort language code from an Insertunit cc track name.
function guessLang(name: string): string {
  const n = (name || "").toLowerCase();
  if (n.includes("eng") || n.includes("english")) return "en";
  if (n.includes("рус") || n.includes("rus")) return "ru";
  if (n.includes("укр") || n.includes("ukr")) return "uk";
  if (n.includes("span") || n.includes("esp")) return "es";
  if (n.includes("fr")) return "fr";
  if (n.includes("de") || n.includes("ger")) return "de";
  return "";
}

const _insertunit = new InsertunitScraper();

// interkh streams need the insertunit Referer, so both streams and cc subs are
// proxied. Prefer DASH (its <BaseURL>[0] is a real CDN host; the HLS variant
// uses an "x-bc" placeholder host that 410s).
export const insertunit: SourceModule = {
  id: "insertunit",
  name: "Ari",
  label: "Multi-audio · built-in subs",
  active: true,
  rank: 2,
  async fetch(ctx) {
    const r = await _insertunit
      .fetchSources(ctx.tmdbId, ctx.season, ctx.episode)
      .catch(() => null);
    if (!r) return null;
    const ref = INSERTUNIT_REFERER;
    const streams = [];
    if (r.dash) {
      streams.push({
        file: ctx.proxyStream(r.dash, ref),
        label: "auto",
        type: "dash" as const,
      });
    } else if (r.hls) {
      streams.push({
        file: ctx.proxyStream(r.hls, ref),
        label: "auto",
        type: "hls" as const,
      });
    }
    if (!streams.length) return null;
    const subtitles = (r.cc || []).map((c) => ({
      url: ctx.proxySub(c.url, ref),
      display: c.name,
      language: guessLang(c.name),
      source: "Ari",
    }));
    return { streams, subtitles };
  },
};