create SDK

This commit is contained in:
Gavin Fox
2026-07-27 04:28:33 -07:00
commit 1e11642aad
47 changed files with 4319 additions and 0 deletions

519
src/config.js Normal file
View File

@@ -0,0 +1,519 @@
export const SOURCES = [
{
key: '1embed',
label: '1Embed',
sourceFile: '1embed',
proxyParam: '1e',
timeout: 20000,
jitter: 500,
retries: 2,
multiUrl: true,
},
{
key: '123anime',
label: '123Anime',
sourceFile: '123anime',
proxyParam: 'a1',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: false,
skipProxy: true,
cdnHeaders: [{
pattern: /hlsx\d+cdn\.|burntburst\d+\.store|echovideo\.ru/i,
headers: {
Referer: 'https://play2.echovideo.ru/',
Origin: 'https://play2.echovideo.ru',
},
},],
},
{
key: 'anihq-sub',
label: 'AniHQ (Sub)',
sourceFile: 'anihq',
proxyParam: 'ahsub',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
skipProxy: true
},
{
key: 'anihq-dub',
label: 'AniHQ (Dub)',
sourceFile: 'anihq',
proxyParam: 'ahdub',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
skipProxy: true
},
{
key: 'anineko-sub',
sourceFile: 'anineko',
label: 'AniNeko (Sub)',
proxyParam: 'anksub',
timeout: 25000,
jitter: 500,
retries: 2
},
{
key: 'anineko-dub',
sourceFile: 'anineko',
label: 'AniNeko (Dub)',
proxyParam: 'ankdub',
timeout: 25000,
jitter: 500,
retries: 2
},
{
key: 'anipm-sub',
sourceFile: 'anipm',
label: 'AniPM (Sub)',
proxyParam: 'apsub',
timeout: 25000,
jitter: 500,
retries: 2,
skipProxy: true,
skipVerify: true,
multiUrl: true,
verifyHeaders: {
Referer: 'https://ani.pm/',
Origin: 'https://ani.pm',
},
},
{
key: 'anipm-dub',
sourceFile: 'anipm',
label: 'AniPM (Dub)',
proxyParam: 'apdub',
timeout: 25000,
jitter: 500,
retries: 2,
skipProxy: true,
skipVerify: true,
multiUrl: true,
verifyHeaders: {
Referer: 'https://ani.pm/',
Origin: 'https://ani.pm',
},
},
{
key: 'flaxmovies',
label: 'FlaxMovies',
sourceFile: 'flaxmovies',
proxyParam: 'fx',
timeout: 20000,
jitter: 500,
retries: 2,
multiUrl: true,
disabled: true, // Temporarily disabled because their site is down, all streams are returning 403's
cdnHeaders: [{
pattern: /flix2watch\.pro/i,
headers: {
Referer: 'https://flaxmovies.xyz/',
Origin: 'https://flaxmovies.xyz',
},
},],
},
{
key: 'fsharetv',
label: 'FShareTV',
sourceFile: 'fsharetv',
proxyParam: 'fs',
timeout: 25000,
jitter: 600,
retries: 2,
multiUrl: false,
verifyHeaders: {
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
Referer: 'https://fsharetv.cc',
},
},
{
key: 'goated',
label: 'Goated',
sourceFile: 'goated',
proxyParam: 'gt',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
skipProxy: true,
},
{
key: 'fsonic',
label: 'Fsonic',
sourceFile: 'fsonic',
proxyParam: 'fn',
timeout: 35000,
jitter: 600,
retries: 1,
multiUrl: true,
},
{
key: 'fsonline',
label: 'FSOnline',
sourceFile: 'fsonline',
proxyParam: 'fo',
timeout: 20000,
retries: 1,
jitter: 0,
multiUrl: true,
skipProxy: true,
},
{
key: 'hexa',
label: 'Hexa',
sourceFile: 'hexa',
proxyParam: 'hx',
timeout: 20000,
jitter: 500,
retries: 2,
},
{
key: 'kisskh',
label: 'KissKH',
sourceFile: 'kisskh',
proxyParam: 'kk',
timeout: 30000,
jitter: 500,
retries: 1,
},
{
key: 'lookmovie',
label: 'LookMovie',
sourceFile: 'lookmovie',
proxyParam: 'lm',
timeout: 20000,
jitter: 500,
retries: 2,
multiUrl: true,
verifyHeaders: {
'Accept-Language': 'en-US,en;q=0.9',
},
},
{
key: 'mapple',
label: 'Mapple',
sourceFile: 'mapple',
proxyParam: 'mp',
timeout: 15000,
retries: 1,
jitter: 0
},
{
key: 'meowtv',
label: 'MeowTV',
sourceFile: 'meowtv',
proxyParam: 'mt',
timeout: 20000,
jitter: 500,
retries: 2,
multiUrl: true,
skipProxy: true
},
{
key: 'nebula',
label: 'Nebula',
sourceFile: 'nebula',
proxyParam: 'nb',
timeout: 25000,
jitter: 500,
retries: 1,
multiUrl: true,
skipProxy: true,
},
{
key: 'nekowatch-sub',
sourceFile: 'nekowatch',
label: 'NekoWatch (Sub)',
proxyParam: 'nwsub',
timeout: 30000,
jitter: 500,
retries: 2
},
{
key: 'nekowatch-dub',
sourceFile: 'nekowatch',
label: 'NekoWatch (Dub)',
proxyParam: 'nwdub',
timeout: 30000,
jitter: 500,
retries: 2
},
{
key: 'opstream',
label: 'OpStream',
sourceFile: 'opstream',
proxyParam: 'op',
timeout: 35000,
jitter: 500,
retries: 2,
multiUrl: true,
skipProxy: true,
verifyHeaders: {
Referer: 'https://opstream.fun/',
Origin: 'https://opstream.fun',
},
},
{
key: 'peachify',
label: 'Peachify',
sourceFile: 'peachify',
proxyParam: 'pf',
timeout: 30000,
jitter: 500,
retries: 1,
multiUrl: true
},
{
key: 'pengu',
label: 'Pengu',
sourceFile: 'pengu',
proxyParam: 'pg',
timeout: 25000,
jitter: 500,
retries: 1,
multiUrl: true,
skipProxy: true,
disabled: true, // Temporarily disabled because they now require google auth
},
{
key: 'purstream',
sourceFile: 'purstream',
label: 'Purstream',
proxyParam: 'ps',
timeout: 20000,
jitter: 500,
retries: 2,
},
{
key: 'vidapi',
label: 'VidAPI',
sourceFile: 'vidapi',
proxyParam: 'va',
timeout: 25000,
jitter: 500,
retries: 1,
multiUrl: true
},
{
key: 'vidcore',
sourceFile: 'vidcore',
label: 'Vidcore',
proxyParam: 'vc',
timeout: 30000,
jitter: 500,
retries: 2,
sourcesTimeout: 10000,
},
{
key: 'videasy',
label: 'Videasy',
sourceFile: 'videasy',
proxyParam: 'vy',
timeout: 40000,
sourcesTimeout: 10000,
jitter: 900,
retries: 3,
multiUrl: true,
verifyHeaders: {
Accept: 'application/json, /; q=0.01',
Referer: 'https://player.videasy.net/',
Origin: 'https://player.videasy.net',
},
},
{
key: 'vidfast',
label: 'Vidfast',
sourceFile: 'vidfast',
proxyParam: 'vf',
timeout: 35000,
jitter: 500,
retries: 1,
},
{
key: 'vidlink',
label: 'Vidlink',
sourceFile: 'vidlink',
proxyParam: 'vl',
timeout: 20000,
jitter: 500,
retries: 2,
skipProxy: true,
multiUrl: true,
disabled: true, // Temporarily disabled because their site is down, all streams are returning 403's
},
{
key: 'vidnest',
label: 'VidNest',
sourceFile: 'vidnest',
proxyParam: 'vdn',
timeout: 20000,
retries: 1,
jitter: 0,
multiUrl: true,
},
{
key: 'vidnest-sub',
label: 'VidNest (Sub)',
sourceFile: 'vidnest',
proxyParam: 'vdn',
timeout: 20000,
retries: 1,
jitter: 0,
multiUrl: true,
skipProxy: true,
},
{
key: 'vidnest-dub',
label: 'VidNest (Dub)',
sourceFile: 'vidnest',
proxyParam: 'vdn',
timeout: 20000,
retries: 1,
jitter: 0,
multiUrl: true,
skipProxy: true,
},
{
key: 'vidrift',
label: 'VidRift',
sourceFile: 'vidrift',
proxyParam: 'vr',
timeout: 20000,
jitter: 500,
retries: 2,
multiUrl: true,
skipProxy: true
},
{
key: 'vidrock',
label: 'VidRock',
sourceFile: 'vidrock',
proxyParam: 'vr',
timeout: 20000,
jitter: 800,
retries: 3,
multiUrl: true,
cdnHeaders: [{
pattern: /./,
headers: {
Accept: '/',
'Accept-Language': 'en-US,en;q=0.9',
Referer: 'https://vidrock.ru/',
Origin: 'https://vidrock.ru',
},
},],
},
{
key: 'vidsync',
label: 'VidSync',
sourceFile: 'vidsync',
proxyParam: 'vs',
timeout: 20000,
retries: 2,
multiUrl: true,
disabled: true, // Temporarily disabled due to enc dec issues
},
{
key: 'vidup',
label: 'VidUp',
sourceFile: 'vidup',
proxyParam: 'vu',
timeout: 35000,
jitter: 500,
retries: 1,
multiUrl: true
},
{
key: 'vidzee',
label: 'VidZee',
sourceFile: 'vidzee',
proxyParam: 'vz',
timeout: 20000,
sourcesTimeout: 10000,
jitter: 400,
retries: 3,
disabled: true, // Temporarily disabled because their site is down, all streams are returning 403's
verifyHeaders: {
Accept: '/',
'Accept-Language': 'en-US,en;q=0.9',
Referer: 'https://player.vidzee.wtf',
Origin: 'https://player.vidzee.wtf',
},
},
{
key: 'vixsrc',
label: 'VixSrc',
sourceFile: 'vixsrc',
proxyParam: 'vx',
timeout: 35000,
jitter: 0,
retries: 2,
multiUrl: false,
skipProxy: true,
verifyHeaders: {
Accept: 'application/json, text/javascript, /; q=0.01',
'Accept-Language': 'en-US,en;q=0.9',
Referer: 'https://vixsrc.to/',
Origin: 'https://vixsrc.to',
},
},
{
key: 'xpass',
sourceFile: 'xpass',
label: 'XPass',
proxyParam: 'xp',
timeout: 20000,
jitter: 500,
retries: 2,
skipProxy: true,
},
];
export const HEALTH_PROBE_ID = '155';
export const SOURCE_MAP = Object.fromEntries(SOURCES.map(s => [s.key, s]));
export default SOURCES;

View File

@@ -0,0 +1,8 @@
import { getDownloads, getDownloadsTv } from "./sources/trendimovies";
export async function fetchDownloads(id, s = null, e = null) {
const res = s != null && e != null
? await getDownloadsTv(id, s, e)
: await getDownloads(id);
return res;
}

View File

@@ -0,0 +1,63 @@
const BASE = "https://trendimovies.com";
const HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Referer": "https://trendimovies.com/",
};
async function fetchPage(path) {
const res = await fetch(`${BASE}${path}`, { headers: HEADERS });
if (!res.ok) throw new Error(`trendimovies ${res.status}`);
return res.text();
}
function parseLinks(html) {
const m = html.match(/DownloadSection[^>]*props="([^"]+)"/s) ||
html.match(/props="([^"]+)"[^>]*ssr[^>]*client="load"[^>]*opts="[^"]*DownloadSection/s);
if (!m) {
const alt = html.match(/"DownloadSection"/);
if (!alt) return [];
const propsMatch = html.match(/props="({[^"]*DownloadSection[^"]*})"/s);
if (!propsMatch) return [];
return extractLinks(propsMatch[1].replace(/"/g, '"').replace(/'/g, "'"));
}
return extractLinks(m[1].replace(/"/g, '"').replace(/'/g, "'"));
}
function extractLinks(propsJson) {
try {
const parsed = JSON.parse(propsJson);
const rawLinks = parsed?.links?.[1];
if (!Array.isArray(rawLinks)) return [];
return rawLinks
.map(item => item?.[1])
.filter(l => l && typeof l === "object")
.map(l => ({
url: unwrap(l.url),
quality: unwrap(l.quality) || "HD",
size: unwrap(l.file_size) || null,
type: "mkv",
active: unwrap(l.is_active) !== false,
}))
.filter(l => l.url && l.active);
} catch {
return [];
}
}
function unwrap(v) {
if (Array.isArray(v) && v.length === 2 && (v[0] === 0 || v[0] === 1)) return v[1];
return v;
}
export async function getDownloads(tmdbId) {
const html = await fetchPage(`/movie/${tmdbId}`);
return parseLinks(html);
}
export async function getDownloadsTv(tmdbId, season, episode) {
const html = await fetchPage(`/tv/${tmdbId}/season/${season}/episode/${episode}`);
return parseLinks(html);
}

38
src/health.js Normal file
View File

@@ -0,0 +1,38 @@
import { HEALTH_PROBE_ID } from "./config.js";
const withTimeout = (promise, ms) => Promise.race([promise, new Promise(r => setTimeout(() => r(null), ms))]);
async function withRetry(fn, attempts = 2, delay = 300) {
for (let i = 0; i < attempts; i++) {
try {
const result = await fn();
if (result != null) return result;
} catch (err) {
if (i === attempts - 1) throw err;
await new Promise(r => setTimeout(r, delay + Math.random() * delay * 0.5));
}
}
return null;
}
export async function probeSource(tmdbApiKey, cfg, mod) {
const audio = /dub$/.test(cfg.key) ? 'dub' : 'sub';
const streamArgs = {
id: HEALTH_PROBE_ID,
s: null,
e: null,
clientIP: null,
absoluteBase: null,
audio,
tmdbApiKey,
config: cfg,
};
const t = Date.now();
const result = await withTimeout(
withRetry(() => mod.getStream(streamArgs), cfg.retries, 300),
cfg.timeout
);
const firstUrl = result?.url ?? result?.allUrls?.[0]?.url;
return { ok: !!(typeof firstUrl === 'string' && firstUrl.startsWith('http')), ms: Date.now() - t };
}

65
src/sdk.js Normal file
View File

@@ -0,0 +1,65 @@
import SOURCES, { SOURCE_MAP } from "./config.js";
import { fetchDownloads } from "./downloads/downloads.js";
import { probeSource } from "./health.js";
import { fetchSubtitles, getSubPathsMovie, getSubPathsTv } from "./subtitles.js";
import { createStreamArgs } from "./utils/helpers.js";
const ALL_SOURCE_MODULES = Object.fromEntries(
await Promise.all(SOURCES.map(async cfg => [cfg.key, await import(`./sources/${cfg.sourceFile}.js`)]))
);
const SOURCE_MODULES = Object.fromEntries(
Object.entries(ALL_SOURCE_MODULES).filter(([key]) => !SOURCE_MAP[key]?.disabled)
);
export default class VylaSDK {
constructor({ tmdbApiKey } = {}) {
this.tmdbApiKey = tmdbApiKey;
}
getSources(excludeDisabled = false) {
if (excludeDisabled) {
return SOURCES.filter(cfg => !cfg.disabled);
}
return SOURCES;
}
async probeAllSources() {
const active = SOURCES.filter(cfg => !cfg.disabled);
const results = await Promise.allSettled(
active.map(async cfg => await probeSource(cfg, SOURCE_MODULES[cfg.key]))
);
const sources = Object.fromEntries(
active.map((cfg, i) => [
cfg.key,
results[i].status === 'fulfilled' ? results[i].value : { ok: false, ms: null },
])
);
return sources;
}
async probeSource(key) {
const cfg = SOURCES.find(cfg => cfg.key === key);
if (!cfg) throw new Error(`Source with key "${key}" not found`);
const mod = SOURCE_MODULES[key];
if (!mod) throw new Error(`Source module for key "${key}" not found`);
return await probeSource(this.tmdbApiKey, cfg, mod);
}
async getStream(key, id, s = null, e = null, clientIP = null) {
const cfg = SOURCES.find(cfg => cfg.key === key);
if (!cfg) throw new Error(`Source with key "${key}" not found`);
const mod = SOURCE_MODULES[key];
if (!mod) throw new Error(`Source module for key "${key}" not found`);
const streamArgs = await createStreamArgs(cfg, this.tmdbApiKey, id, s, e, clientIP);
return await mod.getStream(streamArgs);
}
async getSubtitles(id, s = null, e = null) {
const paths = s != null && e != null
? await getSubPathsTv(id, s, e)
: await getSubPathsMovie(id);
return await fetchSubtitles(paths);
}
async getDownloads(id, s = null, e = null) {
return await fetchDownloads(id, s, e);
}
}

57
src/sources/123anime.js Normal file
View File

@@ -0,0 +1,57 @@
import { getTmdbInfo, fetchJson, USER_AGENT } from '../utils/helpers.js';
async function resolveSlug(id, s, e, info) {
const titles = info?.titles || [];
const keywords = [];
for (const title of titles) {
const t = title.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim();
if (!keywords.includes(t)) keywords.push(t);
const words = title.split(/[\s:]+/).filter(w => w.length > 2);
if (words.length >= 2) {
const short = words.slice(0, 3).join(' ').toLowerCase();
if (!keywords.includes(short)) keywords.push(short);
}
}
const slugCandidates = [];
for (const keyword of keywords) {
try {
const data = await fetchJson(`https://123animehub.cc/ajax/film/search?keyword=${encodeURIComponent(keyword)}&_=${Date.now()}`, { headers: { 'X-Requested-With': 'XMLHttpRequest', 'Referer': 'https://123animehub.cc', 'Accept': 'application/json', 'User-Agent': USER_AGENT }, signal: AbortSignal.timeout(8000) });
let html = data.html || '', idx = 0;
while ((idx = html.indexOf('href="/anime/', idx)) !== -1) {
const start = idx + 13;
const end = html.indexOf('"', start);
const slug = html.slice(start, end);
if (!slugCandidates.includes(slug)) slugCandidates.push(slug);
idx = end;
}
} catch { }
if (slugCandidates.length) break;
}
for (const slug of slugCandidates) {
try {
const data = await fetchJson(`https://123animehub.cc/ajax/episode/info?epr=${encodeURIComponent(s ? `${slug}/${s}/${e}` : `${slug}/1`)}&ts=1&_=${Date.now()}`, { headers: { 'Referer': `https://123animehub.cc/anime/${slug}`, 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/javascript, */*; q=0.01', 'User-Agent': USER_AGENT }, signal: AbortSignal.timeout(10000) });
if (data?.target) return data.target;
} catch { }
}
return null;
}
export async function getStream({ id, s, e }) {
if (!s) return null;
const info = await getTmdbInfo(id, 'tv', s);
if (!info.isAnime) return null;
const target = await resolveSlug(id, s, e, info);
if (!target) return null;
const embedMatch = target.match(/\/embed-[^/]+\/([A-Za-z0-9+/=]+)$/);
if (!embedMatch) return null;
const origin = new URL(target).origin;
try {
const embedRes = await fetch(target, { headers: { 'User-Agent': USER_AGENT }, signal: AbortSignal.timeout(10000) });
if (!embedRes.ok) return null;
const setCookie = embedRes.headers.get('set-cookie');
const data = await fetchJson(`${origin}/hs/getSources?id=${embedMatch[1]}`, { headers: { 'Referer': target, 'Accept': '*/*', 'User-Agent': USER_AGENT, ...(setCookie ? { 'Cookie': setCookie.split(';')[0].trim() } : {}) }, signal: AbortSignal.timeout(10000) });
const src = typeof data.sources === 'string' && data.sources.length ? data.sources : (Array.isArray(data.sources) && data.sources.length ? (data.sources[0].file || data.sources[0].src || data.sources[0].url) : null);
if (src) return { url: src, headers: { 'Referer': 'https://play2.echovideo.ru/', 'Origin': 'https://play2.echovideo.ru' } };
} catch { }
return null;
}

92
src/sources/1embed.js Normal file
View File

@@ -0,0 +1,92 @@
import { USER_AGENT } from '../utils/helpers.js';
const BASE_URL = "https://1embed.cc";
const PROVIDERS = [
{ id: "sdev", name: "1Embed - SDev" },
{ id: "xpa", name: "1Embed - XPass" },
{ id: "vnes", name: "1Embed - VidNest" },
{ id: "pro", name: "1Embed - Pro" },
{ id: "pur", name: "1Embed - Purstream" },
{ id: "czo", name: "1Embed - Cinezo" },
{ id: "fas", name: "1Embed - VidFast" }
];
let cachedToken = null;
let tokenExpiresAt = 0;
async function getToken() {
const now = Math.floor(Date.now() / 1000);
if (cachedToken && tokenExpiresAt > now + 10) return cachedToken;
try {
const res = await fetch(`${BASE_URL}/api/token`, { headers: { "Referer": `${BASE_URL}/`, "User-Agent": USER_AGENT, "Accept": "application/json" }, signal: AbortSignal.timeout(10000) });
if (!res.ok) return null;
const data = await res.json();
cachedToken = data.token;
tokenExpiresAt = data.expiresAt;
return cachedToken;
} catch { return null; }
}
async function fetchProviderStream(provider, type, id, s, e, token) {
try {
const referer = type === "movie" ? `${BASE_URL}/embed/movie/${id}` : `${BASE_URL}/embed/tv/${id}/${s}/${e}`;
const payload = { type, id: String(id), provider: provider.id };
if (type === "tv") { payload.season = Number(s); payload.episode = Number(e); }
const res = await fetch(`${BASE_URL}/api/sources`, {
method: "POST",
headers: { "User-Agent": USER_AGENT, "Referer": referer, "Origin": BASE_URL, "Accept": "application/json", "Content-Type": "application/json", "x-bcine-key": token, "X-Requested-With": "XMLHttpRequest" },
body: JSON.stringify(payload),
signal: AbortSignal.timeout(15000)
});
if (!res.ok) return null;
const data = await res.json();
let rawSources = [];
if (Array.isArray(data.servers)) rawSources.push(...data.servers);
if (data.providerResults?.[provider.id]?.sources) rawSources.push(...data.providerResults[provider.id].sources);
if (data.providerResults?.[provider.id] && Array.isArray(data.providerResults[provider.id])) rawSources.push(...data.providerResults[provider.id]);
if (!rawSources.length) return null;
const results = [];
for (const src of rawSources) {
if (typeof src !== 'object' || !src) continue;
let streamUrl = src.url || src.file || src.link;
if (typeof streamUrl !== 'string') continue;
if (streamUrl.startsWith('/')) streamUrl = `${BASE_URL}${streamUrl}`;
const isWorker = streamUrl.includes('omena-puu') || streamUrl.includes('nocach') || streamUrl.includes('?p=');
const isHls = src.type === 'hls' || streamUrl.includes('m3u8') || isWorker;
if (isHls && !streamUrl.includes('m3u8')) streamUrl += streamUrl.includes('?') ? '&format=.m3u8' : '?format=.m3u8';
const srcHeaders = (src.headers && typeof src.headers === 'object') ? src.headers : {};
results.push({
url: streamUrl,
server: provider.name,
quality: src.quality || src.label || src.title || "Auto",
type: isHls ? "hls" : "mp4",
headers: { "User-Agent": USER_AGENT, "Referer": referer, "Origin": BASE_URL, ...srcHeaders },
});
}
return results;
} catch {
return null;
}
}
export async function getStream(args) {
const { id, s, e, server } = args;
const type = s != null && e != null ? "tv" : "movie";
const token = await getToken();
if (!token) return null;
let targets = PROVIDERS;
if (server && server !== 'all') {
const cleanName = server.replace('1Embed - ', '').toLowerCase();
targets = PROVIDERS.filter(p => p.id === cleanName || p.name.toLowerCase().includes(cleanName));
if (!targets.length) targets = PROVIDERS;
}
const settled = await Promise.allSettled(targets.map(p => fetchProviderStream(p, type, id, s, e, token)));
const allUrls = [];
for (const r of settled) if (r.status === 'fulfilled' && r.value) allUrls.push(...r.value);
return allUrls.length ? { allUrls } : null;
}
export async function getSources() { return PROVIDERS.map(p => p.name); }

112
src/sources/anihq.js Normal file
View File

@@ -0,0 +1,112 @@
import { getTmdbInfo, USER_AGENT } from '../utils/helpers.js';
async function extractVoe(voeUrl) {
try {
const res = await fetch(voeUrl, {
headers: {
'User-Agent': USER_AGENT,
'Referer': 'https://anihq.cc/'
}
});
const mirrorUrl = res.url;
const html = await res.text();
const scriptMatch = html.match(/<script type="application\/json">\["(.*?)"\]<\/script>/);
if (!scriptMatch) return null;
let str = scriptMatch[1];
str = str.replace(/[a-zA-Z]/g, (c) => {
return String.fromCharCode((c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26);
});
const junk = ['@$', '^^', '~@', '%?', '*~', '!!', '#&'];
junk.forEach(j => { str = str.split(j).join(''); });
str = Buffer.from(str, 'base64').toString('utf-8');
let shifted = '';
for (let i = 0; i < str.length; i++) {
shifted += String.fromCharCode(str.charCodeAt(i) - 3);
}
let reversed = shifted.split('').reverse().join('');
let finalJsonStr = Buffer.from(reversed, 'base64').toString('utf-8');
const finalData = JSON.parse(finalJsonStr);
if (finalData.file || finalData.source) {
return {
url: finalData.file || finalData.source,
mirror: mirrorUrl
};
}
} catch (e) {
return null;
}
return null;
}
function cleanSlug(title) {
return title
.toLowerCase()
.replace(/[^a-z0-9\s]/g, '')
.replace(/\s+/g, '-')
.replace(/^-+|-+$/g, '');
}
export async function getStream({ id, s, e, audio }) {
if (!s) return null;
const info = await getTmdbInfo(id, 'tv', s);
if (!info.isAnime) return null;
const seriesTitle = info.titles.find(t =>
t.toLowerCase() !== 'east blue' &&
t.length > 3
) || info.titles[0];
const cleanTitle = cleanSlug(seriesTitle);
const typeSuffix = audio === 'dub' ? 'english-dubbed' : 'english-subbed';
const url = `https://anihq.cc/watch/${cleanTitle}-episode-${e}-${typeSuffix}/`;
try {
let pageRes = await fetch(url, { headers: { 'User-Agent': USER_AGENT } });
let html = await pageRes.text();
if (!pageRes.ok) {
const searchUrl = `https://anihq.cc/search?keyword=${encodeURIComponent(seriesTitle)}`;
const searchRes = await fetch(searchUrl, { headers: { 'User-Agent': USER_AGENT } });
const searchHtml = await searchRes.text();
const searchRegex = new RegExp(`href="(https://anihq.cc/watch/[^"]+-episode-${e}-${typeSuffix}/)"`, 'i');
const match = searchHtml.match(searchRegex);
if (match) {
const retryRes = await fetch(match[1], { headers: { 'User-Agent': USER_AGENT } });
html = await retryRes.text();
} else return null;
}
const iframeMatch = html.match(/iframe[^>]+src=["'](https:\/\/(?:[a-z0-9.]+|ellenpoliticalfollow\.com)\/e\/[a-zA-Z0-9]+)["']/i);
if (!iframeMatch) return null;
const result = await extractVoe(iframeMatch[1]);
if (result && result.url) {
const mirrorOrigin = new URL(result.mirror).origin;
return {
url: result.url,
headers: {
'Referer': mirrorOrigin + '/',
'User-Agent': USER_AGENT,
'Origin': mirrorOrigin
}
};
}
} catch (err) {
return null;
}
return null;
}

81
src/sources/anineko.js Normal file
View File

@@ -0,0 +1,81 @@
import { getTmdbInfo } from '../utils/helpers.js';
const BASE_URL = "https://anineko.to";
function cleanTitle(t) { return t ? t.toLowerCase().replace(/[^a-z0-9]/g, '') : ''; }
function decodeEntities(str) {
return str.replace(/&(nbsp|amp|quot|lt|gt);/g, (m, e) => ({ "nbsp": " ", "amp": "&", "quot": "\"", "lt": "<", "gt": ">" })[e]).replace(/&#(\d+);/gi, (m, n) => String.fromCharCode(parseInt(n, 10)));
}
async function search(query) {
try {
const html = await fetchText(`${BASE_URL}/browser?keyword=${encodeURIComponent(query)}`, { signal: AbortSignal.timeout(6000) });
const results = [];
let idx = 0;
while ((idx = html.indexOf('nv-anime-thumb', idx)) !== -1) {
const startHref = html.lastIndexOf('<a', idx);
if (startHref === -1) { idx += 14; continue; }
const hrefMatch = html.slice(startHref, html.indexOf('>', startHref)).match(/href=["']([^"']+)["']/i);
if (!hrefMatch) { idx += 14; continue; }
const slug = hrefMatch[1].split('/').pop();
const h3Start = html.indexOf('nv-anime-title', idx);
if (h3Start !== -1) {
const contentStart = html.indexOf('>', h3Start) + 1;
const contentEnd = html.indexOf('<', contentStart);
if (contentEnd !== -1) results.push({ slug, text: html.slice(contentStart, contentEnd).trim() });
}
idx += 14;
}
return results;
} catch { return []; }
}
async function extractHls(embedUrl) {
try {
const html = await fetchText(embedUrl, { headers: { Referer: `${BASE_URL}/` }, signal: AbortSignal.timeout(6000) });
const m = html.match(/const\s+src\s*=\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i) || html.match(/file\s*:\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i) || html.match(/["'](https?:\/\/[^"']+\/master\.m3u8[^"']*)["']/i) || html.match(/["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i);
if (m) return decodeEntities(m[1]);
} catch { }
return null;
}
export async function getStream({ id, s, e, audio }) {
try {
const info = await getTmdbInfo(id, s ? 'tv' : 'movie', s);
if (!info || !info.isAnime) return null;
let seriesSlug = null;
for (const title of info.titles) {
const results = await search(title);
const targetClean = cleanTitle(title);
for (const r of results) if (cleanTitle(r.text) === targetClean || r.text.toLowerCase().includes(title.toLowerCase())) { seriesSlug = r.slug; break; }
if (seriesSlug) break;
}
if (!seriesSlug && info.titles.length) {
const fSearch = await search(info.titles[0]);
if (fSearch.length) seriesSlug = fSearch[0].slug;
}
if (!seriesSlug) return null;
const watchHtml = await fetchText(`${BASE_URL}/watch/${seriesSlug}/ep-${e || 1}`, { headers: { Referer: `${BASE_URL}/watch/${seriesSlug}` }, signal: AbortSignal.timeout(6000) });
const byAudio = { sub: [], dub: [] };
for (const panel of watchHtml.matchAll(/<div\b[^>]*class=["'][^"']*nv-server-grid[^"']*["'][^>]*data-id=["']([^"']+)["'][^>]*>([\s\S]*?)(?=<div\b[^>]*class=["'][^"']*nv-server-grid|$)/gi)) {
const panelAudio = panel[1].toLowerCase().includes("dub") ? "dub" : "sub";
for (const btn of panel[2].matchAll(/data-video=["']([^"']+)["']/gi)) byAudio[panelAudio].push(decodeEntities(btn[1]));
}
const audiosToTry = audio === "all" ? ["sub", "dub"] : (audio === "dub" ? ["dub", "sub"] : ["sub", "dub"]);
const allUrls = [];
for (const aud of audiosToTry) {
for (const embed of byAudio[aud]) {
const hls = await extractHls(embed);
if (hls) allUrls.push({ url: hls, type: "hls", audio: aud, server: "AniNeko", skipProxy: false });
}
if (allUrls.length) break;
}
return allUrls.length ? { allUrls } : null;
} catch { return null; }
}
export async function getSources(args) {
const stream = await getStream(args);
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
}

70
src/sources/anipm.js Normal file
View File

@@ -0,0 +1,70 @@
import { getTmdbInfo, fetchJson } from '../utils/helpers.js';
const BASE_URL = "https://ani.pm";
function cleanTitle(t) { return t ? t.toLowerCase().replace(/[^a-z0-9]/g, '') : ''; }
async function searchCatalog(query) {
try {
const data = await fetchJson(`${BASE_URL}/api/anime/catalog?q=${encodeURIComponent(query)}&page=1`, { signal: AbortSignal.timeout(6000) });
return Array.isArray(data?.items) ? data.items : [];
} catch { return []; }
}
async function resolveEntry(titles, year) {
const orderedTitles = titles.length > 1 ? [...titles].reverse() : titles;
let best = null, bestScore = -1;
for (const title of orderedTitles) {
const items = await searchCatalog(title);
const targetClean = cleanTitle(title);
for (const item of items) {
const itemTitles = [item.title, item.native].filter(Boolean).map(cleanTitle);
let score = 0;
if (itemTitles.some(t => t === targetClean)) score += 5;
else if (itemTitles.some(t => t.includes(targetClean) || targetClean.includes(t))) score += 3;
else continue;
if (item.type === 'TV') score += 4;
else if (item.type === 'Movie' || item.type === 'Special' || item.type === 'ONA') score -= 2;
if (year && item.year) {
const diff = Math.abs(item.year - year);
if (diff === 0) score += 3;
else if (diff === 1) score += 1;
else if (diff > 2) score -= 3;
}
if (score > bestScore) { bestScore = score; best = item; }
}
if (bestScore >= 9) break;
}
return best;
}
export async function getStream({ id, s, e, audio }) {
try {
const info = await getTmdbInfo(id, s ? 'tv' : 'movie', s);
if (!info || !info.isAnime) return null;
const entry = await resolveEntry(info.titles, info.year);
if (!entry) return null;
const episodeNum = e || 1;
const params = new URLSearchParams({ title: entry.title || '', ep: String(episodeNum) });
if (entry.year) params.set('year', entry.year);
if (entry.anilistId) params.set('anilistId', entry.anilistId);
if (entry.malId) params.set('malId', entry.malId);
const servers = await fetchJson(`${BASE_URL}/api/anime/src/servers?${params}`, { signal: AbortSignal.timeout(10000) });
if (!servers) return null;
const audiosToTry = audio === "all" ? ["sub", "dub"] : (audio === "dub" ? ["dub", "sub"] : ["sub", "dub"]);
for (const aud of audiosToTry) {
const list = servers[aud];
if (!Array.isArray(list) || !list.length) continue;
const allUrls = list.filter(srv => (srv.kind === "hls" || srv.kind === "file") && srv.url)
.sort((a, b) => (b.priority || 0) - (a.priority || 0))
.map(srv => ({ url: srv.url.startsWith('/') ? `${BASE_URL}${srv.url}` : srv.url, type: srv.kind === "hls" ? "hls" : "mp4", audio: aud, server: srv.provider ? `AniPM-${srv.provider}` : "AniPM", headers: { Referer: `${BASE_URL}/`, Origin: BASE_URL }, skipProxy: false }));
if (allUrls.length) return { allUrls };
}
return null;
} catch { return null; }
}
export async function getSources(args) {
const stream = await getStream(args);
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
}

14
src/sources/flaxmovies.js Normal file
View File

@@ -0,0 +1,14 @@
import { fetchJson } from '../utils/helpers.js';
const API_BASE = 'https://itjiocunahckqxcnzpoy.supabase.co/functions/v1';
const APIKEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Iml0amlvY3VuYWhja3F4Y256cG95Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzczMzM1MjQsImV4cCI6MjA5MjkwOTUyNH0.9x9ykdHAzrv_GSvnawPeQaOxQeh3sZg0QAh4u9VOF4M';
const HEADERS = { 'apikey': APIKEY, 'authorization': `Bearer ${APIKEY}`, 'content-type': 'application/json', 'origin': 'https://flaxmovies.xyz', 'referer': 'https://flaxmovies.xyz/' };
export async function getStream({ id, s, e }) {
try {
const isMovie = !s;
const data = await fetchJson(isMovie ? `${API_BASE}/get-movie` : `${API_BASE}/get-tv`, { method: 'POST', headers: HEADERS, body: JSON.stringify(isMovie ? { id: String(id) } : { id: String(id), season: Number(s), episode: Number(e) }), signal: AbortSignal.timeout(15000) });
if (!data?.signed_url) return null;
return { allUrls: [{ url: data.signed_url, headers: { 'Referer': 'https://flaxmovies.xyz/', 'Origin': 'https://flaxmovies.xyz' } }] };
} catch { return null; }
}

44
src/sources/fsharetv.js Normal file
View File

@@ -0,0 +1,44 @@
import { fetchText, fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
const BASE_URL = 'https://fsharetv.cc';
const HEADERS = { 'User-Agent': USER_AGENT, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.9', 'Referer': `${BASE_URL}/` };
const API_HEADERS = { ...HEADERS, 'Accept': 'application/json, */*; q=0.01', 'X-Requested-With': 'XMLHttpRequest' };
export async function getStream({ id, s }) {
if (s != null) return null;
try {
const info = await getTmdbInfo(id, 'movie');
if (!info?.imdbId) return null;
const html = await fetchText(`${BASE_URL}/movie/${info.imdbId}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
let watchPath = null;
const match = html.match(/href="(\/w\/[^"]+)"/);
if (match) watchPath = match[1];
if (!watchPath) return null;
const watchHtml = await fetchText(`${BASE_URL}${watchPath}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
let sourceId = null;
const sidMatch = watchHtml.match(/(?:source_id|file_id|setSource)[\s=:\('"]+([^'"\)]+)/i);
if (sidMatch) sourceId = sidMatch[1];
if (!sourceId) return null;
const json = await fetchJson(`${BASE_URL}/api/file/${sourceId}/source?trailer=Png81APqcxU&type=watch`, { headers: { ...API_HEADERS, 'Referer': `${BASE_URL}${watchPath}` }, signal: AbortSignal.timeout(8000) });
if (json.status !== 'ok') return null;
const groups = [];
if (Array.isArray(json.data?.file?.sources)) groups.push(json.data.file.sources);
if (Array.isArray(json.data?.file?.alternatives)) json.data.file.alternatives.forEach(g => Array.isArray(g) && groups.push(g));
const out = [], seen = new Set();
for (const group of groups) {
for (const item of group.filter(st => st?.src).sort((a, b) => parseInt(b.quality) - parseInt(a.quality))) {
const url = item.src.startsWith('http') ? item.src : `${BASE_URL}${item.src}`;
if (seen.has(url)) continue;
seen.add(url);
out.push({ url, quality: parseInt(item.quality) || 0, label: item.label || null });
}
}
out.sort((a, b) => b.quality - a.quality);
return out.length ? { allUrls: out.map(u => ({ url: u.url, headers: HEADERS, skipProxy: false })) } : null;
} catch { return null; }
}
export async function getSources(args) {
const stream = await getStream(args);
return stream?.allUrls ? stream.allUrls.map(u => u.url) : [];
}

47
src/sources/fsonic.js Normal file
View File

@@ -0,0 +1,47 @@
import { fetchText, fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
const BASE_URL = 'https://www.fsonic.net';
const FSHARE_BASE = 'https://fsharetv.co';
const HEADERS = { 'User-Agent': USER_AGENT, 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en-US,en;q=0.9' };
export async function getStream({ id, s }) {
if (s != null) return null;
try {
const info = await getTmdbInfo(id, 'movie');
if (!info?.titles?.length) return null;
let watchSlug = null;
for (const title of info.titles) {
try {
const html = await fetchText(`${BASE_URL}/movie/search/${encodeURIComponent(title)}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
let idx = 0, found = null;
const yearStr = String(info.year);
while ((idx = html.indexOf('href="/watch/', idx)) !== -1) {
const start = idx + 6;
const end = html.indexOf('"', start);
const link = html.slice(start, end);
if (!found) found = link;
if (link.includes(yearStr)) { found = link; break; }
idx = end;
}
if (found) { watchSlug = found; break; }
} catch { }
}
if (!watchSlug) return null;
const wHtml = await fetchText(`${BASE_URL}${watchSlug}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
let token = null, trailer = null;
const match = wHtml.match(/init\('([^']+)',\s*(?:'[^']*',\s*)?'([^']+)'/);
if (match) { token = match[1]; trailer = match[2]; }
if (!token) return null;
const json = await fetchJson(`${BASE_URL}/api/source/${token}?trailer=${trailer}&type=watch`, { headers: { ...HEADERS, 'Accept': 'application/json, text/plain, */*', 'Referer': `${BASE_URL}${watchSlug}` }, signal: AbortSignal.timeout(8000) });
if (json.status !== 'ok') return null;
const allGroups = [];
if (json.data?.file?.sources?.length) allGroups.push(json.data.file.sources);
if (json.data?.file?.alternatives) for (const g of json.data.file.alternatives) if (g?.length) allGroups.push(g);
const urls = new Set();
for (const group of allGroups) {
const best = [...group].filter(st => st?.src).sort((a, b) => parseInt(b.quality) - parseInt(a.quality))[0];
if (best) urls.add(best.src.startsWith('http') ? best.src : `${FSHARE_BASE}${best.src}`);
}
return urls.size ? { allUrls: [...urls].map(url => ({ url, headers: { ...HEADERS, 'Referer': `${FSHARE_BASE}/` }, skipProxy: false })) } : null;
} catch { return null; }
}

46
src/sources/fsonline.js Normal file
View File

@@ -0,0 +1,46 @@
import { fetchText, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
const ORIGIN = 'https://www3.fsonline.app';
const AJAX_URL = `${ORIGIN}/wp-admin/admin-ajax.php`;
const HEADERS = { 'User-Agent': USER_AGENT, 'Origin': ORIGIN, 'Referer': `${ORIGIN}/` };
export async function getStream({ id, s, e }) {
try {
const isTv = s != null && e != null;
const info = await getTmdbInfo(id, isTv ? 'tv' : 'movie');
if (!info?.titles?.length) return null;
const searchHtml = await fetchText(`${ORIGIN}/?s=${encodeURIComponent(info.titles[0] + (info.year ? ' ' + info.year : ''))}`, { headers: HEADERS });
const typeFolder = isTv ? 'seriale' : 'film';
const linkMatch = searchHtml.match(new RegExp(`href=["'](https?://www3\\.fsonline\\.app/${typeFolder}/([^"'/]+)/)["']`, 'i'));
if (!linkMatch) return null;
const targetPageUrl = isTv ? `${ORIGIN}/episoade/${linkMatch[2].replace(/-\d{4}$/, '')}-sezonul-${s}-episodul-${e}/` : linkMatch[1];
const pageHtml = await fetchText(targetPageUrl, { headers: HEADERS });
const movieId = (pageHtml.match(/movie-id=['"]([^'"]+)['"]/) || pageHtml.match(/movie-id=([^ >]+)/))?.[1];
if (!movieId) return null;
const ajaxHtml = await fetchText(AJAX_URL, { method: 'POST', headers: { ...HEADERS, 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest', 'Referer': targetPageUrl }, body: `action=lazy_player&movieID=${movieId}` });
const results = [];
let idx = 0;
while ((idx = ajaxHtml.indexOf('data-vs="', idx)) !== -1) {
const embedStart = idx + 9;
const embedEnd = ajaxHtml.indexOf('"', embedStart);
const embedUrl = ajaxHtml.slice(embedStart, embedEnd);
const spanStart = ajaxHtml.indexOf('<span>', embedEnd);
const spanEnd = ajaxHtml.indexOf('</span>', spanStart);
const serverLabel = ajaxHtml.slice(spanStart + 6, spanEnd).trim().toLowerCase();
if (serverLabel.includes('filesun')) {
try {
const rHtml = await fetchText(embedUrl, { headers: { 'Referer': ORIGIN, 'User-Agent': USER_AGENT } });
const m3u8Match = rHtml.match(/file:\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/) || rHtml.match(/["']?file["']?:\s*["'](https?:\/\/[^"']+)["']/);
if (m3u8Match) results.push({ url: m3u8Match[1].replace(/\\\//g, '/'), server: 'FSOnline - FileSuN', headers: { 'Referer': embedUrl, 'Origin': 'https://player.fsonline.app', 'User-Agent': USER_AGENT } });
} catch { }
}
idx = spanEnd;
}
return results.length ? { allUrls: results } : null;
} catch { return null; }
}
export async function getSources(args) {
const res = await getStream(args);
return res?.allUrls ? [...new Set(res.allUrls.map(u => u.server))] : [];
}

262
src/sources/goated.js Normal file
View File

@@ -0,0 +1,262 @@
import crypto from "crypto";
export const ID = "goated";
export const DOMAIN = "https://goated.cx";
export const RETURN_ALL_URLS = true;
const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
const enc = new TextEncoder();
const info = enc.encode("goated-api-v1");
const salt = enc.encode("goated-stream-salt");
const generateRandomIP = () => {
const p = () => Math.floor(Math.random() * 255);
let first = p();
while ([0, 10, 127, 169, 172, 192].includes(first)) first = p();
return `${first}.${p()}.${p()}.${p()}`;
};
let cachedSession = null;
let sessionExpiresAt = 0;
let cachedProviders = null;
let providersExpiresAt = 0;
async function getValidSession(sessionIp) {
const now = Date.now();
if (cachedSession && now < sessionExpiresAt) {
return cachedSession;
}
try {
const chalRes = await fetch(`${DOMAIN}/api/auth/challenge`, {
headers: {
"User-Agent": UA,
"X-Forwarded-For": sessionIp,
"X-Real-IP": sessionIp,
Origin: DOMAIN,
Referer: DOMAIN + "/",
},
signal: AbortSignal.timeout(7000),
});
if (!chalRes.ok) throw new Error();
const chalData = await chalRes.json();
const { challenge, difficulty } = chalData;
const targetPrefix = "0".repeat(Math.ceil((difficulty || 16) / 4));
let nonceStr = "";
for (let i = 0; i < 5000000; i++) {
const nonce = Math.random().toString(36).substring(2, 10);
const hash = crypto.createHash("sha256").update(`${challenge}:${nonce}`).digest("hex");
if (hash.startsWith(targetPrefix)) {
nonceStr = nonce;
break;
}
}
if (!nonceStr) throw new Error();
const res = await fetch(`${DOMAIN}/api/auth/session`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent": UA,
"X-Forwarded-For": sessionIp,
"X-Real-IP": sessionIp,
Origin: DOMAIN,
Referer: DOMAIN + "/",
},
body: JSON.stringify({ challenge, nonce: nonceStr }),
signal: AbortSignal.timeout(7000),
});
if (!res.ok) throw new Error();
const data = await res.json();
if (!data.token) throw new Error();
cachedSession = data.token;
sessionExpiresAt = now + (data.expiresIn || 600) * 1000 - 30000;
return cachedSession;
} catch (e) {
throw new Error();
}
}
async function getProviders() {
const now = Date.now();
if (cachedProviders && now < providersExpiresAt) return cachedProviders;
try {
const res = await fetch(`${DOMAIN}/api/providers`, {
headers: { "User-Agent": UA },
signal: AbortSignal.timeout(5000)
});
if (!res.ok) return [];
const data = await res.json();
cachedProviders = data.providers || [];
providersExpiresAt = now + 3600 * 1000;
return cachedProviders;
} catch (e) {
return [];
}
}
async function deriveKey(tokenString) {
const rawKey = await crypto.subtle.importKey("raw", enc.encode(tokenString), "HKDF", false, ["deriveKey"]);
return crypto.subtle.deriveKey(
{ name: "HKDF", hash: "SHA-256", salt, info },
rawKey,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"],
);
}
async function encryptParams(payloadObj, key) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const data = enc.encode(JSON.stringify(payloadObj));
const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, data);
const combined = new Uint8Array(iv.length + ct.byteLength);
combined.set(iv);
combined.set(new Uint8Array(ct), iv.length);
return Buffer.from(combined).toString("base64");
}
async function decryptUrl(b64Url, key) {
const raw = Buffer.from(b64Url, "base64");
const iv = raw.slice(0, 12);
const ct = raw.slice(12);
try {
const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct);
return Buffer.from(plain).toString("utf8");
} catch {
return null;
}
}
async function decryptDeep(obj, key) {
if (!obj || typeof obj !== "object") return obj;
if (Array.isArray(obj)) return Promise.all(obj.map(x => decryptDeep(x, key)));
const out = {};
for (const [k, v] of Object.entries(obj)) {
if ((k === "url" || k === "file") && typeof v === "string" && v.length > 40) {
try {
const dec = await decryptUrl(v, key);
out[k] = dec ? dec : v;
} catch {
out[k] = v;
}
} else if (k === "links" && Array.isArray(v)) {
out[k] = await Promise.all(v.map(async x => {
if (typeof x === "string" && x.length > 40) {
try {
const dec = await decryptUrl(x, key);
return dec ? dec : x;
} catch {
return x;
}
}
return x;
}));
} else {
out[k] = await decryptDeep(v, key);
}
}
return out;
}
export async function getStream({ id, s, e, clientIP }) {
try {
const sessionIp = generateRandomIP();
const sessionJwt = await getValidSession(sessionIp);
const actualKey = await deriveKey(sessionJwt);
const isTv = s && e && s !== "null" && e !== "null";
const contentType = isTv ? "tv" : "movies";
const refererPath = isTv ? `tv/${id}-${s}-${e}/play` : `movie/${id}/play`;
let providers = await getProviders();
let activeProviders = providers.filter(p => p.active && (p.endpoints || []).includes(contentType));
if (activeProviders.length === 0) activeProviders = [{ name: null }];
const fetchProviderStream = async (provider) => {
try {
const payload = isTv
? { type: "tv", id: id.toString(), season: s.toString(), episode: e.toString() }
: { type: "movie", id: id.toString() };
if (provider.name) payload.provider = provider.name;
const dParam = await encryptParams(payload, actualKey);
const streamRes = await fetch(`${DOMAIN}/api/stream?d=${encodeURIComponent(dParam)}`, {
headers: {
Authorization: `Bearer ${sessionJwt}`,
"User-Agent": UA,
"X-Forwarded-For": sessionIp,
"X-Real-IP": sessionIp,
Origin: DOMAIN,
Referer: `${DOMAIN}/${refererPath}`,
},
signal: AbortSignal.timeout(10000)
});
if (!streamRes.ok) return [];
let rawData = await streamRes.json();
if (rawData.enc) {
const raw = Buffer.from(rawData.enc, "base64");
const decIv = raw.slice(0, 12);
const decCt = raw.slice(12);
const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv: decIv }, actualKey, decCt);
rawData = JSON.parse(Buffer.from(decrypted).toString("utf-8"));
}
const streamData = await decryptDeep(rawData, actualKey);
if (!streamData.ok || !streamData.streams || streamData.streams.length === 0) return [];
const validStreams = [];
for (const stream of streamData.streams) {
if (!stream.url) continue;
if (stream.quality && stream.quality.toUpperCase() !== "AUTO" && streamData.streams.some(st => st.quality && st.quality.toUpperCase() === "AUTO")) continue;
let finalUrl = stream.url;
if (finalUrl.startsWith("/api/")) finalUrl = `${DOMAIN}${finalUrl}`;
validStreams.push({
url: finalUrl,
headers: {
"User-Agent": UA,
Referer: DOMAIN + "/",
Origin: DOMAIN,
},
server: stream.source || stream.server || provider.nickname || provider.name || "Goated",
quality: stream.quality || "Auto",
type: finalUrl.includes(".mp4") ? "mp4" : "hls",
skipVerify: true,
skipProxy: true,
});
}
return validStreams;
} catch (err) {
return [];
}
};
const promises = activeProviders.map(p => fetchProviderStream(p));
const results = await Promise.allSettled(promises);
const allUrls = [];
for (const res of results) {
if (res.status === "fulfilled" && res.value.length > 0) {
allUrls.push(...res.value);
}
}
if (allUrls.length > 0) {
return { ...allUrls[0], allUrls };
}
} catch (err) { }
return null;
}

82
src/sources/hexa.js Normal file
View File

@@ -0,0 +1,82 @@
import crypto from 'node:crypto';
import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
const API_BASE = 'https://enc-dec.app/api';
const DOMAINS = ['hexa.su', 'flixer.su'];
async function getChallengeToken() {
try {
const data = await fetchJson(`${API_BASE}/enc-hexa`, { signal: AbortSignal.timeout(6000) });
return data?.status === 200 ? data.result.token : null;
} catch { return null; }
}
export async function getStream({ id, s, e, server }) {
try {
const apiKey = crypto.randomBytes(32).toString('hex');
const capToken = await getChallengeToken();
if (!capToken) return null;
const isTv = s != null && e != null;
let decrypted = null;
for (const domain of DOMAINS) {
try {
const url = isTv
? `https://theemoviedb.${domain}/api/tmdb/tv/${id}/season/${s}/episode/${e}/images`
: `https://theemoviedb.${domain}/api/tmdb/movie/${id}/images`;
const encrypted = await fetchText(url, {
headers: {
'User-Agent': USER_AGENT,
'Referer': `https://${domain}/`,
'Accept': 'text/plain',
'X-Fingerprint-Lite': 'e9136c41504646444',
'X-Api-Key': apiKey,
'X-Cap-Token': capToken
},
signal: AbortSignal.timeout(10000)
});
if (!encrypted) continue;
const decData = await fetchJson(`${API_BASE}/dec-hexa`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: encrypted, key: apiKey }),
signal: AbortSignal.timeout(8000)
});
if (decData?.status === 200 && decData.result?.sources?.length) {
decrypted = decData.result;
break;
}
} catch { continue; }
}
if (!decrypted?.sources?.length) return null;
let sources = decrypted.sources.map(src => ({
url: src.url,
server: `Hexa - ${src.name || src.server || 'Server'}`,
type: src.url.includes('.m3u8') ? 'hls' : 'mp4',
quality: src.quality || 'Auto',
subtitles: (decrypted.subtitles || []).map(sub => ({
url: sub.url,
lang: sub.label || sub.lang || 'Unknown'
})),
skipProxy: false
}));
if (server && server !== 'all') {
const cleanName = server.replace('Hexa - ', '');
sources = sources.filter(u => u.server.includes(cleanName));
}
return sources.length ? { allUrls: sources } : null;
} catch { return null; }
}
export async function getSources() {
return ['Hexa'];
}

60
src/sources/kisskh.js Normal file
View File

@@ -0,0 +1,60 @@
import { fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
export const SKIP_VERIFY = true;
export const MULTI_URL = false;
const ENC_API = "https://enc-dec.app/api";
const BASE = "https://kisskh.do";
const HEADERS = { 'User-Agent': USER_AGENT, Accept: 'application/json' };
function cleanTitle(t) { return t ? t.toLowerCase().replace(/\(\d{4}\)/g, '').replace(/[^a-z0-9]/g, '') : ''; }
async function encKey(episodeId, type) {
try {
const data = await fetchJson(`${ENC_API}/enc-kisskh?text=${episodeId}&type=${type}`, { signal: AbortSignal.timeout(6000) });
return data?.status === 200 ? data.result : null;
} catch { return null; }
}
export async function getStream({ id, s, e }) {
try {
const info = await getTmdbInfo(id, s ? 'tv' : 'movie', s);
if (!info) return null;
let drama = null, bestScore = -1;
for (const title of (info.titles.length > 1 ? [...info.titles].reverse() : info.titles)) {
const results = await fetchJson(`${BASE}/api/DramaList/Search?q=${encodeURIComponent(title)}`, { headers: HEADERS, signal: AbortSignal.timeout(6000) }).catch(() => []);
const targetClean = cleanTitle(title);
for (const item of results) {
const parts = (item.title || '').split(/\s*-\s*/).map(cleanTitle);
let score = 0;
if (parts.some(p => p === targetClean)) score += 5;
else if (parts.some(p => p.includes(targetClean) || targetClean.includes(p))) score += 3;
else continue;
if (score > bestScore) { bestScore = score; drama = item; }
}
if (drama && bestScore >= 5) break;
}
if (!drama) return null;
const detail = await fetchJson(`${BASE}/api/DramaList/Drama/${drama.id}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
const episodeId = detail?.episodes?.find(ep => Math.floor(ep.number) === Number(e || 1))?.id;
if (!episodeId) return null;
const vidKey = await encKey(episodeId, 'vid');
if (!vidKey) return null;
const videoData = await fetchJson(`${BASE}/api/DramaList/Episode/${episodeId}.png?err=false&ts=&time=&kkey=${vidKey}`, { headers: HEADERS, signal: AbortSignal.timeout(10000) });
if (!videoData?.Video) return null;
let subtitles = [];
const subKey = await encKey(episodeId, 'sub');
if (subKey) {
try {
const subList = await fetchJson(`${BASE}/api/Sub/${episodeId}?kkey=${subKey}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
if (Array.isArray(subList)) subtitles = subList.map(sub => ({ url: sub.src, label: sub.label, lang: sub.land, default: !!sub.default }));
} catch { }
}
return { allUrls: [{ url: videoData.Video, type: videoData.Video.includes('.m3u8') ? 'hls' : 'mp4', audio: 'sub', server: 'KissKH', skipProxy: false, subtitles }] };
} catch { return null; }
}
export async function getSources(args) {
const stream = await getStream(args);
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
}

78
src/sources/lookmovie.js Normal file
View File

@@ -0,0 +1,78 @@
import { USER_AGENT, fetchText, fetchJson } from '../utils/helpers.js';
const TMDB_BASE = 'https://api.themoviedb.org/3';
const LM_DOMAINS = ['https://www.lookmovie2.to', 'https://lookmovie2.to', 'https://lookmovie.foundation'];
const HEADERS_BASE = { 'User-Agent': USER_AGENT, 'Accept-Language': 'en-US,en;q=0.9' };
async function searchLookMovie(type, title, year, clientIP) {
for (const base of LM_DOMAINS) {
try {
const headers = { ...HEADERS_BASE, 'Accept': 'application/json', 'Referer': `${base}/`, 'X-Requested-With': 'XMLHttpRequest', ...(clientIP && { 'X-Forwarded-For': clientIP }) };
const data = await fetchJson(`${base}/api/v1/${type}/do-search/?q=${encodeURIComponent(title)}`, { headers, signal: AbortSignal.timeout(4000) });
const results = data?.result;
if (results?.length) {
const match = results.find(r => String(r.year) === String(year)) ?? results.find(r => r.title?.toLowerCase() === title.toLowerCase()) ?? results[0];
if (match) return { match, base };
}
} catch { }
}
return null;
}
const STORAGE_RE = /window\[['"](?:movie|show)_storage['"]\]\s*=\s*\{([^}]+)\}/s;
const ID_MOVIE_RE = /['"]?(?:id_movie|movieId)['"]?\s*[:=]\s*['"]?(\d+)['"]?/i;
const SEASONS_RE = /seasons\s*:\s*(\[[\s\S]+?\])\s*[,}]/;
function getEpisodeIdFast(html, s, e) {
const sMatch = html.match(STORAGE_RE);
if (sMatch) {
const block = sMatch[1], sm = block.match(SEASONS_RE);
if (sm) {
try {
const seasons = JSON.parse(sm[1]);
const season = seasons.find(x => String(x?.season ?? x?.meta?.season) === String(s));
if (season) {
const ep = Array.isArray(season.episodes) ? season.episodes.find(x => String(x.episode) === String(e)) : (season.episodes?.[String(e)] || Object.values(season.episodes || {}).find(x => String(x.episode) === String(e)));
if (ep) return String(ep.id_episode ?? ep.id);
}
} catch { }
}
}
const parts = html.split(/id_episode['"]?\s*[:=]\s*['"]?(\d+)['"]?/i);
for (let i = 1; i < parts.length; i += 2) {
const context = parts[i - 1].slice(-300) + parts[i + 1].slice(0, 300);
if (context.match(new RegExp(`['"]?episode['"]?\\s*[:=]\\s*['"]?${e}['"]?`, 'i')) && context.match(new RegExp(`['"]?season['"]?\\s*[:=]\\s*['"]?${s}['"]?`, 'i'))) return parts[i];
}
const am = html.match(new RegExp(`data-season=["']${s}["'][^>]*?data-episode=["']${e}["'][^>]*?data-id=["'](\\d+)["']`, 'i')) || html.match(new RegExp(`data-episode=["']${e}["'][^>]*?data-season=["']${s}["'][^>]*?data-id=["'](\\d+)["']`, 'i'));
return am ? am[1] : null;
}
export async function getStream({ id, s, e, tmdbApiKey, clientIP }) {
try {
const tmdbKey = tmdbApiKey;
if (!tmdbKey) return null;
const isTV = s != null && e != null;
const typeStr = isTV ? 'shows' : 'movies';
const tmdbData = await fetchJson(`${TMDB_BASE}/${isTV ? 'tv' : 'movie'}/${id}?api_key=${tmdbKey}`, { signal: AbortSignal.timeout(3000) });
const title = tmdbData?.title || tmdbData?.name;
const year = (tmdbData?.first_air_date || tmdbData?.release_date || '').slice(0, 4);
if (!title) return null;
const searchRes = await searchLookMovie(typeStr, title, year, clientIP);
if (!searchRes?.match?.slug) return null;
const { match, base } = searchRes;
const headers = { ...HEADERS_BASE, 'Accept': 'text/html', 'Referer': `${base}/`, ...(clientIP && { 'X-Forwarded-For': clientIP }) };
const html = await fetchText(`${base}/${typeStr}/play/${match.slug}`, { headers, signal: AbortSignal.timeout(8000) });
const storageMatch = html.match(STORAGE_RE);
if (!storageMatch) return null;
const hashMatch = storageMatch[1].match(/hash\s*:\s*['"]([^'"]+)['"]/);
const expiresMatch = storageMatch[1].match(/expires\s*:\s*(\d+)/);
if (!hashMatch || !expiresMatch) return null;
const streamId = isTV ? getEpisodeIdFast(html, s, e) : (match.id_movie || match.id || html.match(ID_MOVIE_RE)?.[1]);
if (!streamId) return null;
const accessHeaders = { ...HEADERS_BASE, 'Accept': 'application/json', 'Referer': `${base}/`, 'X-Requested-With': 'XMLHttpRequest', ...(clientIP && { 'X-Forwarded-For': clientIP }) };
const data = await fetchJson(`${base}/api/v1/security/${isTV ? 'episode' : 'movie'}-access?id_${isTV ? 'episode' : 'movie'}=${streamId}&hash=${hashMatch[1]}&expires=${expiresMatch[1]}`, { headers: accessHeaders, signal: AbortSignal.timeout(8000) });
const streams = data?.streams ?? data?.result?.streams ?? data?.data?.streams ?? data;
const allUrls = Object.entries(streams || {}).filter(([, v]) => typeof v === 'string' && v.includes('.m3u8')).map(([quality, url]) => ({ url, quality }));
return allUrls.length ? { allUrls } : null;
} catch { return null; }
}

115
src/sources/mapple.js Normal file
View File

@@ -0,0 +1,115 @@
import { USER_AGENT } from '../utils/helpers.js';
const BASE_URL = "https://mapple.club";
const PROXY_HEADERS = { Referer: `${BASE_URL}/`, Origin: BASE_URL, "User-Agent": USER_AGENT };
const K = new Uint32Array([0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2]);
const W_BUF = new Uint32Array(64);
function sha256Into(buf, total, out) {
let h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a, h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19;
const dv = new DataView(buf.buffer, buf.byteOffset, total);
for (let off = 0; off < total; off += 64) {
for (let i = 0; i < 16; i++) W_BUF[i] = dv.getUint32(off + i * 4, false);
for (let i = 16; i < 64; i++) {
const a = W_BUF[i - 15], b = W_BUF[i - 2];
const s0 = ((a >>> 7) | (a << 25)) ^ ((a >>> 18) | (a << 14)) ^ (a >>> 3);
const s1 = ((b >>> 17) | (b << 15)) ^ ((b >>> 19) | (b << 13)) ^ (b >>> 10);
W_BUF[i] = (W_BUF[i - 16] + s0 + W_BUF[i - 7] + s1) | 0;
}
let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7;
for (let i = 0; i < 64; i++) {
const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
const ch = (e & f) ^ (~e & g);
const t1 = (h + S1 + ch + K[i] + W_BUF[i]) | 0;
const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
const maj = (a & b) ^ (a & c) ^ (b & c);
const t2 = (S0 + maj) | 0;
h = g; g = f; f = e; e = (d + t1) | 0; d = c; c = b; b = a; a = (t1 + t2) | 0;
}
h0 = (h0 + a) | 0; h1 = (h1 + b) | 0; h2 = (h2 + c) | 0; h3 = (h3 + d) | 0; h4 = (h4 + e) | 0; h5 = (h5 + f) | 0; h6 = (h6 + g) | 0; h7 = (h7 + h) | 0;
}
const odv = new DataView(out.buffer, out.byteOffset, 32);
odv.setUint32(0, h0 >>> 0, false); odv.setUint32(4, h1 >>> 0, false); odv.setUint32(8, h2 >>> 0, false); odv.setUint32(12, h3 >>> 0, false); odv.setUint32(16, h4 >>> 0, false); odv.setUint32(20, h5 >>> 0, false); odv.setUint32(24, h6 >>> 0, false); odv.setUint32(28, h7 >>> 0, false);
}
function solvePoW(challenge, difficulty) {
const encoder = new TextEncoder();
const cBuf = encoder.encode(challenge);
const maxLen = cBuf.length + 16;
const total = (maxLen + 1 + 8 + 63) & ~63;
const buf = new Uint8Array(total);
buf.set(cBuf);
const hash = new Uint8Array(32);
const maskBytes = Math.floor(difficulty / 8);
const maskBits = difficulty % 8;
const finalMask = maskBits > 0 ? (0xff << (8 - maskBits)) & 0xff : 0;
const dv = new DataView(buf.buffer);
for (let nonce = 0; nonce < 10000000; nonce++) {
const str = nonce.toString();
const len = cBuf.length + str.length;
for (let i = 0; i < str.length; i++) buf[cBuf.length + i] = str.charCodeAt(i);
buf[len] = 0x80;
for (let i = len + 1; i < total - 8; i++) buf[i] = 0;
dv.setUint32(total - 4, len * 8, false);
dv.setUint32(total - 8, 0, false);
sha256Into(buf, total, hash);
let ok = true;
for (let i = 0; i < maskBytes; i++) if (hash[i] !== 0) { ok = false; break; }
if (ok && (finalMask === 0 || (hash[maskBytes] & finalMask) === 0)) return { found: true, nonce: str };
}
return { found: false };
}
const SERVER_LIST = [{ id: "mapple", name: "Mapple" }, { id: "s1", name: "Nexus" }, { id: "s2", name: "Cipher" }, { id: "s3", name: "Pulse" }, { id: "s4", name: "Vertex" }, { id: "s10", name: "Chimp" }];
export async function getStream(args) {
const { id, s, e, server } = args;
const isTv = s != null && e != null;
const mediaType = isTv ? "tv" : "movie";
const tvSlug = isTv ? `${s}-${e}` : "";
const pageUrl = isTv ? `${BASE_URL}/watch/tv/${id}/${s}/${e}` : `${BASE_URL}/watch/movie/${id}`;
try {
const pageRes = await fetch(pageUrl, { headers: PROXY_HEADERS, signal: AbortSignal.timeout(10000) });
if (!pageRes.ok) return null;
const html = await pageRes.text();
const reqMatch = html.match(/window\.__REQUEST_TOKEN__\s*=\s*"([^"]+)"/);
if (!reqMatch) return null;
const requestToken = reqMatch[1];
const initRes = await fetch(`${BASE_URL}/api/playback-init`, { method: "POST", headers: { "Content-Type": "application/json", ...PROXY_HEADERS, Referer: pageUrl }, body: JSON.stringify({ mediaId: Number(id), mediaType, requestToken }), signal: AbortSignal.timeout(10000) });
const initData = await initRes.json();
let streamToken = initData.token;
if (initData.requiresPow && initData.pow) {
const powRes = solvePoW(initData.pow.challenge, initData.pow.difficulty);
if (powRes.found) {
const solveRes = await fetch(`${BASE_URL}/api/playback-init`, { method: "POST", headers: { "Content-Type": "application/json", ...PROXY_HEADERS, Referer: pageUrl }, body: JSON.stringify({ mediaId: Number(id), mediaType, requestToken, pow: { challengeId: initData.pow.challengeId, nonce: powRes.nonce } }), signal: AbortSignal.timeout(10000) });
streamToken = (await solveRes.json()).token;
}
}
if (!streamToken) return null;
let targets = SERVER_LIST;
if (server && server !== "all") {
const clean = server.replace("Mapple - ", "");
targets = SERVER_LIST.filter(sv => sv.name === clean);
if (!targets.length) targets = SERVER_LIST;
}
const allUrls = [];
for (const srv of targets) {
try {
const streamUrl = `${BASE_URL}/api/stream?mediaId=${id}&mediaType=${mediaType}&tv_slug=${tvSlug}&source=${srv.id}&apikey=mptv_sk_a8f29c4e7b3d1f&requestToken=${requestToken}&token=${streamToken}`;
const streamRes = await fetch(streamUrl, { headers: { ...PROXY_HEADERS, Referer: pageUrl }, signal: AbortSignal.timeout(8000) });
const streamData = await streamRes.json();
if (streamData.success && streamData.data?.stream_url) {
let fileUrl = streamData.data.stream_url;
if (fileUrl.includes('omena-puu') || fileUrl.includes('nocach')) fileUrl += fileUrl.includes('?') ? '&format=.m3u8' : '?format=.m3u8';
allUrls.push({ url: fileUrl, server: `Mapple - ${srv.name}`, headers: PROXY_HEADERS, skipProxy: false, skipVerify: true, skipHlsCheck: true });
}
} catch { }
}
return allUrls.length ? { allUrls } : null;
} catch { return null; }
}
export async function getSources() { return SERVER_LIST.map(s => `Mapple - ${s.name}`); }
export const SKIP_VERIFY = true;
export const MULTI_URL = true;

111
src/sources/meowtv.js Normal file
View File

@@ -0,0 +1,111 @@
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
const API_BASE = 'https://api.meowtv.ru';
const REFERER = 'https://meowtv.ru/';
const ENC_DEC_API = 'https://enc-dec.app/api/dec-meowtv';
const HEADERS = {
'User-Agent': USER_AGENT,
'Accept': 'application/json',
'Referer': REFERER,
'Origin': 'https://meowtv.ru',
'Accept-Language': 'en-US,en;q=0.9'
};
const SERVERS = [
'pseudo',
'lynx',
'tik',
'ipcloud',
'v4:English',
'turkce',
'v5:Hindi',
'v4:Hindi',
'v6:Hindi'
];
const TIMEOUT_MS = 15000;
export const VERIFY_HEADERS = { 'User-Agent': USER_AGENT, 'Referer': REFERER, 'Origin': 'https://meowtv.ru' };
async function decryptPayload(payload) {
try {
const response = await fetchJson(ENC_DEC_API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data: payload })
});
if (response?.status !== 200 || !response?.result) {
return null;
}
return typeof response.result === 'string' ? JSON.parse(response.result) : response.result;
} catch (e) {
return null;
}
}
export async function getStream({ id, s, e, server }) {
try {
const isTv = !!s;
let targets = SERVERS;
if (server && server !== 'all') {
const cleanName = server.replace('MeowTV - ', '');
targets = SERVERS.includes(cleanName) ? [cleanName] : [targets[0]];
}
const results = await Promise.all(targets.map(async srv => {
try {
return await Promise.race([
(async () => {
const path = isTv
? `/streams/tv/${id}/${s}/${e}?s=${encodeURIComponent(srv)}`
: `/streams/movie/${id}?s=${encodeURIComponent(srv)}`;
const payload = await fetchJson(`${API_BASE}${path}`, {
headers: HEADERS
});
if (!payload) return null;
const data = await decryptPayload(payload);
if (!data) return null;
const urls = [];
if (data?.url?.startsWith('http')) {
urls.push({
url: data.url,
server: `MeowTV - ${srv}`,
});
}
if (Array.isArray(data?.streams)) {
for (const stream of data.streams) {
if (stream?.url?.startsWith('http')) {
urls.push({
url: stream.url,
server: `MeowTV - ${srv} (${stream.language || 'Unknown'})`,
});
}
}
}
return urls;
})(),
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout exceeded')), TIMEOUT_MS))
]);
} catch (err) {
return null;
}
}));
const allUrls = results.filter(Boolean).flat();
return allUrls.length ? { allUrls } : null;
} catch (e) {
return null;
}
}
export async function getSources() {
return SERVERS.map(s => `MeowTV - ${s}`);
}

62
src/sources/nebula.js Normal file
View File

@@ -0,0 +1,62 @@
import { fetchJson } from '../utils/helpers.js';
const BASE_URL = 'https://nebula.work.gd/private/7f6b9d179ba7ecd77a39520e';
async function getImdbId(tmdbApiKey, tmdbId, s, e) {
const key = tmdbApiKey;
if (!key) return null;
try {
const type = s != null && e != null ? 'tv' : 'movie';
const url = `https://api.themoviedb.org/3/${type}/${tmdbId}/external_ids?api_key=${key}`;
const res = await fetchJson(url);
return res?.imdb_id || null;
} catch(err) {
return null;
}
}
export async function getStream({ id, s, e, tmdbApiKey }) {
try {
const imdbId = await getImdbId(tmdbApiKey, id, s, e);
if (!imdbId) return null;
const path = s != null && e != null
? `/stream/series/${imdbId}:${s}:${e}.json`
: `/stream/movie/${imdbId}.json`;
const response = await fetch(`${BASE_URL}${path}`, {
headers: {
'Accept': 'application/json'
},
signal: AbortSignal.timeout(15000)
});
if (!response.ok) return null;
const data = await response.json();
if (!data?.streams?.length) return null;
const allUrls = data.streams
.filter(stream => stream.url && stream.url.trim() !== '' && !stream.url.includes('hubcloud'))
.map(stream => {
const headers = stream.behaviorHints?.proxyHeaders?.request || {};
let serverName = stream.name || 'Unknown';
if (serverName.includes('\n')) serverName = serverName.split('\n')[0];
const filename = stream.filename || stream.behaviorHints?.filename;
if (filename) serverName += ` - ${filename}`;
return {
url: stream.url,
server: `Nebula - ${serverName}`,
type: stream.url.includes('.m3u8') ? 'hls' : 'mp4',
headers: Object.keys(headers).length > 0 ? headers : undefined,
skipProxy: stream.behaviorHints?.notWebReady === false
};
}).filter(Boolean);
return allUrls.length ? { allUrls } : null;
} catch (err) {
return null;
}
}

57
src/sources/nekowatch.js Normal file
View File

@@ -0,0 +1,57 @@
import { fetchJson, fetchText, getTmdbInfo, tmdbToAnilist } from '../utils/helpers.js';
const BASE_URL = "https://nekowatch.xyz";
const ANIME_JS_URL = `${BASE_URL}/js/anime.js?v=v263_fast_player_start`;
const FALLBACK_ORDER = ['kiwi', 'ally', 'bee', 'arc', 'anikoto', 'jet', 'bonk', 'moo', 'hop', 'pulsar', 'pahe'];
let providerOrderCache = null, providerOrderTs = 0;
async function getProviderOrder() {
const now = Date.now();
if (providerOrderCache && now - providerOrderTs < 1800000) return providerOrderCache;
try {
const js = await fetchText(ANIME_JS_URL, { signal: AbortSignal.timeout(6000) });
const match = js.match(/PROVIDER_ORDER\s*=\s*\[([\s\S]*?)\]/);
if (match) {
const order = [...match[1].matchAll(/['"]([a-z0-9_]+)['"]/gi)].map(m => m[1]);
if (order.length) { providerOrderCache = order; providerOrderTs = now; return order; }
}
} catch { }
return providerOrderCache || FALLBACK_ORDER;
}
export async function getStream({ id, s, e, audio }) {
try {
const info = await getTmdbInfo(id, s ? 'tv' : 'movie', s);
if (!info || !info.isAnime) return null;
const anilistId = await tmdbToAnilist(id, s ? 'tv' : 'movie', s, info.titles, info.year);
if (!anilistId) return null;
const ep = e || 1;
const audiosToTry = audio === "all" ? ["sub", "dub"] : (audio === "dub" ? ["dub", "sub"] : ["sub", "dub"]);
const providerOrder = await getProviderOrder();
for (const aud of audiosToTry) {
const results = await Promise.allSettled(providerOrder.map(p => fetchJson(`${BASE_URL}/api/anime/watch/${p}/${anilistId}/${aud}/${p}-${ep}`, { signal: AbortSignal.timeout(8000) })));
const allUrls = [], seenActual = new Set();
for (const r of results) {
if (r.status !== 'fulfilled' || !r.value) continue;
const data = r.value;
let streams = [], actual = data.attempted?.actualProvider || data.provider;
if (Array.isArray(data.streams)) streams = data.streams;
else {
for (const key of Object.keys(data)) if (data[key] && Array.isArray(data[key].streams)) { streams.push(...data[key].streams); if (!actual) actual = data[key].provider || key; }
}
if (!streams.length) continue;
if (actual) { if (seenActual.has(actual)) continue; seenActual.add(actual); }
const valid = streams.filter(st => st.type === "hls" && st.url).sort((a, b) => (b.priority || 0) - (a.priority || 0));
for (const st of valid) allUrls.push({ url: st.url, type: "hls", audio: aud, server: st.server ? `NekoWatch-${st.server}` : "NekoWatch", headers: st.referer ? { Referer: st.referer } : undefined, skipProxy: /\.workers\.dev(\/|$)/i.test(st.url) });
}
if (allUrls.length) return { allUrls };
}
return null;
} catch { return null; }
}
export async function getSources(args) {
const stream = await getStream(args);
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
}

123
src/sources/opstream.js Normal file
View File

@@ -0,0 +1,123 @@
import { getTmdbInfo, USER_AGENT } from '../utils/helpers.js';
const BASE_URL = 'https://opstream.fun';
const API_BASE = `${BASE_URL}/api/resolve`;
const API_HEADERS = {
'User-Agent': USER_AGENT,
'Accept': 'application/x-ndjson; charset=utf-8',
'Referer': `${BASE_URL}/`,
'Origin': BASE_URL,
};
const STREAM_HEADERS = {
'User-Agent': USER_AGENT,
'Referer': `${BASE_URL}/`,
'Origin': BASE_URL,
};
export async function getStream({ id, s, e }) {
try {
const isTv = s != null && e != null;
const info = await getTmdbInfo(id, isTv ? 'tv' : 'movie');
if (!info?.titles?.length || !info.imdbId) return null;
const params = new URLSearchParams({
type: isTv ? 'tv' : 'movie',
tmdbId: id,
title: info.titles[0],
year: info.year || '',
imdbId: info.imdbId,
progress: '1'
});
if (isTv) {
params.append('season', s);
params.append('episode', e);
}
const res = await fetch(`${API_BASE}?${params}`, {
headers: API_HEADERS,
signal: AbortSignal.timeout(15000)
});
if (!res.ok) return null;
const text = await res.text();
const lines = text.split('\n');
let streamData = null;
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
const json = JSON.parse(trimmed);
if (json.t === 'done' && json.data) {
streamData = json.data;
break;
}
} catch { continue; }
}
if (!streamData) return null;
const subtitles = (streamData.captions || []).map(c => ({
url: c.url.startsWith('http') ? c.url : `${BASE_URL}${c.url}`,
lang: c.label || 'Unknown'
}));
const allUrls = [];
if (streamData.qualities && streamData.qualities.length > 0) {
for (const q of streamData.qualities) {
let finalUrl = q.url;
if (!finalUrl.startsWith('http')) {
finalUrl = `${BASE_URL}${finalUrl}`;
}
const isDash = finalUrl.includes('.mpd');
const isHls = finalUrl.includes('.m3u8');
allUrls.push({
url: finalUrl,
server: `OpStream - ${q.label}`,
type: isDash ? 'dash' : (isHls ? 'hls' : 'mp4'),
headers: STREAM_HEADERS,
subtitles,
skipProxy: true
});
}
} else if (streamData.url) {
let finalUrl = streamData.url;
if (finalUrl.includes('?u=')) {
try {
const uParam = new URL(finalUrl, BASE_URL).searchParams.get('u');
if (uParam) finalUrl = Buffer.from(uParam, 'base64').toString('utf8');
} catch {}
} else if (!finalUrl.startsWith('http')) {
finalUrl = `${BASE_URL}${finalUrl}`;
}
const isDash = streamData.kind === 'dash' || finalUrl.includes('.mpd');
const isHls = streamData.kind === 'hls' || finalUrl.includes('.m3u8');
allUrls.push({
url: finalUrl,
server: 'OpStream',
type: isDash ? 'dash' : (isHls ? 'hls' : 'mp4'),
headers: STREAM_HEADERS,
subtitles,
skipProxy: true
});
}
if (allUrls.length === 0) return null;
return { allUrls };
} catch { return null; }
}
export async function getSources() {
return ['OpStream'];
}

78
src/sources/peachify.js Normal file
View File

@@ -0,0 +1,78 @@
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
const DEC_API = "https://enc-dec.app/api/dec-peachify";
const ORIGIN = "https://peachify.top";
const HEADERS = { "User-Agent": USER_AGENT, "Origin": ORIGIN, "Referer": `${ORIGIN}/` };
const SERVERS = [
{ label: "Wolf", path: "air", api: "https://usa.eat-peach.sbs" },
{ label: "Spider", path: "holly", api: "https://usa.eat-peach.sbs" },
{ label: "Iron", path: "moviebox", api: "https://uwu.eat-peach.sbs" },
{ label: "Multi", path: "multi", api: "https://usa.eat-peach.sbs" },
{ label: "Dark", path: "net", api: "https://uwu.eat-peach.sbs" },
];
export async function getStream({ id, s, e, server }) {
try {
const isTv = s != null && e != null;
const segment = isTv ? `tv/${id}/${s}/${e}` : `movie/${id}`;
let targets = SERVERS;
if (server && server !== 'all') {
const cleanName = server.replace('Peachify - ', '');
targets = SERVERS.filter(srv => srv.label === cleanName);
if (!targets.length) targets = SERVERS;
}
const settled = await Promise.allSettled(targets.map(async srv => {
const url = `${srv.api}/${srv.path}/${segment}`;
const rawData = await fetchJson(url, { headers: HEADERS, signal: AbortSignal.timeout(10000) });
if (!rawData?.data) throw new Error();
const decJson = await fetchJson(DEC_API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: rawData.data }),
signal: AbortSignal.timeout(10000)
});
if (decJson.status !== 200 || !decJson.result) throw new Error();
const res = decJson.result;
const sources = Array.isArray(res) ? res : (res.sources || [res]);
const subs = res.subtitles || [];
return sources.map(source => {
const streamUrl = source.url || source.file || source.link;
if (!streamUrl) return null;
return {
url: streamUrl,
server: `Peachify - ${srv.label}`,
quality: source.quality || "Auto",
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
headers: { ...HEADERS, "Origin": ORIGIN },
subtitles: subs.map(sub => ({
url: sub.url || sub.file,
lang: sub.language || sub.lang || sub.label || 'Unknown'
})),
skipProxy: false,
skipVerify: true,
skipHlsCheck: true
};
}).filter(Boolean);
}));
const allUrls = [];
for (const r of settled) {
if (r.status === 'fulfilled' && r.value) allUrls.push(...r.value);
}
return allUrls.length ? { allUrls } : null;
} catch { return null; }
}
export async function getSources() {
return SERVERS.map(s => `Peachify - ${s.label}`);
}

45
src/sources/pengu.js Normal file
View File

@@ -0,0 +1,45 @@
import { fetchJson } from '../utils/helpers.js';
const PENGU_CONFIG = '%7B%22source_4khdhub%22%3A%22on%22%2C%22source_moviebox%22%3A%22on%22%2C%22source_moviesdrives%22%3A%22on%22%2C%22source_vaplayer%22%3A%22on%22%2C%22source_hdghartv%22%3A%22on%22%2C%22res_2160%22%3A%22on%22%2C%22res_1080%22%3A%22on%22%2C%22res_720%22%3A%22on%22%2C%22disable_direct%22%3A%22on%22%7D';
const BASE_URL = `https://pengu.uk/${PENGU_CONFIG}`;
async function getImdbId(tmdbApiKey, tmdbId, s, e) {
const key = tmdbApiKey;
if (!key) return null;
try {
const url = s != null && e != null
? `https://api.themoviedb.org/3/tv/${tmdbId}/external_ids?api_key=${key}`
: `https://api.themoviedb.org/3/movie/${tmdbId}?api_key=${key}`;
const res = await fetchJson(url);
return res?.imdb_id || null;
} catch { return null; }
}
export async function getStream({ id, s, e, tmdbApiKey }) {
try {
const imdbId = await getImdbId(tmdbApiKey, id, s, e);
if (!imdbId) return null;
const path = s != null && e != null
? `/stream/series/${imdbId}:${s}:${e}.json`
: `/stream/movie/${imdbId}.json`;
const res = await fetchJson(`${BASE_URL}${path}`);
if (!res?.streams?.length) return null;
const allUrls = res.streams.map(stream => {
if (!stream.url) return null;
const headers = stream.behaviorHints?.proxyHeaders?.request || {};
return {
url: stream.url,
server: `Pengu - ${stream.name || 'Server'}`,
headers: Object.keys(headers).length > 0 ? headers : undefined,
skipProxy: stream.behaviorHints?.notWebReady === false
};
}).filter(Boolean);
return allUrls.length ? { allUrls } : null;
} catch {
return null;
}
}

104
src/sources/purstream.js Normal file
View File

@@ -0,0 +1,104 @@
import { fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
const DOMAIN = 'https://purstream.club';
const API_BASE = 'https://api.purstream.club/api/v1';
const HEADERS = {
'User-Agent': USER_AGENT,
'Accept': 'application/json, text/plain, */*',
'Referer': `${DOMAIN}/`,
'Origin': DOMAIN,
'X-Requested-With': 'XMLHttpRequest',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-site'
};
export async function getStream({ id, s, e }) {
try {
const isTv = s != null;
const info = await getTmdbInfo(id, isTv ? 'tv' : 'movie');
if (!info?.titles?.length) {
return null;
}
const title = info.titles[0];
const searchUrl = `${API_BASE}/search-bar/search/${encodeURIComponent(title)}`;
const searchData = await fetchJson(searchUrl, {
headers: HEADERS,
signal: AbortSignal.timeout(10000)
});
const items = searchData?.data?.items?.movies?.items || [];
const type = isTv ? 'tv' : 'movie';
const lowerTitle = title.toLowerCase();
let match = items.find(item =>
item.type === type &&
item.title?.toLowerCase() === lowerTitle &&
(!info.year || item.release_date?.startsWith(String(info.year)))
);
if (!match) {
match = items.find(item => item.type === type);
}
if (!match?.id) {
return null;
}
const streamUrl = isTv
? `${API_BASE}/stream/${match.id}/episode?season=${s}&episode=${e}`
: `${API_BASE}/stream/${match.id}`;
const json = await fetchJson(streamUrl, {
headers: HEADERS,
signal: AbortSignal.timeout(10000)
});
const sources = json?.data?.items?.sources;
if (json?.type !== 'success' || !Array.isArray(sources) || !sources.length) {
return null;
}
const chosen = sources.find(src => src.stream_url);
if (!chosen?.stream_url) {
return null;
}
const result = {
url: chosen.stream_url,
headers: {
'User-Agent': USER_AGENT,
'Referer': `${DOMAIN}/`
},
server: 'purstream',
quality: chosen.source_name || 'Auto',
type: chosen.format || 'hls',
skipProxy: true
};
return result;
} catch (err) {
return null;
}
}
export async function getSources(args) {
const stream = await getStream(args);
return stream ? [stream] : [];
}
export const VERIFY_HEADERS = {
...HEADERS
};

115
src/sources/vidapi.js Normal file
View File

@@ -0,0 +1,115 @@
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
const API_BASE = 'https://streamdata.vaplayer.ru/api.php';
const EMBED_ORIGIN = 'https://nextgencloudfabric.com';
export const VERIFY_HEADERS = {
'User-Agent': USER_AGENT,
'Referer': `${EMBED_ORIGIN}/`,
'Origin': EMBED_ORIGIN
};
function extractStreamUrls(obj, urls = new Set()) {
if (!obj) return urls;
if (typeof obj === 'string') {
if (obj.startsWith('http') && (obj.includes('.m3u8') || obj.includes('.mp4') || obj.includes('.mpd'))) {
urls.add(obj);
}
} else if (Array.isArray(obj)) {
for (const item of obj) extractStreamUrls(item, urls);
} else if (typeof obj === 'object') {
for (const val of Object.values(obj)) {
extractStreamUrls(val, urls);
}
}
return urls;
}
export async function getStream({ id, s, e }) {
try {
const isTv = s != null && e != null;
const type = isTv ? 'tv' : 'movie';
const url = `${API_BASE}?tmdb=${id}&type=${type}${isTv ? `&season=${s}&episode=${e}` : ''}`;
const data = await fetchJson(url, {
headers: {
'Accept': '*/*',
'Origin': EMBED_ORIGIN,
'Referer': `${EMBED_ORIGIN}/`,
'User-Agent': USER_AGENT
},
signal: AbortSignal.timeout(10000)
});
const streamUrls = Array.from(extractStreamUrls(data));
if (!streamUrls.length) return null;
let subs = [];
if (data.subtitles && Array.isArray(data.subtitles)) {
subs = data.subtitles;
} else if (data.captions && Array.isArray(data.captions)) {
subs = data.captions;
}
const subtitles = subs.map(sub => ({
url: sub.url || sub.file || sub.src,
lang: sub.display || sub.label || sub.language || sub.lang || 'Unknown'
})).filter(s => s.url);
const allUrls = streamUrls.map(streamUrl => {
let type = 'mp4';
if (streamUrl.includes('.mpd')) type = 'dash';
else if (streamUrl.includes('.m3u8')) type = 'hls';
return {
url: streamUrl,
server: 'VidAPI',
quality: 'Auto',
type,
headers: VERIFY_HEADERS,
subtitles: subtitles.length > 0 ? subtitles : undefined,
skipProxy: false
};
});
return allUrls.length ? { allUrls } : null;
} catch {
return null;
}
}
export async function getSources() {
return ['VidAPI'];
}
export async function proxyStream({ url, res, proxyUtils: { fetchUpstream, rewriteM3u8 } }) {
try {
const upstream = await fetchUpstream(url, 0, VERIFY_HEADERS);
if (!upstream) return res.end();
const ct = (upstream.headers?.['content-type'] || '').toLowerCase();
const isM3u8 = ct.includes('mpegurl') || ct.includes('m3u8') || /\.m3u8?(\?|$)/i.test(url);
if (isM3u8) {
const chunks = [];
for await (const c of upstream) chunks.push(c);
let body = Buffer.concat(chunks).toString('utf8');
res.setHeader('Content-Type', 'application/vnd.apple.mpegurl');
res.setHeader('Access-Control-Allow-Origin', '*');
return res.end(rewriteM3u8(body, url, '&va=1&tt=1'));
}
res.setHeader('Content-Type', ct || 'application/octet-stream');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Cache-Control', 'public, max-age=3600');
upstream.pipe(res);
} catch {
if (!res.headersSent) {
res.statusCode = 502;
res.end();
}
}
}

43
src/sources/vidcore.js Normal file
View File

@@ -0,0 +1,43 @@
import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
const API_BASE = 'https://enc-dec.app/api';
const HEADERS = { 'User-Agent': USER_AGENT, 'Referer': 'https://vidcore.net/', 'X-Requested-With': 'XMLHttpRequest' };
async function getDynamicServers(id, s, e) {
try {
const html = await fetchText(s != null && e != null ? `https://vidcore.net/tv/${id}/${s}/${e}/` : `https://vidcore.net/movie/${id}/`, { headers: HEADERS });
const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/) || html.match(/"(?:en|token)":"(.*?)"/);
if (!match?.[1]) return null;
const encData = await fetchJson(`${API_BASE}/enc-vidcore?text=${encodeURIComponent(match[1])}`);
if (!encData?.result) return null;
const { servers: serversUrl, stream: streamUrl, token } = encData.result;
const reqHeaders = { ...HEADERS, 'X-CSRF-Token': token };
const serversEncrypted = await fetchText(serversUrl, { method: 'POST', headers: reqHeaders });
const decData = await fetchJson(`${API_BASE}/dec-vidcore`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: serversEncrypted }) });
return decData?.result ? { servers: decData.result, streamUrl, reqHeaders } : null;
} catch { return null; }
}
export async function getStream({ id, s, e }) {
const data = await getDynamicServers(id, s, e);
if (!data?.servers?.length) return null;
for (const srv of data.servers) {
try {
const streamEncrypted = await fetchText(`${data.streamUrl}/${srv.data}`, { method: 'POST', headers: data.reqHeaders });
const decData = await fetchJson(`${API_BASE}/dec-vidcore`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: streamEncrypted }) });
const url = decData?.result?.url;
if (url) {
const r = await fetch(url, { headers: { ...HEADERS, 'Range': 'bytes=0-511' }, signal: AbortSignal.timeout(5000) });
if (r.ok || r.status === 206) return { url, subtitles: (decData.result.captions || []).map(c => ({ url: c.file, lang: c.label || 'Unknown' })), headers: { ...HEADERS, 'Origin': 'https://vidcore.net' }, server: srv.name };
}
} catch { }
}
return null;
}
export async function getSources(args) {
const data = await getDynamicServers(args.id, args.s, args.e);
return data?.servers ? data.servers.map(s => s.name) : [];
}

98
src/sources/videasy.js Normal file
View File

@@ -0,0 +1,98 @@
import { getTmdbInfo, fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
const WINGS_BASE = "https://api.wingsdatabase.com";
const HEADERS = { "Accept": "*/*", "Origin": "https://player.videasy.to", "Referer": "https://player.videasy.to/", "User-Agent": USER_AGENT };
const SERVERS = [
{ id: 'jett', name: 'Jett' },
{ id: 'cdn', name: 'Yoru' },
{ id: 'tejo', name: 'Tejo' },
{ id: 'neon2', name: 'Neon' },
{ id: 'ym', name: 'Sage' },
{ id: 'downloader2', name: 'Cypher' },
{ id: 'm4uhd', name: 'Breach' },
{ id: 'hdmovie', name: 'Vyse' },
{ id: 'meine', name: 'Killjoy' },
{ id: 'lamovie', name: 'Omen' },
{ id: 'superflix', name: 'Raze' }
];
export async function getStream({ id, s, e, server }) {
try {
const isTv = s != null && e != null;
const info = await getTmdbInfo(id, isTv ? 'tv' : 'movie');
if (!info?.titles?.length) return null;
const seedData = await fetchJson(`${WINGS_BASE}/seed?mediaId=${id}`, { headers: HEADERS, signal: AbortSignal.timeout(5000) });
const seed = seedData?.seed;
if (!seed) return null;
const encTitle = encodeURIComponent(encodeURIComponent(info.titles[0]));
let targets = SERVERS;
if (server && server !== 'all') {
const cleanName = server.replace('VidEasy - ', '');
targets = SERVERS.filter(sv => sv.name === cleanName);
if (!targets.length) targets = SERVERS;
}
const settled = await Promise.allSettled(targets.map(async srv => {
if (srv.id === 'cdn' && isTv) throw new Error();
let url = `${WINGS_BASE}/${srv.id}/sources-with-title?title=${encTitle}&mediaType=${isTv ? 'tv' : 'movie'}&year=${info.year || ''}&tmdbId=${id}&imdbId=${info.imdbId || "tt0000000"}&enc=2&seed=${seed}`;
if (isTv) url += `&episodeId=${e}&seasonId=${s}`;
const encText = await fetchText(url, { headers: HEADERS, signal: AbortSignal.timeout(10000) });
const decJson = await fetchJson("https://enc-dec.app/api/dec-videasy", {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: encText, id: String(id), seed }),
signal: AbortSignal.timeout(10000)
});
if (decJson.status !== 200 || !decJson.result) throw new Error();
let sourcesArray = [];
let subsArray = [];
if (Array.isArray(decJson.result)) {
sourcesArray = decJson.result;
} else if (decJson.result.sources && Array.isArray(decJson.result.sources)) {
sourcesArray = decJson.result.sources;
if (decJson.result.subtitles && Array.isArray(decJson.result.subtitles)) {
subsArray = decJson.result.subtitles.map(sub => ({
url: sub.url || sub.file,
lang: sub.language || sub.lang || sub.label || 'Unknown'
})).filter(sub => sub.url);
}
} else {
sourcesArray = [decJson.result];
}
return sourcesArray.map(res => {
const streamUrl = res.url || res.file || res.link || res.playlist || res.stream;
return streamUrl ? {
url: streamUrl,
quality: res.quality || "Auto",
server: `VidEasy - ${srv.name}`,
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
headers: HEADERS,
subtitles: subsArray.length > 0 ? subsArray : undefined,
skipProxy: false,
skipVerify: true,
skipHlsCheck: true
} : null;
}).filter(Boolean);
}));
const allUrls = [];
for (const r of settled) {
if (r.status === 'fulfilled' && r.value) allUrls.push(...r.value);
}
return allUrls.length ? { allUrls } : null;
} catch { return null; }
}
export async function getSources() {
return SERVERS.map(s => `VidEasy - ${s.name}`);
}

53
src/sources/vidfast.js Normal file
View File

@@ -0,0 +1,53 @@
import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
const API_BASE = 'https://enc-dec.app/api';
const DOMAIN = 'https://vidfast.vc';
const HEADERS = { 'User-Agent': USER_AGENT, 'Referer': `${DOMAIN}/`, 'X-Requested-With': 'XMLHttpRequest' };
async function getVidfastMeta(id, s, e) {
try {
const embedUrl = s != null && e != null ? `${DOMAIN}/tv/${id}/${s}/${e}/` : `${DOMAIN}/movie/${id}/`;
const html = await fetchText(embedUrl, { headers: { 'User-Agent': USER_AGENT } });
const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/) || html.match(/"(?:en|token)":"(.*?)"/);
if (!match?.[1]) return null;
const encData = await fetchJson(`${API_BASE}/enc-vidfast?text=${encodeURIComponent(match[1])}`);
if (encData.status !== 200 || !encData.result) return null;
const { servers: serversUrl, stream: streamUrl, token } = encData.result;
const reqHeaders = { ...HEADERS, 'X-CSRF-Token': token };
const serversEncrypted = await fetchText(serversUrl, { method: 'POST', headers: reqHeaders });
const decServersData = await fetchJson(`${API_BASE}/dec-vidfast`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: serversEncrypted }) });
if (decServersData.status !== 200 || !decServersData.result) return null;
return { servers: decServersData.result, streamUrl, reqHeaders };
} catch { return null; }
}
export async function getStream({ id, s, e, server }) {
const meta = await getVidfastMeta(id, s, e);
if (!meta?.servers?.length) return null;
const { servers, streamUrl, reqHeaders } = meta;
let targets = servers;
if (server && server !== 'all') {
const cleanName = server.replace('VidFast - ', '');
targets = servers.filter(srv => srv.name === cleanName);
if (!targets.length) targets = servers;
}
const results = await Promise.allSettled(targets.map(async srv => {
const streamEncrypted = await fetchText(`${streamUrl}/${srv.data}`, { method: 'POST', headers: reqHeaders });
const decStreamData = await fetchJson(`${API_BASE}/dec-vidfast`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: streamEncrypted }) });
if (decStreamData.status !== 200 || !decStreamData.result?.url) throw new Error();
return { url: decStreamData.result.url, server: `VidFast - ${srv.name || 'Server'}`, type: decStreamData.result.url.includes('.m3u8') ? 'hls' : 'mp4', subtitles: (decStreamData.result.captions || []).map(c => ({ url: c.file, lang: c.label || 'Unknown' })), headers: { ...HEADERS, 'Origin': DOMAIN } };
}));
const allUrls = [];
for (const r of results) if (r.status === 'fulfilled') allUrls.push(r.value);
return allUrls.length ? { allUrls } : null;
}
export async function getSources(args) {
const meta = await getVidfastMeta(args.id, args.s, args.e);
return meta?.servers ? meta.servers.map(srv => `VidFast - ${srv.name}`) : [];
}

28
src/sources/vidlink.js Normal file
View File

@@ -0,0 +1,28 @@
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
const BASE = "https://vidlink.pro";
const HEADERS = { 'User-Agent': USER_AGENT, Origin: BASE, Referer: `${BASE}/` };
export async function getStream({ id, s, e }) {
try {
const encData = await fetchJson(`https://enc-dec.app/api/enc-vidlink?text=${id}`, { signal: AbortSignal.timeout(6000) });
if (encData?.status !== 200 || !encData?.result) return null;
const data = await fetchJson(s ? `${BASE}/api/b/tv/${encData.result}/${s}/${e || 1}` : `${BASE}/api/b/movie/${encData.result}`, { headers: HEADERS, signal: AbortSignal.timeout(10000) });
const stream = data?.stream;
if (!stream) return null;
if (stream.type === 'file' && stream.qualities) {
let picked = null;
for (const q of ['1080', '720', '480', '360']) if (stream.qualities[q]?.url) { picked = stream.qualities[q].url; break; }
if (!picked) picked = stream.qualities[Object.keys(stream.qualities)[0]]?.url;
if (picked) return { allUrls: [{ url: picked, type: 'mp4', audio: 'sub', server: 'Vidlink', skipHlsCheck: true, skipCache: true }] };
} else if (stream.playlist) {
return { allUrls: [{ url: stream.playlist, type: 'hls', audio: 'sub', server: 'Vidlink', skipHlsCheck: true, skipCache: true }] };
}
return null;
} catch { return null; }
}
export async function getSources(args) {
const stream = await getStream(args);
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
}

77
src/sources/vidnest.js Normal file
View File

@@ -0,0 +1,77 @@
import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
const BASE_URL = 'https://vidnest.fun';
const API_BASE_URL = 'https://new.vidnest.fun';
const REQUEST_HEADERS = { 'User-Agent': USER_AGENT, 'Accept': 'application/json, text/javascript, */*; q=0.01', 'Accept-Language': 'en-US,en;q=0.9', 'Referer': `${BASE_URL}/`, 'Origin': BASE_URL };
const CDN_PROXY_HEADERS = { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0', 'accept': '*/*', 'accept-language': 'en-US,en;q=0.5', 'origin': 'https://megaplay.buzz', 'referer': 'https://megaplay.buzz/' };
export const CDN_HEADERS = [
{ pattern: /letsgocdn\d+\.shop/i, headers: { 'Referer': BASE_URL, 'Origin': BASE_URL, 'User-Agent': USER_AGENT } },
{ pattern: /cdn\.mewstream\.buzz/i, headers: { 'Referer': BASE_URL, 'Origin': BASE_URL, 'User-Agent': USER_AGENT } }
];
const ALPHABET = 'RB0fpH8ZEyVLkv7c2i6MAJ5u3IKFDxlS1NTsnGaqmXYdUrtzjwObCgQP94hoeW+/=';
const REV_MAP = new Uint8Array(256);
for (let i = 0; i < ALPHABET.length; i++) REV_MAP[ALPHABET.charCodeAt(i)] = i;
function decrypt(payload) {
const len = payload.length;
const bytes = new Uint8Array(len);
let j = 0;
for (let i = 0; i < len; i += 4) {
const c0 = REV_MAP[payload.charCodeAt(i)] ?? 64;
const c1 = REV_MAP[payload.charCodeAt(i + 1)] ?? 64;
const c2 = REV_MAP[payload.charCodeAt(i + 2)] ?? 64;
const c3 = REV_MAP[payload.charCodeAt(i + 3)] ?? 64;
bytes[j++] = (c0 << 2) | (c1 >> 4);
if (c2 !== 64) bytes[j++] = ((c1 & 15) << 4) | (c2 >> 2);
if (c3 !== 64) bytes[j++] = ((c2 & 3) << 6) | c3;
}
return JSON.parse(new TextDecoder().decode(bytes.subarray(0, j)));
}
async function tmdbToAnilist(tmdbId, season) {
try {
const data = await fetchJson(`https://api.ani.zip/mappings?tmdb_id=${tmdbId}&type=tv&season=${season || 1}`, { signal: AbortSignal.timeout(6000) });
return data?.mappings?.[0]?.anilist_id || null;
} catch { return null; }
}
const SERVER_KEYS = ['hollymoviehd', 'allmovies', 'catflix', 'purstream', 'lamda', 'vidlink', 'klikxxi'];
export async function getStream({ id, s, e, tmdbApiKey, audio }) {
if (s) {
const k = tmdbApiKey;
const showData = k ? await fetchJson(`https://api.themoviedb.org/3/tv/${id}?api_key=${k}`, { signal: AbortSignal.timeout(5000) }).catch(() => null) : null;
if (showData?.genres?.some(g => g.id === 16)) {
const anilistId = await tmdbToAnilist(id, s);
if (anilistId) {
try {
const json = await fetchJson(`${API_BASE_URL}/hianime/anime/${anilistId}/${e || 1}/${audio === 'dub' ? 'dub' : 'sub'}`, { headers: REQUEST_HEADERS, signal: AbortSignal.timeout(15000) });
const data = json.encrypted ? decrypt(json.data) : json.data;
const file = data?.sources?.[0]?.file;
if (file) return { url: `https://megacloud.animanga.fun/proxy?url=${encodeURIComponent(file)}&headers=${encodeURIComponent(JSON.stringify(CDN_PROXY_HEADERS))}`, headers: REQUEST_HEADERS };
} catch { }
}
}
}
if (audio === 'dub') return null;
const segment = s ? `tv/${id}/${s}/${e || 1}` : `movie/${id}`;
const settled = await Promise.allSettled(SERVER_KEYS.map(async server => {
const json = await fetchJson(`${API_BASE_URL}/${server}/${segment}`, { headers: REQUEST_HEADERS, signal: AbortSignal.timeout(10000) });
const data = json.encrypted ? decrypt(json.data) : json.data;
const file = data?.sources?.[0]?.file ?? data?.streams?.[0]?.url ?? data?.url?.[0]?.link ?? data?.data?.stream?.playlist;
if (file) return { url: file, server, headers: REQUEST_HEADERS };
throw new Error();
}));
const allUrls = [];
for (const r of settled) if (r.status === 'fulfilled') allUrls.push(r.value);
return allUrls.length ? { allUrls } : null;
}
export async function getSources(args) {
const res = await getStream(args);
return res?.allUrls ? res.allUrls.map(u => u.server) : [];
}
export const VERIFY_HEADERS = { ...REQUEST_HEADERS };

95
src/sources/vidrift.js Normal file
View File

@@ -0,0 +1,95 @@
'use strict';
const BASE = "https://embed.vidrift.in";
const DEFAULT_HEADERS = {
"Accept": "*/*",
"Accept-Language": "en-US,en;q=0.9",
"Cache-Control": "no-cache",
"Pragma": "no-cache",
"Referer": `${BASE}/`,
"Origin": BASE,
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0"
};
function decodeStreams(encodedStr) {
try {
const base64Str = encodedStr.replace(/-/g, '+').replace(/_/g, '/');
const binaryStr = typeof atob === 'function'
? atob(base64Str)
: Buffer.from(base64Str, 'base64').toString('binary');
const bytes = new Uint8Array(binaryStr.length);
for (let i = 0; i < binaryStr.length; i++) {
bytes[i] = binaryStr.charCodeAt(i);
}
const key = bytes[0];
let decodedStr = '';
for (let i = 1; i < bytes.length; i++) {
decodedStr += String.fromCharCode(bytes[i] ^ key);
}
return JSON.parse(decodedStr);
} catch (e) {
return [];
}
}
export async function getStream(args) {
const { id, s, e, server: serverName } = args;
const type = s != null && e != null ? "tv" : "movie";
const endpoint = type === "tv"
? `${BASE}/embed/tv/${id}/${s}/${e}`
: `${BASE}/embed/movie/${id}`;
try {
const res = await fetch(endpoint, {
headers: {
...DEFAULT_HEADERS,
"Accept": "text/html",
},
signal: AbortSignal.timeout(10000)
});
if (!res.ok) return null;
const html = await res.text();
const match = html.match(/var\s+_s\s*=\s*['"]([^'"]+)['"]/);
if (!match) return null;
const encodedStreams = match[1];
const streamsData = decodeStreams(encodedStreams);
if (!Array.isArray(streamsData) || !streamsData.length) {
return null;
}
const allUrls = streamsData.map((stream, index) => ({
url: stream.url.startsWith("http")
? stream.url
: `${BASE}${stream.url}`,
server: stream.label || `VidRift - Server ${index + 1}`,
type: "hls",
headers: DEFAULT_HEADERS
}));
if (serverName && serverName !== "all") {
const filtered = allUrls.filter(x => x.server === serverName);
return filtered.length ? { allUrls: filtered } : null;
}
return { allUrls };
} catch (err) {
return null;
}
}
export async function getSources(args) {
const res = await getStream(args);
if (!res?.allUrls) return ["VidRift - Server 1"];
return res.allUrls.map(x => x.server);
}

145
src/sources/vidrock.js Normal file
View File

@@ -0,0 +1,145 @@
import { webcrypto } from 'node:crypto';
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
const GCM_HEX_KEY = '7f3e9c2a8b5d1f4e6a9c3b7d2e5f8a1c4b6d9e2f5a8c1b4d7e9f2a5c8b1d4e7f';
const BASE_URL = 'https://vidrock.ru/';
const PROXY_PREFIX = 'https://proxy.vidrock.store/';
let cryptoKey = null;
function hexToBytes(hex) {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
}
return bytes;
}
function base64UrlToBytes(value) {
let base64 = value.replace(/-/g, '+').replace(/_/g, '/');
const padding = base64.length % 4;
if (padding === 2) {
base64 += '==';
} else if (padding === 3) {
base64 += '=';
} else if (padding === 1) {
throw new Error('Invalid base64url');
}
return new Uint8Array(Buffer.from(base64, 'base64'));
}
async function getKey() {
if (cryptoKey) return cryptoKey;
const keyBytes = hexToBytes(GCM_HEX_KEY);
cryptoKey = await webcrypto.subtle.importKey(
'raw',
keyBytes,
'AES-GCM',
false,
['decrypt']
);
return cryptoKey;
}
async function decryptStreamUrl(value) {
try {
const data = base64UrlToBytes(value);
if (data.length < 28) {
throw new Error('Ciphertext too short');
}
const iv = data.slice(0, 12);
const ciphertext = data.slice(12);
const key = await getKey();
const decrypted = await webcrypto.subtle.decrypt(
{
name: 'AES-GCM',
iv
},
key,
ciphertext
);
return new TextDecoder().decode(decrypted);
} catch (error) {
return null;
}
}
export async function getStream({ id, s, e }) {
try {
const path = s
? `tv/${id}/${s}/${e || 1}`
: `movie/${id}`;
const apiData = await fetchJson(`${BASE_URL}api/${path}`, {
headers: {
'User-Agent': USER_AGENT,
Referer: BASE_URL,
Origin: BASE_URL
}
});
if (!apiData || typeof apiData !== 'object') {
return null;
}
const streams = [];
for (const [provider, info] of Object.entries(apiData)) {
if (!info?.url) {
continue;
}
const decryptedUrl = await decryptStreamUrl(info.url);
if (!decryptedUrl) {
continue;
}
let url = decryptedUrl;
if (url.startsWith(PROXY_PREFIX)) {
url = decodeURIComponent(
url.slice(PROXY_PREFIX.length).replace(/^\/+/, '')
);
}
const isXpass = url.includes('xpass.top');
streams.push({
name: provider,
url,
file: url,
stream: url,
src: url,
quality: info.type || 'auto',
type: info.type || 'hls',
headers: {
'User-Agent': USER_AGENT,
Referer: isXpass
? 'https://play.xpass.top/'
: BASE_URL,
Origin: isXpass
? 'https://play.xpass.top'
: 'https://vidrock.ru'
}
});
}
return streams.length ? { streams } : null;
} catch (error) {
return null;
}
}

38
src/sources/vidsync.js Normal file
View File

@@ -0,0 +1,38 @@
import { fetchJson, fetchText, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
const API_BASE = "https://enc-dec.app/api";
const DOMAIN = "https://vidsync.xyz";
const HEADERS = { "Accept": "*/*", "Origin": DOMAIN, "Referer": `${DOMAIN}/`, "User-Agent": USER_AGENT, "X-Requested-With": "XMLHttpRequest" };
const SERVERS = ["cinevault", "cinedub", "cinebox", "cineflix", "cinevip", "cinecloud", "cine4k"];
export async function getStream({ id, s, e, server }) {
try {
const isTv = s != null && e != null;
const type = isTv ? 'tv' : 'movie';
const info = await getTmdbInfo(id, type);
if (!info?.titles?.length) return null;
const turnstileJson = await fetchJson(`${API_BASE}/enc-vidsync`, { signal: AbortSignal.timeout(5000) });
if (turnstileJson.status !== 200 || !turnstileJson.result?.token) return null;
const turnstileToken = turnstileJson.result.token;
let targets = SERVERS;
if (server && server !== 'all') {
const clean = server.replace('VidSync - ', '');
targets = SERVERS.includes(clean) ? [clean] : SERVERS;
}
const encTitle = encodeURIComponent(info.titles[0]).replace(/%20/g, '+');
const results = await Promise.allSettled(targets.map(async srv => {
let url = `${DOMAIN}/api/stream/fetch?title=${encTitle}&type=${type}&releaseYear=${info.year}&mediaId=${id}&serverName=${srv}`;
if (isTv) url += `&season=${s}&episode=${e}`;
const encryptedText = await fetchText(url, { headers: { ...HEADERS, "X-Cf-Turnstile": turnstileToken }, signal: AbortSignal.timeout(10000) });
const decJson = await fetchJson(`${API_BASE}/dec-vidsync`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: encryptedText, id: String(id) }), signal: AbortSignal.timeout(10000) });
if (decJson.status !== 200 || !decJson.result) throw new Error();
const streams = Array.isArray(decJson.result) ? decJson.result : [decJson.result];
return streams.map(st => ({ url: st.url || st.file, server: `VidSync - ${srv}`, quality: st.quality || "Auto", type: (st.url || st.file || "").includes(".m3u8") ? "hls" : "mp4", headers: { ...HEADERS, "Origin": DOMAIN }, skipProxy: false, skipVerify: true, skipHlsCheck: true }));
}));
const allUrls = [];
for (const r of results) if (r.status === 'fulfilled') allUrls.push(...r.value);
return allUrls.length ? { allUrls } : null;
} catch { return null; }
}
export async function getSources() { return SERVERS.map(s => `VidSync - ${s}`); }

79
src/sources/vidup.js Normal file
View File

@@ -0,0 +1,79 @@
import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
const API_BASE = 'https://enc-dec.app/api';
const DOMAIN = 'https://vidup.to';
const HEADERS = { 'User-Agent': USER_AGENT, 'Referer': `${DOMAIN}/`, 'X-Requested-With': 'XMLHttpRequest' };
async function getVidupMeta(id, s, e) {
try {
const embedUrl = s != null && e != null ? `${DOMAIN}/tv/${id}/${s}/${e}/` : `${DOMAIN}/movie/${id}/`;
const html = await fetchText(embedUrl, { headers: { 'User-Agent': USER_AGENT } });
const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/) || html.match(/"(?:en|token)":"(.*?)"/);
if (!match?.[1]) return null;
const encData = await fetchJson(`${API_BASE}/enc-vidup?text=${encodeURIComponent(match[1])}`);
if (encData.status !== 200 || !encData.result) return null;
const { servers: serversUrl, stream: streamUrl, token } = encData.result;
const reqHeaders = { ...HEADERS, 'X-CSRF-Token': token };
const serversEncrypted = await fetchText(serversUrl, { method: 'POST', headers: reqHeaders });
const decServersData = await fetchJson(`${API_BASE}/dec-vidup`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: serversEncrypted })
});
if (decServersData.status !== 200 || !decServersData.result) return null;
return { servers: decServersData.result, streamUrl, reqHeaders };
} catch { return null; }
}
export async function getStream({ id, s, e, server }) {
const meta = await getVidupMeta(id, s, e);
if (!meta?.servers?.length) return null;
const { servers, streamUrl, reqHeaders } = meta;
let targets = servers;
if (server && server !== 'all') {
const cleanName = server.replace('VidUp - ', '');
targets = servers.filter(srv => srv.name === cleanName);
if (!targets.length) targets = servers;
}
const results = await Promise.allSettled(targets.map(async srv => {
const streamEncrypted = await fetchText(`${streamUrl}/${srv.data}`, { method: 'POST', headers: reqHeaders });
const decStreamData = await fetchJson(`${API_BASE}/dec-vidup`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: streamEncrypted })
});
if (decStreamData.status !== 200 || !decStreamData.result?.url) throw new Error();
return {
url: decStreamData.result.url,
server: `VidUp - ${srv.name || 'Server'}`,
type: decStreamData.result.url.includes('.m3u8') ? 'hls' : 'mp4',
subtitles: (decStreamData.result.captions || []).map(c => ({
url: c.file,
lang: c.label || 'Unknown'
})),
headers: { ...HEADERS, 'Origin': DOMAIN }
};
}));
const allUrls = [];
for (const r of results) {
if (r.status === 'fulfilled') allUrls.push(r.value);
}
return allUrls.length ? { allUrls } : null;
}
export async function getSources(args) {
const meta = await getVidupMeta(args.id, args.s, args.e);
return meta?.servers ? meta.servers.map(srv => `VidUp - ${srv.name}`) : [];
}

120
src/sources/vidzee.js Normal file
View File

@@ -0,0 +1,120 @@
import { webcrypto } from 'node:crypto';
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
const PLAYER_URL = 'https://player.vidzee.wtf';
const CORE_URL = 'https://core.vidzee.wtf';
let cachedKey = null, cachedKeyTs = 0;
async function getDecKey(headers) {
const now = Date.now();
if (cachedKey && now - cachedKeyTs < 300000) return cachedKey;
const res = await fetch(`${CORE_URL}/api-key`, {
headers,
signal: AbortSignal.timeout(7000)
});
if (!res.ok) throw new Error(`API key fetch failed: ${res.status}`);
const e = await res.text();
const t = Buffer.from(e.replace(/\s+/g, ''), 'base64');
const s = new Uint8Array(t.length - 28 + 16);
s.set(t.subarray(28), 0);
s.set(t.subarray(12, 28), t.length - 28);
const keyMat = await webcrypto.subtle.digest('SHA-256', new TextEncoder().encode('c4a8f1d7e2b9a6c3d0f5e8a1b7c4d9e2'));
const key = await webcrypto.subtle.importKey('raw', keyMat, { name: 'AES-GCM' }, false, ['decrypt']);
try {
const decrypted = await webcrypto.subtle.decrypt({
name: 'AES-GCM',
iv: t.subarray(0, 12),
tagLength: 128
}, key, s);
cachedKey = new TextDecoder().decode(decrypted);
cachedKeyTs = now;
return cachedKey;
} catch (err) {
throw new Error('Failed to decrypt Vidzee master key');
}
}
function decrypt(enc, key) {
try {
const dec = Buffer.from(enc, 'base64').toString('utf8');
const sep = dec.indexOf(':');
if (sep === -1) return '';
const ivBase64 = dec.slice(0, sep);
const ciphertext = dec.slice(sep + 1);
const keyPadded = key.padEnd(32, '\0');
const decrypted = CryptoJS.AES.decrypt(
ciphertext,
CryptoJS.enc.Utf8.parse(keyPadded),
{
iv: CryptoJS.enc.Base64.parse(ivBase64),
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
);
return decrypted.toString(CryptoJS.enc.Utf8) || '';
} catch (err) {
return '';
}
}
export async function getStream({ id, s, e, clientIP }) {
const headers = {
'User-Agent': USER_AGENT,
'Accept': 'application/json',
'Referer': PLAYER_URL,
'Origin': PLAYER_URL,
...(clientIP && { 'X-Forwarded-For': clientIP })
};
try {
const decKey = await getDecKey(headers);
const serversToTry = [3, 0, 1, 2, 4, 5, 6, 8];
for (const sr of serversToTry) {
try {
const url = `${PLAYER_URL}/api/server?id=${id}&sr=${sr}${s ? `&ss=${s}&ep=${e || 1}` : ''}`;
const data = await fetchJson(url, { headers, signal: AbortSignal.timeout(5000) });
if (data?.url?.length) {
for (const entry of data.url) {
if (!entry.link) continue;
const decrypted = decrypt(entry.link, decKey);
if (decrypted?.startsWith('http')) {
return {
allUrls: [
{
url: decrypted,
skipHlsCheck: true,
headers: {
'User-Agent': USER_AGENT,
'Referer': PLAYER_URL,
'Origin': PLAYER_URL
}
}
]
};
}
}
}
} catch (err) {
continue;
}
}
return null;
} catch (err) {
return null;
}
}

98
src/sources/vixsrc.js Normal file
View File

@@ -0,0 +1,98 @@
import { fetchText, USER_AGENT } from '../utils/helpers.js';
const BASE_URL = 'https://vixsrc.to';
const HEADERS = {
'User-Agent': USER_AGENT,
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Language': 'en-US,en;q=0.9',
'Referer': `${BASE_URL}/`,
'Origin': BASE_URL
};
function extract(regex, text) {
const m = text?.match(regex);
if (!m) return null;
return m[1].replace(/\\/g, '');
}
export async function getStream({ id, s, e, absoluteBase }) {
try {
const apiUrl = s && e
? `${BASE_URL}/api/tv/${id}/${s}/${e}`
: `${BASE_URL}/api/movie/${id}`;
const apiProxy = absoluteBase
? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(apiUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}`
: apiUrl;
const apiText = await fetchText(apiProxy).catch(() => null);
const apiData = JSON.parse(apiText || '{}');
if (!apiData.src) return null;
const embedUrl = apiData.src.startsWith('http') ? apiData.src : `${BASE_URL}${apiData.src}`;
const embedProxy = absoluteBase
? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(embedUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}`
: embedUrl;
const html = await fetchText(embedProxy).catch(() => null);
if (!html) return null;
const token = extract(/token["']\s*:\s*["']([^"']+)/, html);
const expires = extract(/expires["']\s*:\s*["']([^"']+)/, html);
let playlist = extract(/url["']\s*:\s*["']([^"']+)/, html);
if (!token || !expires || !playlist) return null;
playlist = playlist.replace(/\\/g, '');
const lang = extract(/lang(?:uage)?["']\s*:\s*["']([a-z]{2,5})/i, html) || 'en';
const rawMasterUrl = `${playlist}${playlist.includes('?') ? '&' : '?'}token=${token}&expires=${expires}&h=1&lang=${lang}`;
const masterProxy = absoluteBase
? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(rawMasterUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify(HEADERS))}`
: rawMasterUrl;
const playlistText = await fetchText(rawMasterUrl, { headers: HEADERS }).catch(() => null);
if (!playlistText?.includes('#EXTM3U')) return null;
let finalUrl = null;
let bestHeight = 0;
const lines = playlistText.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (!line.startsWith('#EXT-X-STREAM-INF')) continue;
const height = parseInt(line.match(/RESOLUTION=\d+x(\d+)/)?.[1] || '0', 10);
let next = lines[i + 1]?.trim();
if (!next || next.startsWith('#')) continue;
if (height > bestHeight) {
bestHeight = height;
finalUrl = next;
}
}
if (!finalUrl) finalUrl = rawMasterUrl;
if (!finalUrl.startsWith('http')) {
const base = rawMasterUrl.split('/playlist/')[0];
finalUrl = finalUrl.startsWith('/') ? `${base}${finalUrl}` : `${base}/${finalUrl}`;
}
const alreadyProxied = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?\/api\?url=/.test(finalUrl);
return {
allUrls: [
{
url: finalUrl,
skipProxy: alreadyProxied,
headers: {
...HEADERS,
'Referer': `${BASE_URL}/`
}
}
]
};
} catch (err) {
return null;
}
}

40
src/sources/xpass.js Normal file
View File

@@ -0,0 +1,40 @@
import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
const BASE = "https://play.xpass.top";
const HEADERS = { "accept": "*/*", "accept-language": "en-US,en;q=0.9", "cache-control": "no-cache", "pragma": "no-cache", "User-Agent": USER_AGENT, "Origin": BASE, "Referrer": `${BASE}/`, "Cookie": "auth_token=de21073d24bca9b50f189b402ac870734cf945f2085cb7e1a4fc453fcfe4f57e" };
export async function getStream({ id, s, e, server }) {
const isTv = s && e && s !== 'null' && e !== 'null';
const hdrs = { ...HEADERS, referer: `${BASE}/e/${isTv ? 'tv' : 'movie'}/${id}?autostart=true` };
try {
let sources = [];
if (!isTv) {
const text = await fetchText(`${BASE}/e/movie/${id}?autostart=true`, { headers: hdrs, signal: AbortSignal.timeout(7000) });
const match = text.match(/var backups=(\[[\s\S]*?\])/);
if (match) sources = JSON.parse(match[1]);
} else {
sources = await fetchJson(`${BASE}/data/tv/${id}/${s}/${e}?autostart=true&force=true`, { headers: hdrs, signal: AbortSignal.timeout(7000) });
}
if (!sources?.length) return null;
if (server && server !== 'all') {
const exact = sources.find(src => src.name === server.replace('XPass - ', ''));
if (exact) sources = [exact]; else return null;
}
const allUrls = [];
for (const source of sources) {
if (!source.url) continue;
try {
const mdata = await fetchJson(`${BASE}${source.url}`, { headers: hdrs, signal: AbortSignal.timeout(5000) });
if (!mdata?.playlist?.[0]?.sources) continue;
const targetStream = mdata.playlist[0].sources.find(st => st.type === 'hls') || mdata.playlist[0].sources[0];
if (targetStream?.file) allUrls.push({ server: `XPass - ${source.name}`, type: targetStream.type === 'hls' ? 'hls' : 'mp4', url: targetStream.file, headers: { "Origin": HEADERS.Origin, "Referer": HEADERS.Referrer, "User-Agent": HEADERS["User-Agent"] } });
} catch { }
}
return allUrls.length ? { allUrls } : null;
} catch { return null; }
}
export async function getSources(args) {
const stream = await getStream(args);
return stream?.allUrls ? stream.allUrls.map(u => u.server) : [];
}

127
src/subtitles.js Normal file
View File

@@ -0,0 +1,127 @@
import { getUA } from './utils/helpers.js';
const SUBTITLE_BASES = [
'https://sub.vdrk.site/v1',
'https://sub.vdrk.site/v2',
'https://fed-subs.pstream.mov'
];
const VYLIAN_MESSAGES = [
"Thanks for using Vyla!"
];
function generateVylianVtt() {
let vttContent = 'WEBVTT\n\n';
let currentTime = 1;
while (currentTime <= 7200) {
const duration = Math.floor(Math.random() * 4) + 1;
const message = VYLIAN_MESSAGES[Math.floor(Math.random() * VYLIAN_MESSAGES.length)];
const startTime = formatVttTime(currentTime);
const endTime = formatVttTime(currentTime + duration);
vttContent += `${startTime} --> ${endTime}\n${message}\n\n`;
currentTime += duration + Math.floor(Math.random() * 60) + 30;
}
return vttContent;
}
function formatVttTime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}.000`;
}
export async function fetchSubtitles(paths = []) {
try {
const results = await Promise.all(
paths.map(async ({ base, path }) => {
try {
const res = await fetch(`${base}${path}`, {
headers: { 'User-Agent': getUA() },
signal: AbortSignal.timeout(5000),
});
if (!res.ok) {
res.body?.cancel();
return [];
}
const data = await res.json();
if (base.includes('/v2')) {
return Array.isArray(data)
? data.map(x => ({
label: x.label,
file: x.file || x.url,
type: 'vtt',
source: 'v2'
}))
: [];
}
if (base.includes('fed-subs.pstream.mov')) {
if (!data?.subtitles || typeof data.subtitles !== 'object') return [];
return Object.entries(data.subtitles)
.map(([language, sub]) => {
if (!sub?.subtitle_link) return null;
const ext = sub.subtitle_link.split('.').pop()?.toLowerCase();
return {
label: sub.subtitle_name || language,
file: sub.subtitle_link,
type: ext === 'vtt' ? 'vtt' : 'srt',
source: 'febbox'
};
})
.filter(Boolean);
}
const v1 = Array.isArray(data) ? data : [];
return v1.map(x => ({
label: x.label,
file: x.file || x.url,
type: 'vtt',
source: 'v1'
}));
} catch {
return [];
}
})
);
const val = results.flat();
val.push({
label: 'Vylian',
file: 'data:text/vtt;base64,' + Buffer.from(generateVylianVtt()).toString('base64'),
type: 'vtt',
source: 'easter-egg'
});
return val;
} catch {
return [];
}
}
export async function getSubPathsMovie(id) {
return [
{ base: SUBTITLE_BASES[0], path: `/movie/${id}` },
{ base: SUBTITLE_BASES[1], path: `/movie/${id}` },
{ base: SUBTITLE_BASES[2], path: `/movie/tt${id}` }
];
}
export async function getSubPathsTv(id, season, episode) {
return [
{ base: SUBTITLE_BASES[0], path: `/tv/${id}/${season}/${episode}` },
{ base: SUBTITLE_BASES[1], path: `/tv/${id}/${season}/${episode}` },
{ base: SUBTITLE_BASES[2], path: `/tv/tt${id}/${season}/${episode}` }
];
}
export { SUBTITLE_BASES };

194
src/utils/helpers.js Normal file
View File

@@ -0,0 +1,194 @@
import crypto from 'node:crypto';
export const UA_LIST = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
'Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
];
export const getUA = () => UA_LIST[Math.floor(Math.random() * UA_LIST.length)];
export const USER_AGENT = UA_LIST[0];
let sharedDcKey = null;
export function decryptPartner(responseText, keyEnv) {
try {
const sep = responseText.indexOf(':');
if (sep === -1) return null;
const iv = Buffer.from(responseText.slice(0, sep), 'base64');
const encrypted = responseText.slice(sep + 1);
if (!sharedDcKey) sharedDcKey = crypto.createHash('sha256').update(keyEnv || '').digest();
const decipher = crypto.createDecipheriv('aes-256-cbc', sharedDcKey, iv);
return JSON.parse(decipher.update(encrypted, 'base64', 'utf8') + decipher.final('utf8'));
} catch {
return null;
}
}
export async function fetchJson(url, options) {
const res = await fetch(url, options);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
export async function fetchText(url, options) {
const res = await fetch(url, options);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.text();
}
const tmdbInfoCache = new Map();
const anilistCache = new Map();
const tmdbValidationCache = new Map();
function pruneHelperCache(cache) {
const now = Date.now();
for (const [k, v] of cache) if (now - v.ts >= v.ttl) cache.delete(k);
if (cache.size > 3000) {
const overflow = cache.size - 3000;
const it = cache.keys();
for (let i = 0; i < overflow; i++) cache.delete(it.next().value);
}
}
setInterval(() => {
pruneHelperCache(tmdbValidationCache);
pruneHelperCache(tmdbInfoCache);
pruneHelperCache(anilistCache);
}, 60000).unref();
function cacheGet(cache, key) {
const entry = cache.get(key);
if (!entry) return undefined;
if (Date.now() - entry.ts >= entry.ttl) { cache.delete(key); return undefined; }
return entry.val;
}
function cacheSet(cache, key, val, ttl) { cache.set(key, { val, ts: Date.now(), ttl }); }
export async function createStreamArgs(source, tmdbApiKey, id, s, e, clientIP=null) {
const getAbsoluteBase = host => (host.startsWith('localhost') || host.startsWith('127.0.0.1')) ? `http://${host}` : `https://${host}`;
const audio = /dub$/.test(source.key) ? 'dub' : 'sub';
return {
id: id,
s: s,
e: e,
clientIP: clientIP,
absoluteBase: '/',
audio,
tmdbApiKey,
config: source,
}
}
export async function validateTmdbId(tmdbApiKey, tmdbId, mediaType = 'movie') {
const key = `${tmdbId}-${mediaType}`;
const cached = cacheGet(tmdbValidationCache, key);
if (cached !== undefined) return cached;
const k = tmdbApiKey;
if (!k) return { valid: false, error: 'TMDB API key not configured' };
try {
const res = await fetch(`https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${k}`, { signal: AbortSignal.timeout(5000) });
if (res.ok) {
const data = await res.json();
const isValid = !!(data?.id && (data?.title || data?.name));
const result = { valid: isValid, error: isValid ? null : 'Invalid TMDB ID' };
cacheSet(tmdbValidationCache, key, result, 300000);
return result;
}
const result = { valid: false, error: `TMDB API error: ${res.status}` };
cacheSet(tmdbValidationCache, key, result, 60000);
return result;
} catch (error) {
const result = { valid: false, error: `Failed to validate TMDB ID: ${error.message}` };
cacheSet(tmdbValidationCache, key, result, 60000);
return result;
}
}
export async function getTmdbInfo(tmdbApiKey, tmdbId, mediaType, season) {
const key = `${tmdbId}-${mediaType}-${season || ''}`;
const cached = cacheGet(tmdbInfoCache, key);
if (cached !== undefined) return cached;
const k = tmdbApiKey;
if (!k) return { isAnime: false, titles: [], year: null, imdbId: null };
try {
const [mainRes, seasonRes] = await Promise.all([
fetch(`https://api.themoviedb.org/3/${mediaType}/${tmdbId}?api_key=${k}&append_to_response=external_ids`, { signal: AbortSignal.timeout(5000) }),
season && mediaType === 'tv' ? fetch(`https://api.themoviedb.org/3/tv/${tmdbId}/season/${season}?api_key=${k}`, { signal: AbortSignal.timeout(5000) }) : Promise.resolve(null),
]);
let mainData = null, seasonData = null;
if (mainRes.ok) mainData = await mainRes.json(); else mainRes.body?.cancel();
if (seasonRes) { if (seasonRes.ok) seasonData = await seasonRes.json(); else seasonRes.body?.cancel(); }
const genres = mainData?.genres || [];
const isAnime = genres.some(g => g.id === 16) && ((mainData?.origin_country || []).includes('JP') || mainData?.original_language === 'ja');
const titles = [];
if (seasonData?.name) titles.push(seasonData.name);
const t = mainData?.title || mainData?.name || '';
const ot = mainData?.original_title || mainData?.original_name || '';
if (t) titles.push(t);
if (ot && ot !== t) titles.push(ot);
const dateStr = seasonData?.air_date || mainData?.release_date || mainData?.first_air_date || '';
const result = { isAnime, titles: [...new Set(titles.filter(Boolean))], year: dateStr ? parseInt(dateStr.slice(0, 4), 10) : null, imdbId: mainData?.imdb_id || mainData?.external_ids?.imdb_id || null };
cacheSet(tmdbInfoCache, key, result, 600000);
return result;
} catch { return { isAnime: false, titles: [], year: null, imdbId: null }; }
}
export async function tmdbToAnilist(tmdbId, mediaType, season, titles = [], year = null) {
const key = `${tmdbId}-${mediaType}-${season || ''}`;
const cached = cacheGet(anilistCache, key);
if (cached !== undefined) return cached;
try {
const res = await fetch(`https://api.ani.zip/mappings?tmdb_id=${tmdbId}&type=${mediaType}&season=${season || 1}`, { signal: AbortSignal.timeout(6000) });
if (res.ok) {
const id = (await res.json())?.mappings?.[0]?.anilist_id;
if (id) { cacheSet(anilistCache, key, id, 600000); return id; }
} else res.body?.cancel();
} catch { }
if (!titles.length) return null;
const query = `query ($s: String) { Page(page:1,perPage:10) { media(search:$s,type:ANIME) { id title { romaji english native } startDate { year } format } } }`;
let bestId = null, bestScore = -1;
for (const searchTitle of titles) {
try {
const res = await fetch('https://graphql.anilist.co', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ query, variables: { s: searchTitle } }), signal: AbortSignal.timeout(6000) });
if (!res.ok) { res.body?.cancel(); continue; }
for (const entry of (await res.json())?.data?.Page?.media || []) {
const entryTitles = [entry.title?.romaji, entry.title?.english, entry.title?.native].filter(Boolean).map(t => t.toLowerCase());
const sl = searchTitle.toLowerCase();
let score = 0;
if (entryTitles.some(t => t === sl)) score += 5;
else if (entryTitles.some(t => t.includes(sl) || sl.includes(t))) score += 3;
else if (entryTitles.some(t => { const w = sl.split(/\s+/).filter(x => x.length > 3); return w.length > 0 && w.every(x => t.includes(x)); })) score += 2;
else continue;
if (year && entry.startDate?.year) {
const diff = Math.abs(entry.startDate.year - year);
if (diff === 0) score += 3; else if (diff === 1) score += 1; else if (diff > 2) score -= 3;
}
if (mediaType === 'tv' && ['TV', 'TV_SHORT', 'ONA', 'OVA'].includes(entry.format)) score += 1;
if (mediaType === 'movie' && entry.format === 'MOVIE') score += 1;
if (score > bestScore) { bestScore = score; bestId = entry.id; }
}
if (bestScore >= 8) break;
} catch { }
}
if (bestId) cacheSet(anilistCache, key, bestId, 600000);
return bestId;
}
export async function imdbToTmdb(tmdbApiKey, imdbId, mediaType = 'movie') {
const key = `imdb-${imdbId}`;
const cached = cacheGet(tmdbInfoCache, key);
if (cached !== undefined) return cached;
const k = tmdbApiKey;
if (!k) return null;
try {
const data = await fetchJson(`https://api.themoviedb.org/3/find/${imdbId}?api_key=${k}&external_source=imdb_id`, { signal: AbortSignal.timeout(5000) });
let result = null;
if (mediaType === 'movie' && data.movie_results?.length) result = data.movie_results[0].id;
else if (mediaType === 'tv' && data.tv_results?.length) result = data.tv_results[0].id;
if (result) cacheSet(tmdbInfoCache, key, result, 600000);
return result;
} catch { return null; }
}