|
|
|
|
|
|
|
|
|
|
| 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 {
|
|
|
| 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;
|
| }
|
| }
|
|
|
|
|
| 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",
|
| });
|
| 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 {
|
|
|
| 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";
|
|
|
|
|
| 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();
|
|
|
|
|
|
|
|
|
| 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 };
|
| },
|
| };
|
|
|