mirror of
https://gitlab.com/vyla-entertainment/sdk.git
synced 2026-11-09 19:57:23 -05:00
Fixed all sources, rewrote, made test easier for developers to test everything
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
node_modules/
|
||||
.env
|
||||
test/debug-output.json
|
||||
@@ -2,3 +2,4 @@
|
||||
.env
|
||||
test/
|
||||
node_modules/
|
||||
test/debug-output.json
|
||||
@@ -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
1
src/sdk.d.ts
vendored
@@ -59,7 +59,6 @@ export interface VylaSDKOptions {
|
||||
tmdbApiKey?: string;
|
||||
}
|
||||
|
||||
// Descriptions written with Claude.
|
||||
export default class VylaSDK {
|
||||
tmdbApiKey?: string;
|
||||
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
43
src/sources/lmscript.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
114
src/sources/vidvault.js
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}/`
|
||||
|
||||
@@ -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; }
|
||||
|
||||
197
test/index.js
197
test/index.js
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user