Fixed all sources, rewrote, made test easier for developers to test everything

This commit is contained in:
endoverdosing
2026-07-30 09:25:08 -07:00
parent 902326bdcd
commit 3247f3a731
19 changed files with 705 additions and 577 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
node_modules/
.env
test/debug-output.json

View File

@@ -2,3 +2,4 @@
.env
test/
node_modules/
test/debug-output.json

View File

@@ -1,4 +1,4 @@
// Currently active sources reported on 7:47:50 PM PST 7/6/2026
// Currently active sources reported on 9:22:41 PM PST 7/30/2026
// None Anime Tested: 936075
// Anime Tested: 37854
@@ -135,7 +135,7 @@ export const SOURCES = [
jitter: 500,
retries: 2,
multiUrl: true,
disabled: true, // Temporarily disabled because their site is down, all streams are returning 403's
skipProxy: true,
cdnHeaders: [{
pattern: /flix2watch\.pro/i,
headers: {
@@ -161,18 +161,6 @@ export const SOURCES = [
},
},
{
key: 'goated',
label: 'Goated',
sourceFile: 'goated',
proxyParam: 'gt',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
skipProxy: true,
},
{
key: 'fsonic',
label: 'Fsonic',
@@ -216,6 +204,17 @@ export const SOURCES = [
retries: 1,
},
{
key: 'lmscript',
label: 'LMScript',
sourceFile: 'lmscript',
proxyParam: 'lmsc',
timeout: 20000,
jitter: 500,
retries: 2,
multiUrl: true,
},
{
key: 'lookmovie',
label: 'LookMovie',
@@ -252,38 +251,6 @@ export const SOURCES = [
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',
@@ -332,6 +299,7 @@ export const SOURCES = [
timeout: 20000,
jitter: 500,
retries: 2,
skipProxy: true
},
{
@@ -485,6 +453,17 @@ export const SOURCES = [
multiUrl: true
},
{
key: 'vidvault',
label: 'VidVault',
sourceFile: 'vidvault',
proxyParam: 'vv',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
},
{
key: 'vidzee',
label: 'VidZee',
@@ -494,13 +473,13 @@ export const SOURCES = [
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',
},
disabled: true // Disabled temporarily due to decryption issues
},
{

1
src/sdk.d.ts vendored
View File

@@ -59,7 +59,6 @@ export interface VylaSDKOptions {
tmdbApiKey?: string;
}
// Descriptions written with Claude.
export default class VylaSDK {
tmdbApiKey?: string;

View File

@@ -3,13 +3,11 @@ 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" }
{ id: "BUKE", name: "1Embed - BUKE" },
{ id: "YOTA", name: "1Embed - YOTA" },
{ id: "NEPT", name: "1Embed - NEPT" },
{ id: "AZURE", name: "1Embed - AZURE" },
{ id: "YORU", name: "1Embed - YORU" }
];
let cachedToken = null;
@@ -28,6 +26,30 @@ async function getToken() {
} catch { return null; }
}
function decodeBsUrl(url) {
if (!url) return url;
let target = url;
if (url.includes('d=')) {
try {
const parts = url.split(/[?&]/);
for (const part of parts) {
if (part.startsWith('d=')) {
target = decodeURIComponent(part.substring(2));
break;
}
}
} catch { }
}
if (target.startsWith('bs_')) {
try {
const decoded = Buffer.from(target.slice(3), 'base64').toString('utf-8');
const reversed = decoded.split('').reverse().join('');
if (reversed.startsWith('http')) return reversed;
} catch { }
}
return url;
}
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}`;
@@ -51,11 +73,12 @@ async function fetchProviderStream(provider, type, id, s, e, token) {
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;
let rawUrl = src.url || src.file || src.link;
if (typeof rawUrl !== 'string') continue;
let streamUrl = decodeBsUrl(rawUrl);
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;
const isHls = src.type === 'hls' || src.type === 'm3u8' || 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({
@@ -80,7 +103,7 @@ export async function getStream(args) {
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));
targets = PROVIDERS.filter(p => p.id.toLowerCase() === 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)));

View File

@@ -2,15 +2,27 @@ import { getTmdbInfo, USER_AGENT } from '../utils/helpers.js';
async function extractVoe(voeUrl) {
try {
const res = await fetch(voeUrl, {
let res = await fetch(voeUrl, {
headers: {
'User-Agent': USER_AGENT,
'Referer': 'https://anihq.cc/'
}
});
let html = await res.text();
const redirectMatch = html.match(/window\.location\.href\s*=\s*['"](https?:\/\/[^'"]+)['"]/);
if (redirectMatch) {
res = await fetch(redirectMatch[1], {
headers: {
'User-Agent': USER_AGENT,
'Referer': 'https://anihq.cc/'
}
});
html = await res.text();
}
const mirrorUrl = res.url;
const html = await res.text();
const scriptMatch = html.match(/<script type="application\/json">\["(.*?)"\]<\/script>/);
if (!scriptMatch) return null;

View File

@@ -1,4 +1,4 @@
import { getTmdbInfo } from '../utils/helpers.js';
import { getTmdbInfo, USER_AGENT, fetchText } from '../utils/helpers.js';
const BASE_URL = "https://anineko.to";
@@ -10,7 +10,10 @@ function decodeEntities(str) {
async function search(query) {
try {
const html = await fetchText(`${BASE_URL}/browser?keyword=${encodeURIComponent(query)}`, { signal: AbortSignal.timeout(6000) });
const html = await fetchText(`${BASE_URL}/browser?keyword=${encodeURIComponent(query)}`, {
headers: { 'User-Agent': USER_AGENT },
signal: AbortSignal.timeout(6000)
});
const results = [];
let idx = 0;
while ((idx = html.indexOf('nv-anime-thumb', idx)) !== -1) {
@@ -27,40 +30,144 @@ async function search(query) {
}
idx += 14;
}
if (results.length === 0) {
const linkMatches = [...html.matchAll(/href=["']\/watch\/([^"']+)["']/gi)];
for (const match of linkMatches) {
if (!results.find(r => r.slug === match[1])) {
const linkIdx = html.indexOf(match[0]);
const titleStart = html.lastIndexOf('>', linkIdx);
const titleEnd = html.indexOf('<', linkIdx);
if (titleStart !== -1 && titleEnd !== -1 && titleEnd > linkIdx) {
const title = html.slice(titleStart + 1, Math.min(titleEnd, linkIdx + 200)).trim();
if (title.length > 2 && title.length < 100) {
results.push({ slug: match[1], text: title });
}
}
}
}
}
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);
const html = await fetchText(embedUrl, {
headers: {
'User-Agent': USER_AGENT,
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) ||
html.match(/src:\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i);
if (m) return decodeEntities(m[1]);
} catch { }
return null;
}
export async function getStream({ id, s, e, audio }) {
export async function getStream({ id, s, e, audio, tmdbApiKey }) {
try {
const info = await getTmdbInfo(id, s ? 'tv' : 'movie', s);
const info = await getTmdbInfo(tmdbApiKey, 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;
const potentialSlug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
if (!potentialSlug) continue;
try {
const checkHtml = await fetchText(`${BASE_URL}/watch/${potentialSlug}/ep-${e || 1}`, {
headers: { 'User-Agent': USER_AGENT, Referer: `${BASE_URL}/watch/${potentialSlug}` },
signal: AbortSignal.timeout(4000)
});
if (checkHtml && checkHtml.includes('nv-watch-page')) {
seriesSlug = potentialSlug;
break;
}
} catch {}
}
if (!seriesSlug) {
for (const title of info.titles) {
const results = await search(title);
const targetClean = cleanTitle(title);
const exactSlugMatch = results.find(r => {
const slugClean = cleanTitle(r.slug);
return slugClean === targetClean || r.slug === targetClean;
});
if (exactSlugMatch) {
seriesSlug = exactSlugMatch.slug;
break;
}
const sortedResults = [...results].sort((a, b) => a.slug.length - b.slug.length);
for (const r of sortedResults) {
const resultClean = cleanTitle(r.text);
const isExactMatch = resultClean === targetClean;
const isCloseMatch = r.text.toLowerCase().includes(title.toLowerCase());
if (isExactMatch || isCloseMatch) {
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;
const sortedResults = [...fSearch].sort((a, b) => a.slug.length - b.slug.length);
if (sortedResults.length) seriesSlug = sortedResults[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 watchHtml = await fetchText(`${BASE_URL}/watch/${seriesSlug}/ep-${e || 1}`, {
headers: {
'User-Agent': USER_AGENT,
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 iframeMatch = watchHtml.match(/<iframe[^>]*src=["']([^"']+)["'][^>]*>/i);
if (iframeMatch) {
const iframeSrc = decodeEntities(iframeMatch[1]);
const hls = await extractHls(iframeSrc);
if (hls) {
const audioType = audio === 'dub' ? 'dub' : 'sub';
byAudio[audioType].push(iframeSrc);
}
}
if (byAudio.sub.length === 0 && byAudio.dub.length === 0) {
const tabMatches = [...watchHtml.matchAll(/<button[^>]*class=["'][^"']*nv-server-tab[^"']*["'][^>]*data-id=["']([^"']+)["'][^>]*>/gi)];
const tabMap = {};
for (const match of tabMatches) {
const tabId = match[1].toLowerCase();
const tabClassMatch = match[0].match(/class=["'][^"']*(tab_\d+)[^"']/i);
if (tabClassMatch) {
const tabClass = tabClassMatch[1];
tabMap[tabClass] = tabId.includes('dub') ? 'dub' : 'sub';
}
}
const btnRegex = /<button\s+[^>]*class=["'][^"']*server-video[^"']*["'][^>]*>/gi;
const btnMatches = [...watchHtml.matchAll(btnRegex)];
for (const match of btnMatches) {
const btnHtml = match[0];
const videoMatch = btnHtml.match(/data-video=["']([^"']+)["']/i);
const tabMatch = btnHtml.match(/data-tab=["']([^"']+)["']/i);
if (videoMatch && tabMatch) {
const videoUrl = decodeEntities(videoMatch[1]);
const tabClass = tabMatch[1];
const audioType = tabMap[tabClass] || 'sub';
byAudio[audioType].push(videoUrl);
}
}
}
const audiosToTry = audio === "all" ? ["sub", "dub"] : (audio === "dub" ? ["dub", "sub"] : ["sub", "dub"]);
const allUrls = [];

View File

@@ -1,14 +1,26 @@
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; }
const url = s && e
? `https://sparkling-mode-a1ed.fillatame.workers.dev/?tmdb_id=${id}&season=${s}&episode=${e}`
: `https://sparkling-mode-a1ed.fillatame.workers.dev/?tmdb_id=${id}`;
const data = await fetchJson(url, { signal: AbortSignal.timeout(15000) });
if (!data || !Array.isArray(data.streams)) return null;
const allUrls = [];
for (const stream of data.streams) {
const streamUrl = stream.url || stream.link;
if (!streamUrl) continue;
const isHls = stream.format === 'HLS' || stream.isM3U8 === true || streamUrl.includes('m3u8');
allUrls.push({
url: streamUrl,
server: stream.provider || 'FlaxMovies',
quality: stream.resolution || 'Auto',
type: isHls ? 'hls' : 'mp4'
});
}
return allUrls.length ? { allUrls } : null;
} catch {
return null;
}
}

View File

@@ -4,10 +4,10 @@ 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 }) {
export async function getStream({ id, s, tmdbApiKey }) {
if (s != null) return null;
try {
const info = await getTmdbInfo(id, 'movie');
const info = await getTmdbInfo(tmdbApiKey, id, 'movie');
if (!info?.titles?.length) return null;
let watchSlug = null;
for (const title of info.titles) {

View File

@@ -4,10 +4,10 @@ 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 }) {
export async function getStream({ id, s, e, tmdbApiKey }) {
try {
const isTv = s != null && e != null;
const info = await getTmdbInfo(id, isTv ? 'tv' : 'movie');
const info = await getTmdbInfo(tmdbApiKey, 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';

View File

@@ -1,262 +0,0 @@
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;
}

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

@@ -0,0 +1,43 @@
import { getTmdbInfo } from '../utils/helpers.js';
export async function getStream(args) {
const { id, s, e, tmdbApiKey } = args;
const type = (s != null && e != null) ? 'tv' : 'movie';
try {
const tmdbInfo = await getTmdbInfo(tmdbApiKey, id, type, s);
const name = tmdbInfo.titles[0] || '';
if (!name) return null;
if (type === 'tv') {
return null;
} else {
const url = `https://lmscript.xyz/v1/movies?filters[q]=${encodeURIComponent(name)}&expand=streams`;
const response = await fetch(url);
const data = await response.json();
const movie = data.items?.find(item => `${item.tmdb_prefix}` === `${id}` || `${item.tmdb_id}` === `${id}`);
if (!movie) return null;
const streams = movie.streams || {};
const SOURCE_ORDER = ["1080p", "720p", "480p", "360p"];
const sortedStreams = Object.keys(streams).sort((a, b) => SOURCE_ORDER.indexOf(a) - SOURCE_ORDER.indexOf(b));
if (!sortedStreams.length) return null;
const allUrls = sortedStreams.map(quality => {
const streamUrl = streams[quality];
return {
url: streamUrl,
quality,
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
headers: {}
};
});
return { allUrls };
}
} catch (err) {
return null;
}
}

View File

@@ -1,62 +0,0 @@
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;
}
}

View File

@@ -1,28 +1,24 @@
import { getTmdbInfo, fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
const WINGS_BASE = "https://api.wingsdatabase.com";
const SPEEDRACE_BASE = "https://api.speedracelight.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: 'vsrc', name: 'Neon' },
{ id: 'hdmovie', name: 'Vyse' },
{ id: 'meine', name: 'Killjoy' },
{ id: 'lamovie', name: 'Omen' },
{ id: 'superflix', name: 'Raze' }
];
export async function getStream({ id, s, e, server }) {
export async function getStream({ id, s, e, server, tmdbApiKey }) {
try {
const isTv = s != null && e != null;
const info = await getTmdbInfo(id, isTv ? 'tv' : 'movie');
const info = await getTmdbInfo(tmdbApiKey, 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 seedData = await fetchJson(`${SPEEDRACE_BASE}/seed?mediaId=${id}`, { headers: HEADERS, signal: AbortSignal.timeout(5000) });
const seed = seedData?.seed;
if (!seed) return null;
@@ -36,9 +32,7 @@ export async function getStream({ id, s, e, server }) {
}
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}`;
let url = `${SPEEDRACE_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) });

114
src/sources/vidvault.js Normal file
View File

@@ -0,0 +1,114 @@
import { USER_AGENT } from '../utils/helpers.js';
const BASE_URL = "https://vidvault.ru";
const HEADERS = {
"Referer": `${BASE_URL}/`,
"Origin": BASE_URL,
"User-Agent": USER_AGENT,
};
async function getToken() {
const res = await fetch(`${BASE_URL}/api/get-token`, {
method: "GET",
headers: HEADERS
});
const data = await res.json();
return data.t;
}
function getCaptionType(url) {
return url.toLowerCase().includes(".vtt") ? "vtt" : "srt";
}
export async function getStream(args) {
const { id, s, e } = args;
const type = (s != null && e != null) ? "tv" : "movie";
try {
const token = await getToken();
const payload = { type, tmdbId: String(id) };
if (type === "tv") {
payload.season = String(s);
payload.episode = String(e);
}
const res = await fetch(`${BASE_URL}/api/download-proxy`, {
method: "POST",
headers: {
...HEADERS,
"Content-Type": "application/json",
"x-request-token": token
},
body: JSON.stringify(payload)
});
const data = await res.json();
const allUrls = [];
const subtitles = [];
const mp4 = data?.mp4Data;
if (mp4) {
const country = mp4.country || "";
const lanName = mp4.lanName || "";
const detailPath = mp4.detailPath || "";
const downloads = mp4.downloadInfo?.data?.downloads || [];
for (const d of downloads) {
if (!d?.url) continue;
allUrls.push({
quality: `${d.resolution}p ${detailPath} ${country} ${lanName}`.trim(),
url: d.url,
type: "mp4",
headers: HEADERS
});
}
const srtCaptions = mp4.downloadInfo?.data?.captions || [];
for (const c of srtCaptions) {
if (!c?.url) continue;
subtitles.push({
label: c.lanName || c.lan || "Unknown",
type: getCaptionType(c.url),
language: c.lan || "",
url: c.url
});
}
}
const mkvFiles = data?.mkvData?.files || [];
for (const f of mkvFiles) {
if (!f?.url) continue;
allUrls.push({
quality: `MKV ${f.size || ""}`.trim(),
url: f.url,
type: "mp4",
headers: HEADERS
});
}
const mkvV2 = data?.mkvV2Data;
if (mkvV2?.url) {
allUrls.push({
quality: `${mkvV2.quality || ""} ${mkvV2.language || ""} ${mkvV2.country || ""} (${mkvV2.size || ""})`.trim(),
url: mkvV2.url,
type: "mp4",
headers: HEADERS
});
}
const mkvV3 = data?.mkvV3Data;
if (mkvV3?.url) {
allUrls.push({
quality: `${mkvV3.quality || ""} ${mkvV3.language || ""} ${mkvV3.country || ""} (${mkvV3.size || ""})`.trim(),
url: mkvV3.url,
type: "mp4",
headers: HEADERS
});
}
return allUrls.length ? { allUrls, subtitles } : null;
} catch (err) {
return null;
}
}

View File

@@ -1,70 +1,47 @@
import { webcrypto } from 'node:crypto';
// i can't fix this, i'll wait for someone else to fix this lol
import crypto 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';
const PLAYER_URL = 'https://player.vidzee.wtf';
const KEY = 'c4a8f1d7e2b9a6c3d0f5e8a1b7c4d9e2';
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']);
function decryptAes(encryptedBase64) {
const keyBuf = Buffer.from(KEY, 'utf8');
const ciphertext = Buffer.from(encryptedBase64, 'base64');
try {
const decrypted = await webcrypto.subtle.decrypt({
name: 'AES-GCM',
iv: t.subarray(0, 12),
tagLength: 128
}, key, s);
const decipher = crypto.createDecipheriv('aes-256-ecb', keyBuf, null);
decipher.setAutoPadding(true);
let dec = decipher.update(ciphertext);
dec = Buffer.concat([dec, decipher.final()]);
const text = dec.toString('utf8');
if (text.startsWith('http') || text.includes('{')) return text;
} catch { }
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 iv = Buffer.alloc(16, 0);
const decipher = crypto.createDecipheriv('aes-256-cbc', keyBuf, iv);
decipher.setAutoPadding(true);
let dec = decipher.update(ciphertext);
dec = Buffer.concat([dec, decipher.final()]);
const text = dec.toString('utf8');
if (text.startsWith('http') || text.includes('{')) return text;
} catch { }
const ivBase64 = dec.slice(0, sep);
const ciphertext = dec.slice(sep + 1);
const keyPadded = key.padEnd(32, '\0');
try {
const iv = ciphertext.subarray(0, 16);
const data = ciphertext.subarray(16);
const decipher = crypto.createDecipheriv('aes-256-cbc', keyBuf, iv);
decipher.setAutoPadding(true);
let dec = decipher.update(data);
dec = Buffer.concat([dec, decipher.final()]);
const text = dec.toString('utf8');
if (text.startsWith('http') || text.includes('{')) return text;
} catch { }
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 '';
}
return null;
}
export async function getStream({ id, s, e, clientIP }) {
@@ -77,44 +54,53 @@ export async function getStream({ id, s, e, clientIP }) {
};
try {
const decKey = await getDecKey(headers);
const isTv = s != null && e != null;
const servers = ['tik', 'ipcloud', 'v6:Hindi'];
const results = [];
const serversToTry = [3, 0, 1, 2, 4, 5, 6, 8];
for (const sr of serversToTry) {
for (const sr of servers) {
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) });
const url = isTv
? `${CORE_URL}/streams/tv/${id}/${s}/${e}?s=${encodeURIComponent(sr)}&e=${e}`
: `${CORE_URL}/streams/movie/${id}?s=${encodeURIComponent(sr)}&e=1`;
if (data?.url?.length) {
for (const entry of data.url) {
if (!entry.link) continue;
const data = await fetchJson(url, { headers, signal: AbortSignal.timeout(6000) });
const decrypted = decrypt(entry.link, decKey);
if (decrypted?.startsWith('http')) {
if (data && data.c) {
const decryptedText = decryptAes(data.c);
if (decryptedText) {
let streamUrl = decryptedText;
let extraHeaders = {};
return {
allUrls: [
{
url: decrypted,
skipHlsCheck: true,
headers: {
'User-Agent': USER_AGENT,
'Referer': PLAYER_URL,
'Origin': PLAYER_URL
}
}
]
};
if (decryptedText.startsWith('{')) {
try {
const parsed = JSON.parse(decryptedText);
streamUrl = parsed.url || parsed.link || parsed.file;
if (parsed.headers) extraHeaders = parsed.headers;
} catch { }
}
if (streamUrl && streamUrl.startsWith('http')) {
results.push({
url: streamUrl,
server: `VidZee - ${sr}`,
skipHlsCheck: true,
headers: {
'User-Agent': USER_AGENT,
'Referer': PLAYER_URL,
'Origin': PLAYER_URL,
...extraHeaders
}
});
}
}
}
} catch (err) {
continue;
}
}
return null;
} catch (err) {
return results.length ? { allUrls: results } : null;
} catch {
return null;
}
}

View File

@@ -13,7 +13,11 @@ const HEADERS = {
function extract(regex, text) {
const m = text?.match(regex);
if (!m) return null;
return m[1].replace(/\\/g, '');
let val = m[1];
val = val.replace(/\\u0026/gi, '&');
val = val.replace(/\\\//g, '/');
val = val.replace(/\\/g, '');
return val;
}
export async function getStream({ id, s, e, absoluteBase }) {
@@ -22,36 +26,40 @@ export async function getStream({ id, s, e, absoluteBase }) {
? `${BASE_URL}/api/tv/${id}/${s}/${e}`
: `${BASE_URL}/api/movie/${id}`;
const apiProxy = absoluteBase
const apiProxy = absoluteBase && 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 apiText = await fetchText(apiProxy, { headers: HEADERS }).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
const embedProxy = absoluteBase && 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);
const html = await fetchText(embedProxy, { headers: HEADERS }).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, '');
if (playlist.startsWith('/')) {
playlist = `${BASE_URL}${playlist}`;
}
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))}`
const masterProxy = absoluteBase && absoluteBase !== '/'
? `${absoluteBase.replace(/^https:\/\/(localhost|127\.0\.0\.1)/, 'http://$1')}/api?url=${encodeURIComponent(rawMasterUrl)}&proxyHeaders=${encodeURIComponent(JSON.stringify({ ...HEADERS, Referer: embedUrl }))}`
: rawMasterUrl;
const playlistText = await fetchText(rawMasterUrl, { headers: HEADERS }).catch(() => null);
const playlistText = await fetchText(masterProxy, { headers: { ...HEADERS, 'Referer': embedUrl } }).catch(() => null);
if (!playlistText?.includes('#EXTM3U')) return null;
let finalUrl = null;
@@ -74,17 +82,23 @@ export async function getStream({ id, s, e, absoluteBase }) {
if (!finalUrl) finalUrl = rawMasterUrl;
if (!finalUrl.startsWith('http')) {
const base = rawMasterUrl.split('/playlist/')[0];
finalUrl = finalUrl.startsWith('/') ? `${base}${finalUrl}` : `${base}/${finalUrl}`;
const baseMatch = rawMasterUrl.match(/^(https?:\/\/[^\/]+)/);
const base = baseMatch ? baseMatch[1] : BASE_URL;
if (finalUrl.startsWith('/')) {
finalUrl = `${base}${finalUrl}`;
} else {
const pathParts = rawMasterUrl.split('/');
pathParts.pop();
finalUrl = `${pathParts.join('/')}/${finalUrl}`;
}
}
const alreadyProxied = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?\/api\?url=/.test(finalUrl);
return {
allUrls: [
{
url: finalUrl,
skipProxy: alreadyProxied,
server: 'VixSrc',
skipProxy: true,
headers: {
...HEADERS,
'Referer': `${BASE_URL}/`

View File

@@ -67,8 +67,7 @@ function cacheGet(cache, key) {
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}`;
export async function createStreamArgs(source, tmdbApiKey, id, s, e, clientIP = null) {
const audio = /dub$/.test(source.key) ? 'dub' : 'sub';
return {
id: id,
@@ -83,13 +82,25 @@ export async function createStreamArgs(source, tmdbApiKey, id, s, e, clientIP=nu
}
export async function validateTmdbId(tmdbApiKey, tmdbId, mediaType = 'movie') {
const key = `${tmdbId}-${mediaType}`;
let apiKey = tmdbApiKey;
let id = tmdbId;
let type = mediaType;
if (id === 'movie' || id === 'tv' || !id) {
if (apiKey && apiKey !== 'movie' && apiKey !== 'tv') {
type = id || 'movie';
id = apiKey;
apiKey = process.env.TMDB_API_KEY;
}
}
if (!apiKey) {
apiKey = process.env.TMDB_API_KEY;
}
const key = `${id}-${type}`;
const cached = cacheGet(tmdbValidationCache, key);
if (cached !== undefined) return cached;
const k = tmdbApiKey;
if (!k) return { valid: false, error: 'TMDB API key not configured' };
if (!apiKey) 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) });
const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`, { signal: AbortSignal.timeout(5000) });
if (res.ok) {
const data = await res.json();
const isValid = !!(data?.id && (data?.title || data?.name));
@@ -108,15 +119,30 @@ export async function validateTmdbId(tmdbApiKey, tmdbId, mediaType = 'movie') {
}
export async function getTmdbInfo(tmdbApiKey, tmdbId, mediaType, season) {
const key = `${tmdbId}-${mediaType}-${season || ''}`;
let apiKey = tmdbApiKey;
let id = tmdbId;
let type = mediaType;
let s = season;
if (apiKey === 'movie' || apiKey === 'tv') {
type = apiKey;
id = tmdbId;
apiKey = process.env.TMDB_API_KEY;
} else if (id === 'movie' || id === 'tv') {
type = id;
id = apiKey;
apiKey = process.env.TMDB_API_KEY;
}
if (!apiKey) {
apiKey = process.env.TMDB_API_KEY;
}
const key = `${id}-${type}-${s || ''}`;
const cached = cacheGet(tmdbInfoCache, key);
if (cached !== undefined) return cached;
const k = tmdbApiKey;
if (!k) return { isAnime: false, titles: [], year: null, imdbId: null };
if (!apiKey) 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),
fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&append_to_response=external_ids`, { signal: AbortSignal.timeout(5000) }),
s && type === 'tv' ? fetch(`https://api.themoviedb.org/3/tv/${id}/season/${s}?api_key=${apiKey}`, { signal: AbortSignal.timeout(5000) }) : Promise.resolve(null),
]);
let mainData = null, seasonData = null;
if (mainRes.ok) mainData = await mainRes.json(); else mainRes.body?.cancel();
@@ -125,10 +151,10 @@ export async function getTmdbInfo(tmdbApiKey, tmdbId, mediaType, season) {
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 titleText = mainData?.title || mainData?.name || '';
const ot = mainData?.original_title || mainData?.original_name || '';
if (t) titles.push(t);
if (ot && ot !== t) titles.push(ot);
if (titleText) titles.push(titleText);
if (ot && ot !== titleText) 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);
@@ -178,16 +204,26 @@ export async function tmdbToAnilist(tmdbId, mediaType, season, titles = [], year
}
export async function imdbToTmdb(tmdbApiKey, imdbId, mediaType = 'movie') {
const key = `imdb-${imdbId}`;
let apiKey = tmdbApiKey;
let id = imdbId;
let type = mediaType;
if (id === 'movie' || id === 'tv') {
type = id;
id = apiKey;
apiKey = process.env.TMDB_API_KEY;
}
if (!apiKey) {
apiKey = process.env.TMDB_API_KEY;
}
const key = `imdb-${id}`;
const cached = cacheGet(tmdbInfoCache, key);
if (cached !== undefined) return cached;
const k = tmdbApiKey;
if (!k) return null;
if (!apiKey) 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) });
const data = await fetchJson(`https://api.themoviedb.org/3/find/${id}?api_key=${apiKey}&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 (type === 'movie' && data.movie_results?.length) result = data.movie_results[0].id;
else if (type === 'tv' && data.tv_results?.length) result = data.tv_results[0].id;
if (result) cacheSet(tmdbInfoCache, key, result, 600000);
return result;
} catch { return null; }

View File

@@ -1,54 +1,185 @@
// usage: npm test
// comment out the tests you don't want to run
// Configure the variables below to customize your testing environment
import fs from "fs";
import path from "path";
import VylaSDK from "../src/sdk.js";
import dotenv from "dotenv";
dotenv.config();
const CONFIG = {
DEBUG: true, // Set to true to print outgoing requests, headers, formats, and responses
MOVIE_ID: "550", // Movie TMDB ID to test
TV_ID: "37854", // TV TMDB ID to test, for anime I recommend using a random ID like 37854
TV_SEASON: "1", // TV Season to test
TV_EPISODE: "1", // TV Episode to test
STREAM_SOURCE: "", // Source key to test
TEST_SUBTITLE: false, // Toggle subtitle testing
TEST_DOWNLOAD: false, // Toggle download testing
TEST_MOVIE: false, // Toggle movie testing
TEST_TV: true // Toggle TV testing
};
// debug
if (CONFIG.DEBUG) {
const originalFetch = globalThis.fetch;
const debugFile = path.resolve("test/debug-output.json");
fs.mkdirSync(path.dirname(debugFile), { recursive: true });
const logs = [];
const save = () => {
fs.writeFileSync(debugFile, JSON.stringify(logs, null, 2));
};
globalThis.fetch = async (input, init) => {
const url = typeof input === "string" ? input : input?.url || String(input);
const request = {
timestamp: new Date().toISOString(),
request: {
url,
method: init?.method || "GET",
headers: init?.headers || {},
body: init?.body || null
}
};
console.log("\nRequest:");
console.log(`URL: ${url}`);
console.log(`Method: ${request.request.method}`);
if (init?.headers) console.log("Headers:", JSON.stringify(init.headers, null, 2));
if (init?.body) console.log("Body:", init.body);
try {
const response = await originalFetch(input, init);
request.response = {
status: response.status,
statusText: response.statusText
};
console.log("\nResponse:");
console.log(`URL: ${url}`);
console.log(`Status: ${response.status} ${response.statusText}`);
try {
const clone = response.clone();
const text = await clone.text();
try {
request.response.format = "json";
request.response.body = JSON.parse(text);
console.log("Format: JSON");
console.log(JSON.stringify(request.response.body, null, 2));
} catch {
request.response.format = "text";
request.response.body = text;
console.log("Format: Text/HTML");
console.log(text.substring(0, 800) + (text.length > 800 ? "..." : ""));
}
} catch (err) {
request.response.body = `<Failed to read response body: ${err.message}>`;
}
logs.push(request);
save();
return response;
} catch (err) {
request.error = err.message;
logs.push(request);
save();
console.log(`URL: ${url}`);
console.log(`Error: ${err.message}`);
throw err;
}
};
}
const sdk = new VylaSDK({
tmdbApiKey: process.env.TMDB_API_KEY
});
try {
const movieSubs = await sdk.getSubtitles("155");
console.log("Movie Subtitles Count:", movieSubs.length);
console.log("First Subtitle Sample:", movieSubs[0] || "None");
} catch (err) {
console.error("Movie Subtitles Error:", err.message);
console.log("Starting...");
if (CONFIG.TEST_SUBTITLE && CONFIG.TEST_TV) {
try {
console.log(`\nTV Subtitles for ID: ${CONFIG.TV_ID} (S${CONFIG.TV_SEASON}E${CONFIG.TV_EPISODE})`);
const tvSubs = await sdk.getSubtitles(CONFIG.TV_ID, CONFIG.TV_SEASON, CONFIG.TV_EPISODE);
console.log("TV Subtitles Count:", tvSubs.length);
console.log("First TV Subtitle Sample:", tvSubs[0] || "None");
} catch (err) {
console.error("TV Subtitles Error:", err.message);
}
} else {
console.log("\nSkipped Subtitles Test");
}
try {
const movieDownloads = await sdk.getDownloads("155");
console.log("Movie Downloads:", movieDownloads);
} catch (err) {
console.error("Movie Downloads Error:", err.message);
if (CONFIG.TEST_SUBTITLE && CONFIG.TEST_MOVIE) {
try {
console.log(`\nMovie Subtitles for ID: ${CONFIG.MOVIE_ID}`);
const movieSubs = await sdk.getSubtitles(CONFIG.MOVIE_ID);
console.log("Movie Subtitles Count:", movieSubs.length);
console.log("First Subtitle Sample:", movieSubs[0] || "None");
} catch (err) {
console.error("Movie Subtitles Error:", err.message);
}
} else {
console.log("\nSkipped Subtitles Test");
}
try {
const tvDownloads = await sdk.getDownloads("1399", "1", "1");
console.log("TV Downloads:", tvDownloads);
} catch (err) {
console.error("TV Downloads Error:", err.message);
if (CONFIG.TEST_DOWNLOAD && CONFIG.TEST_MOVIE) {
try {
console.log(`\nMovie Downloads for ID: ${CONFIG.MOVIE_ID}`);
const movieDownloads = await sdk.getDownloads(CONFIG.MOVIE_ID);
console.log("Movie Downloads:", movieDownloads);
} catch (err) {
console.error("Movie Downloads Error:", err.message);
}
} else {
console.log("\nSkipped Downloads Test");
}
try {
const movieResult = await sdk.getStream("cinejoy", "155");
console.log("Movie Stream:", movieResult);
} catch (err) {
console.error("Movie Stream Error:", err.message);
if (CONFIG.TEST_DOWNLOAD && CONFIG.TEST_TV) {
try {
console.log(`\nTV Downloads for ID: ${CONFIG.TV_ID} (S${CONFIG.TV_SEASON}E${CONFIG.TV_EPISODE})`);
const tvDownloads = await sdk.getDownloads(CONFIG.TV_ID, CONFIG.TV_SEASON, CONFIG.TV_EPISODE);
console.log("TV Downloads:", tvDownloads);
} catch (err) {
console.error("TV Downloads Error:", err.message);
}
} else {
console.log("\nSkipped Downloads Test");
}
try {
const tvResult = await sdk.getStream("cinejoy", "1399", "1", "1");
console.log("TV Stream:", tvResult);
} catch (err) {
console.error("TV Stream Error:", err.message);
if (CONFIG.TEST_MOVIE) {
try {
console.log(`\nMovie Stream from "${CONFIG.STREAM_SOURCE}" for ID: ${CONFIG.MOVIE_ID}`);
const movieResult = await sdk.getStream(CONFIG.STREAM_SOURCE, CONFIG.MOVIE_ID);
console.log("Movie Stream:", movieResult);
} catch (err) {
console.error("Movie Stream Error:", err.message);
}
} else {
console.log("\nSkipped Stream Test");
}
try {
const tvSubs = await sdk.getSubtitles("1399", "1", "1");
console.log("TV Subtitles Count:", tvSubs.length);
console.log("First TV Subtitle Sample:", tvSubs[0] || "None");
} catch (err) {
console.error("TV Subtitles Error:", err.message);
if (CONFIG.TEST_TV) {
try {
console.log(`\nTV Stream from "${CONFIG.STREAM_SOURCE}" for ID: ${CONFIG.TV_ID} (S${CONFIG.TV_SEASON}E${CONFIG.TV_EPISODE})`);
const tvResult = await sdk.getStream(CONFIG.STREAM_SOURCE, CONFIG.TV_ID, CONFIG.TV_SEASON, CONFIG.TV_EPISODE);
console.log("TV Stream:", tvResult);
} catch (err) {
console.error("TV Stream Error:", err.message);
}
} else {
console.log("\nSkipped Stream Test");
}