mirror of
https://gitlab.com/vyla-entertainment/sdk.git
synced 2026-11-13 21:57:27 -05:00
organized/fixed streams
This commit is contained in:
@@ -2,7 +2,8 @@
|
||||
"name": "@vyla-entertainment/sdk",
|
||||
"version": "1.20.0",
|
||||
"scripts": {
|
||||
"test": "node test/index.js"
|
||||
"test": "node test/index.js",
|
||||
"test:all": "node test/index.js all"
|
||||
},
|
||||
"author": "vyla-entertainment",
|
||||
"contributors": [
|
||||
|
||||
@@ -94,24 +94,6 @@ export const SOURCES = [
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'cinezo',
|
||||
label: 'Cinezo',
|
||||
sourceFile: 'cinezo',
|
||||
proxyParam: 'czo',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /yenime\.net|flikhub\.net|hakunaymatata\.com/i,
|
||||
headers: {
|
||||
Referer: 'https://player.cinezo.live/',
|
||||
Origin: 'https://player.cinezo.live',
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'cno',
|
||||
label: 'Cno',
|
||||
@@ -355,7 +337,7 @@ export const SOURCES = [
|
||||
retries: 1,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
disabled: false, // Enabled, but only returns streams when a manifest is provided
|
||||
disabled: false,
|
||||
},
|
||||
|
||||
{
|
||||
@@ -499,26 +481,6 @@ export const SOURCES = [
|
||||
multiUrl: true,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'vidrock',
|
||||
label: 'VidRock',
|
||||
sourceFile: 'vidrock',
|
||||
proxyParam: 'vr',
|
||||
timeout: 20000,
|
||||
jitter: 800,
|
||||
retries: 3,
|
||||
multiUrl: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /./,
|
||||
headers: {
|
||||
Accept: '/',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
Referer: 'https://vidrock.ru/',
|
||||
Origin: 'https://vidrock.ru',
|
||||
},
|
||||
},],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'vidup',
|
||||
label: 'VidUp',
|
||||
|
||||
33
src/sdk.d.ts
vendored
33
src/sdk.d.ts
vendored
@@ -20,10 +20,12 @@ export interface SourceConfig {
|
||||
disabled?: boolean;
|
||||
sourcesTimeout?: number;
|
||||
}
|
||||
|
||||
export interface ProbeResult {
|
||||
ok: boolean;
|
||||
ms: number | null;
|
||||
}
|
||||
|
||||
export interface StreamResult {
|
||||
url: string;
|
||||
headers: {
|
||||
@@ -46,6 +48,7 @@ export interface DownloadResult {
|
||||
type: "mkv";
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export interface SubtitleResult {
|
||||
label: string;
|
||||
file: string;
|
||||
@@ -64,30 +67,12 @@ export default class VylaSDK {
|
||||
|
||||
constructor(options?: VylaSDKOptions);
|
||||
|
||||
/**
|
||||
* Get the list of configured sources.
|
||||
* @param excludeDisabled If true, filters out disabled sources.
|
||||
*/
|
||||
getSources(excludeDisabled?: boolean): SourceConfig[];
|
||||
|
||||
/**
|
||||
* Probe all active (non-disabled) sources for availability.
|
||||
*/
|
||||
probeAllSources(): Promise<ProbeAllResult>;
|
||||
|
||||
/**
|
||||
* Probe a single source by key.
|
||||
*/
|
||||
probeSource(key: string): Promise<ProbeResult>;
|
||||
|
||||
/**
|
||||
* Get a playable stream for a given source/media id.
|
||||
* @param key Source key
|
||||
* @param id Media id (e.g. TMDB id)
|
||||
* @param s Season number (for TV), or null for movies
|
||||
* @param e Episode number (for TV), or null for movies
|
||||
* @param clientIP Optional client IP for source-specific logic
|
||||
*/
|
||||
getStream(
|
||||
key: string,
|
||||
id: string | number,
|
||||
@@ -96,24 +81,12 @@ export default class VylaSDK {
|
||||
clientIP?: string | null
|
||||
): Promise<StreamResult | { allUrls: StreamResult[] }>;
|
||||
|
||||
/**
|
||||
* Get subtitles for a movie or TV episode.
|
||||
* @param id Media id
|
||||
* @param s Season number (for TV), or null for movies
|
||||
* @param e Episode number (for TV), or null for movies
|
||||
*/
|
||||
getSubtitles(
|
||||
id: string | number,
|
||||
s?: number | null,
|
||||
e?: number | null
|
||||
): Promise<SubtitleResult[]>;
|
||||
|
||||
/**
|
||||
* Get download links for a movie or TV episode.
|
||||
* @param id Media id
|
||||
* @param s Season number (for TV), or null for movies
|
||||
* @param e Episode number (for TV), or null for movies
|
||||
*/
|
||||
getDownloads(
|
||||
id: string | number,
|
||||
s?: number | null,
|
||||
|
||||
160
src/sdk.js
160
src/sdk.js
@@ -1,78 +1,82 @@
|
||||
import SOURCES, { SOURCE_MAP } from "./config.js";
|
||||
import { fetchDownloads } from "./downloads/downloads.js";
|
||||
import { probeSource } from "./health.js";
|
||||
import { getChapterPages, getMangaDetails, searchManga } from "./manga/atsu.js";
|
||||
import { fetchSubtitles, getSubPathsMovie, getSubPathsTv } from "./subtitles.js";
|
||||
import { createStreamArgs } from "./utils/helpers.js";
|
||||
|
||||
const ALL_SOURCE_MODULES = Object.fromEntries(
|
||||
await Promise.all(SOURCES.map(async cfg => [cfg.key, await import(`./sources/${cfg.sourceFile}.js`)]))
|
||||
);
|
||||
|
||||
const SOURCE_MODULES = Object.fromEntries(
|
||||
Object.entries(ALL_SOURCE_MODULES).filter(([key]) => !SOURCE_MAP[key]?.disabled)
|
||||
);
|
||||
|
||||
export default class VylaSDK {
|
||||
tmdbApiKey = null;
|
||||
penguManifest = null;
|
||||
constructor({ tmdbApiKey, penguManifest } = {}) {
|
||||
this.tmdbApiKey = tmdbApiKey || null;
|
||||
this.penguManifest = penguManifest || null;
|
||||
}
|
||||
getSources(excludeDisabled = false) {
|
||||
if (excludeDisabled) {
|
||||
return SOURCES.filter(cfg => !cfg.disabled);
|
||||
}
|
||||
return SOURCES;
|
||||
}
|
||||
async probeAllSources() {
|
||||
const active = SOURCES.filter(cfg => !cfg.disabled);
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
active.map(async cfg => await probeSource(cfg, SOURCE_MODULES[cfg.key]))
|
||||
);
|
||||
|
||||
const sources = Object.fromEntries(
|
||||
active.map((cfg, i) => [
|
||||
cfg.key,
|
||||
results[i].status === 'fulfilled' ? results[i].value : { ok: false, ms: null },
|
||||
])
|
||||
);
|
||||
|
||||
return sources;
|
||||
}
|
||||
async probeSource(key) {
|
||||
const cfg = SOURCES.find(cfg => cfg.key === key);
|
||||
if (!cfg) throw new Error(`Source with key "${key}" not found`);
|
||||
const mod = SOURCE_MODULES[key];
|
||||
if (!mod) throw new Error(`Source module for key "${key}" not found`);
|
||||
return await probeSource(this, cfg, mod);
|
||||
}
|
||||
async getStream(key, id, s = null, e = null, clientIP = null) {
|
||||
const cfg = SOURCES.find(cfg => cfg.key === key);
|
||||
if (!cfg) throw new Error(`Source with key "${key}" not found`);
|
||||
const mod = SOURCE_MODULES[key];
|
||||
if (!mod) throw new Error(`Source module for key "${key}" not found`);
|
||||
const streamArgs = await createStreamArgs(cfg, this, id, s, e, clientIP);
|
||||
return await mod.getStream(streamArgs);
|
||||
}
|
||||
async getSubtitles(id, s = null, e = null) {
|
||||
const paths = s != null && e != null
|
||||
? await getSubPathsTv(id, s, e)
|
||||
: await getSubPathsMovie(id);
|
||||
return await fetchSubtitles(paths);
|
||||
}
|
||||
async getDownloads(id, s = null, e = null) {
|
||||
return await fetchDownloads(this, id, s, e);
|
||||
}
|
||||
async searchManga(query, page = 1, perPage = 12) {
|
||||
return await searchManga(query, page, perPage);
|
||||
}
|
||||
async getManga(id) {
|
||||
return await getMangaDetails(id);
|
||||
}
|
||||
async getMangaChapter(mangaId, chapterId) {
|
||||
return await getChapterPages(mangaId, chapterId);
|
||||
}
|
||||
}
|
||||
import SOURCES, { SOURCE_MAP } from "./config.js";
|
||||
import { fetchDownloads } from "./downloads/downloads.js";
|
||||
import { probeSource } from "./health.js";
|
||||
import { getChapterPages, getMangaDetails, searchManga } from "./manga/atsu.js";
|
||||
import { fetchSubtitles, getSubPathsMovie, getSubPathsTv } from "./subtitles.js";
|
||||
import { createStreamArgs } from "./utils/helpers.js";
|
||||
|
||||
const ALL_SOURCE_MODULES = Object.fromEntries(
|
||||
await Promise.all(SOURCES.map(async cfg => [cfg.key, await import(`./sources/${cfg.sourceFile}.js`)]))
|
||||
);
|
||||
|
||||
const SOURCE_MODULES = Object.fromEntries(
|
||||
Object.entries(ALL_SOURCE_MODULES).filter(([key]) => !SOURCE_MAP[key]?.disabled)
|
||||
);
|
||||
|
||||
export default class VylaSDK {
|
||||
tmdbApiKey = null;
|
||||
penguManifest = null;
|
||||
constructor({ tmdbApiKey, penguManifest } = {}) {
|
||||
this.tmdbApiKey = tmdbApiKey || null;
|
||||
this.penguManifest = penguManifest || null;
|
||||
}
|
||||
getSources(excludeDisabled = false) {
|
||||
if (excludeDisabled) {
|
||||
return SOURCES.filter(cfg => !cfg.disabled);
|
||||
}
|
||||
return SOURCES;
|
||||
}
|
||||
async probeAllSources() {
|
||||
const active = SOURCES.filter(cfg => !cfg.disabled);
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
active.map(async cfg => await probeSource(this, cfg, SOURCE_MODULES[cfg.key]))
|
||||
);
|
||||
|
||||
const sources = Object.fromEntries(
|
||||
active.map((cfg, i) => [
|
||||
cfg.key,
|
||||
results[i].status === 'fulfilled' ? results[i].value : { ok: false, ms: null },
|
||||
])
|
||||
);
|
||||
|
||||
return sources;
|
||||
}
|
||||
async probeSource(key) {
|
||||
const cfg = SOURCES.find(cfg => cfg.key === key);
|
||||
if (!cfg) throw new Error(`Source with key "${key}" not found`);
|
||||
const mod = SOURCE_MODULES[key];
|
||||
if (!mod) throw new Error(`Source module for key "${key}" not found`);
|
||||
return await probeSource(this, cfg, mod);
|
||||
}
|
||||
async getStream(key, id, s = null, e = null, clientIP = null) {
|
||||
const cfg = SOURCES.find(cfg => cfg.key === key);
|
||||
if (!cfg) throw new Error(`Source with key "${key}" not found`);
|
||||
const mod = SOURCE_MODULES[key];
|
||||
if (!mod) throw new Error(`Source module for key "${key}" not found`);
|
||||
const streamArgs = await createStreamArgs(cfg, this, id, s, e, clientIP);
|
||||
const result = await mod.getStream(streamArgs);
|
||||
if (result && typeof result === 'object') {
|
||||
delete result.subtitles;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
async getSubtitles(id, s = null, e = null) {
|
||||
const paths = s != null && e != null
|
||||
? await getSubPathsTv(id, s, e)
|
||||
: await getSubPathsMovie(id);
|
||||
return await fetchSubtitles(paths);
|
||||
}
|
||||
async getDownloads(id, s = null, e = null) {
|
||||
return await fetchDownloads(this, id, s, e);
|
||||
}
|
||||
async searchManga(query, page = 1, perPage = 12) {
|
||||
return await searchManga(query, page, perPage);
|
||||
}
|
||||
async getManga(id) {
|
||||
return await getMangaDetails(id);
|
||||
}
|
||||
async getMangaChapter(mangaId, chapterId) {
|
||||
return await getChapterPages(mangaId, chapterId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { getUA } from '../utils/helpers.js';
|
||||
import { getUA, resolveImdbId, resolveTmdbKey } from '../utils/helpers.js';
|
||||
|
||||
const SERVICE_ORIGIN = 'https://st.111477.xyz';
|
||||
|
||||
function generateManifestBaseUrl(tmdbKey) {
|
||||
function generateManifestBaseUrl(apiKey) {
|
||||
let config = 'https://a.111477.xyz/::sort=file-desc::limit=3';
|
||||
if (tmdbKey) {
|
||||
config += `::tmdb=${tmdbKey}`;
|
||||
if (apiKey) {
|
||||
config += `::tmdb=${apiKey}`;
|
||||
}
|
||||
const b64 = Buffer.from(config, 'utf8')
|
||||
.toString('base64')
|
||||
@@ -15,28 +15,13 @@ function generateManifestBaseUrl(tmdbKey) {
|
||||
return `${SERVICE_ORIGIN}/config/${b64}`;
|
||||
}
|
||||
|
||||
async function resolveImdbId(sdk, id, isTv) {
|
||||
const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY;
|
||||
if (!apiKey) return null;
|
||||
const type = isTv ? 'tv' : 'movie';
|
||||
try {
|
||||
const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&append_to_response=external_ids`, {
|
||||
signal: AbortSignal.timeout(6000),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
return data.imdb_id || data.external_ids?.imdb_id || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e, sdk }) {
|
||||
const isTv = Boolean(s && e);
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, sdk } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const apiKey = resolveTmdbKey(args, sdk);
|
||||
const ua = getUA();
|
||||
const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY;
|
||||
|
||||
const imdbId = await resolveImdbId(sdk, id, isTv);
|
||||
const imdbId = await resolveImdbId(id, isTv, apiKey);
|
||||
const targetIds = [];
|
||||
|
||||
if (imdbId && imdbId.startsWith('tt')) {
|
||||
@@ -48,7 +33,7 @@ export async function getStream({ id, s, e, sdk }) {
|
||||
const seenUrls = new Set();
|
||||
const allUrls = [];
|
||||
|
||||
const defaultHeaders = {
|
||||
const headers = {
|
||||
'User-Agent': ua,
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
};
|
||||
@@ -60,7 +45,7 @@ export async function getStream({ id, s, e, sdk }) {
|
||||
|
||||
try {
|
||||
const res = await fetch(endpoint, {
|
||||
headers: defaultHeaders,
|
||||
headers,
|
||||
signal: AbortSignal.timeout(12000),
|
||||
});
|
||||
|
||||
@@ -70,19 +55,22 @@ export async function getStream({ id, s, e, sdk }) {
|
||||
if (Array.isArray(data?.streams) && data.streams.length > 0) {
|
||||
for (const item of data.streams) {
|
||||
if (!item || typeof item !== 'object') continue;
|
||||
const url = String(item.url || '').trim();
|
||||
if (!url || !url.startsWith('http') || seenUrls.has(url)) continue;
|
||||
const streamUrl = String(item.url || '').trim();
|
||||
if (!streamUrl || !streamUrl.startsWith('http') || seenUrls.has(streamUrl)) continue;
|
||||
|
||||
seenUrls.add(url);
|
||||
seenUrls.add(streamUrl);
|
||||
|
||||
const rawTitle = item.title ? String(item.title).trim() : '';
|
||||
const rawName = item.name ? String(item.name).trim() : '111477';
|
||||
const label = rawTitle || rawName;
|
||||
|
||||
allUrls.push({
|
||||
url,
|
||||
url: streamUrl,
|
||||
label,
|
||||
headers: defaultHeaders,
|
||||
server: label,
|
||||
quality: 'Auto',
|
||||
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -95,7 +83,7 @@ export async function getStream({ id, s, e, sdk }) {
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
allUrls,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
@@ -1,57 +1,121 @@
|
||||
import { getTmdbInfo, fetchJson, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
async function resolveSlug(id, s, e, info) {
|
||||
const titles = info?.titles || [];
|
||||
const keywords = [];
|
||||
for (const title of titles) {
|
||||
const t = title.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
if (!keywords.includes(t)) keywords.push(t);
|
||||
const words = title.split(/[\s:]+/).filter(w => w.length > 2);
|
||||
if (words.length >= 2) {
|
||||
const short = words.slice(0, 3).join(' ').toLowerCase();
|
||||
if (!keywords.includes(short)) keywords.push(short);
|
||||
}
|
||||
}
|
||||
const slugCandidates = [];
|
||||
for (const keyword of keywords) {
|
||||
try {
|
||||
const data = await fetchJson(`https://123animehub.cc/ajax/film/search?keyword=${encodeURIComponent(keyword)}&_=${Date.now()}`, { headers: { 'X-Requested-With': 'XMLHttpRequest', 'Referer': 'https://123animehub.cc', 'Accept': 'application/json', 'User-Agent': USER_AGENT }, signal: AbortSignal.timeout(8000) });
|
||||
let html = data.html || '', idx = 0;
|
||||
while ((idx = html.indexOf('href="/anime/', idx)) !== -1) {
|
||||
const start = idx + 13;
|
||||
const end = html.indexOf('"', start);
|
||||
const slug = html.slice(start, end);
|
||||
if (!slugCandidates.includes(slug)) slugCandidates.push(slug);
|
||||
idx = end;
|
||||
}
|
||||
} catch { }
|
||||
if (slugCandidates.length) break;
|
||||
}
|
||||
for (const slug of slugCandidates) {
|
||||
try {
|
||||
const data = await fetchJson(`https://123animehub.cc/ajax/episode/info?epr=${encodeURIComponent(s ? `${slug}/${s}/${e}` : `${slug}/1`)}&ts=1&_=${Date.now()}`, { headers: { 'Referer': `https://123animehub.cc/anime/${slug}`, 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json, text/javascript, */*; q=0.01', 'User-Agent': USER_AGENT }, signal: AbortSignal.timeout(10000) });
|
||||
if (data?.target) return data.target;
|
||||
} catch { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
if (!s) return null;
|
||||
const info = await getTmdbInfo(id, 'tv', s);
|
||||
if (!info.isAnime) return null;
|
||||
const target = await resolveSlug(id, s, e, info);
|
||||
if (!target) return null;
|
||||
const embedMatch = target.match(/\/embed-[^/]+\/([A-Za-z0-9+/=]+)$/);
|
||||
if (!embedMatch) return null;
|
||||
const origin = new URL(target).origin;
|
||||
try {
|
||||
const embedRes = await fetch(target, { headers: { 'User-Agent': USER_AGENT }, signal: AbortSignal.timeout(10000) });
|
||||
if (!embedRes.ok) return null;
|
||||
const setCookie = embedRes.headers.get('set-cookie');
|
||||
const data = await fetchJson(`${origin}/hs/getSources?id=${embedMatch[1]}`, { headers: { 'Referer': target, 'Accept': '*/*', 'User-Agent': USER_AGENT, ...(setCookie ? { 'Cookie': setCookie.split(';')[0].trim() } : {}) }, signal: AbortSignal.timeout(10000) });
|
||||
const src = typeof data.sources === 'string' && data.sources.length ? data.sources : (Array.isArray(data.sources) && data.sources.length ? (data.sources[0].file || data.sources[0].src || data.sources[0].url) : null);
|
||||
if (src) return { url: src, headers: { 'Referer': 'https://play2.echovideo.ru/', 'Origin': 'https://play2.echovideo.ru' } };
|
||||
} catch { }
|
||||
return null;
|
||||
import { fetchJson, getTmdbMetadata, resolveTmdbKey, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = 'https://123animehub.cc';
|
||||
const CDN_ORIGIN = 'https://play2.echovideo.ru';
|
||||
|
||||
async function resolveSlug(id, s, e, titles) {
|
||||
const keywords = [];
|
||||
for (const title of titles) {
|
||||
const t = title.toLowerCase().replace(/[^a-z0-9\s]/g, ' ').replace(/\s+/g, ' ').trim();
|
||||
if (!keywords.includes(t)) keywords.push(t);
|
||||
const words = title.split(/[\s:]+/).filter(w => w.length > 2);
|
||||
if (words.length >= 2) {
|
||||
const short = words.slice(0, 3).join(' ').toLowerCase();
|
||||
if (!keywords.includes(short)) keywords.push(short);
|
||||
}
|
||||
}
|
||||
|
||||
const slugCandidates = [];
|
||||
for (const keyword of keywords) {
|
||||
try {
|
||||
const data = await fetchJson(`${BASE_URL}/ajax/film/search?keyword=${encodeURIComponent(keyword)}&_=${Date.now()}`, {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
Referer: `${BASE_URL}/`,
|
||||
Accept: 'application/json',
|
||||
'User-Agent': USER_AGENT,
|
||||
},
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
let html = data.html || '';
|
||||
let idx = 0;
|
||||
while ((idx = html.indexOf('href="/anime/', idx)) !== -1) {
|
||||
const start = idx + 13;
|
||||
const end = html.indexOf('"', start);
|
||||
const slug = html.slice(start, end);
|
||||
if (!slugCandidates.includes(slug)) slugCandidates.push(slug);
|
||||
idx = end;
|
||||
}
|
||||
} catch { }
|
||||
if (slugCandidates.length) break;
|
||||
}
|
||||
|
||||
for (const slug of slugCandidates) {
|
||||
try {
|
||||
const data = await fetchJson(`${BASE_URL}/ajax/episode/info?epr=${encodeURIComponent(s ? `${slug}/${s}/${e}` : `${slug}/1`)}&ts=1&_=${Date.now()}`, {
|
||||
headers: {
|
||||
Referer: `${BASE_URL}/anime/${slug}`,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
Accept: 'application/json, text/javascript, */*; q=0.01',
|
||||
'User-Agent': USER_AGENT,
|
||||
},
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (data?.target) return data.target;
|
||||
} catch { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, sdk } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
if (!s) return null;
|
||||
|
||||
const apiKey = resolveTmdbKey(args, sdk);
|
||||
const meta = await getTmdbMetadata(id, isTv, apiKey);
|
||||
if (!meta?.isAnime) return null;
|
||||
|
||||
const target = await resolveSlug(id, s, e, meta.titles || []);
|
||||
if (!target) return null;
|
||||
|
||||
const embedMatch = target.match(/\/embed-[^/]+\/([A-Za-z0-9+/=]+)$/);
|
||||
if (!embedMatch) return null;
|
||||
|
||||
const origin = new URL(target).origin;
|
||||
try {
|
||||
const embedRes = await fetch(target, {
|
||||
headers: { 'User-Agent': USER_AGENT },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!embedRes.ok) return null;
|
||||
|
||||
const setCookie = embedRes.headers.get('set-cookie');
|
||||
const data = await fetchJson(`${origin}/hs/getSources?id=${embedMatch[1]}`, {
|
||||
headers: {
|
||||
Referer: target,
|
||||
Accept: '*/*',
|
||||
'User-Agent': USER_AGENT,
|
||||
...(setCookie ? { Cookie: setCookie.split(';')[0].trim() } : {}),
|
||||
},
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
|
||||
const streamUrl = typeof data.sources === 'string' && data.sources.length
|
||||
? data.sources
|
||||
: (Array.isArray(data.sources) && data.sources.length ? (data.sources[0].file || data.sources[0].src || data.sources[0].url) : null);
|
||||
|
||||
if (!streamUrl) return null;
|
||||
|
||||
const headers = {
|
||||
Referer: `${CDN_ORIGIN}/`,
|
||||
Origin: CDN_ORIGIN,
|
||||
};
|
||||
|
||||
const allUrls = [{
|
||||
url: streamUrl,
|
||||
server: '123Anime',
|
||||
quality: 'Auto',
|
||||
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers,
|
||||
}];
|
||||
|
||||
return {
|
||||
url: streamUrl,
|
||||
headers,
|
||||
allUrls,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,101 +1,45 @@
|
||||
import { getUA } from "../utils/helpers.js";
|
||||
import { getTmdbMetadata, getUA, resolveTmdbKey, slugify } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = "https://3eyedraven.watch";
|
||||
const API_BASE = "https://3eyedraven.watch/api";
|
||||
|
||||
function getHeaders() {
|
||||
return {
|
||||
"User-Agent": getUA(),
|
||||
Accept: "application/json",
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
};
|
||||
}
|
||||
|
||||
function resolveApiKey(args) {
|
||||
return (
|
||||
args?.tmdbApiKey ||
|
||||
args?.sdk?.tmdbApiKey ||
|
||||
process.env.TMDB_API_KEY ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
async function getTitle(id, isTv, apiKey) {
|
||||
if (!apiKey) return null;
|
||||
const type = isTv ? "tv" : "movie";
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`,
|
||||
{ signal: AbortSignal.timeout(5000) }
|
||||
);
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
return isTv ? data.name : data.title;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function slugify(text) {
|
||||
return text
|
||||
.toLowerCase()
|
||||
.normalize("NFKD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
const BASE_URL = 'https://3eyedraven.watch';
|
||||
const API_BASE = 'https://3eyedraven.watch/api';
|
||||
|
||||
function absolute(url) {
|
||||
if (!url) return "";
|
||||
if (!url) return '';
|
||||
if (/^https?:\/\//.test(url)) return url;
|
||||
return `${BASE_URL}${url.startsWith("/") ? "" : "/"}${url}`;
|
||||
return `${BASE_URL}${url.startsWith('/') ? '' : '/'}${url}`;
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s = null, e = null } = args;
|
||||
const { id, s, e, sdk } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const apiKey = resolveTmdbKey(args, sdk);
|
||||
|
||||
const apiKey = resolveApiKey(args);
|
||||
const title = await getTitle(id, isTv, apiKey);
|
||||
|
||||
if (!title) {
|
||||
if (process.env.DEBUG_CAUSE) {
|
||||
console.error(
|
||||
`[3eyedraven] Could not resolve title for ${isTv ? "tv" : "movie"} ${id} ` +
|
||||
`(tmdbApiKey ${apiKey ? "present" : "MISSING"})`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const meta = await getTmdbMetadata(id, isTv, apiKey);
|
||||
const title = meta?.title;
|
||||
if (!title) return null;
|
||||
|
||||
const identifier = `${slugify(title)}-${id}`;
|
||||
|
||||
const url = isTv
|
||||
? `${API_BASE}/stream/${identifier}?season=${s}&episode=${e}`
|
||||
: `${API_BASE}/stream/${identifier}`;
|
||||
|
||||
const headers = {
|
||||
'User-Agent': getUA(),
|
||||
Accept: 'application/json',
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: getHeaders(),
|
||||
headers,
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
if (process.env.DEBUG_CAUSE) {
|
||||
console.error(`[3eyedraven] HTTP ${res.status} for ${url}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (!res.ok) return null;
|
||||
|
||||
const data = await res.json();
|
||||
const stream = data?.stream;
|
||||
if (!stream?.url) {
|
||||
if (process.env.DEBUG_CAUSE) {
|
||||
console.error(`[3eyedraven] No stream.url in response for ${url}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (!stream?.url) return null;
|
||||
|
||||
const streamUrl = absolute(stream.url);
|
||||
if (!/^https?:\/\//.test(streamUrl)) return null;
|
||||
@@ -105,32 +49,35 @@ export async function getStream(args) {
|
||||
const subUrl = absolute(sub.url);
|
||||
if (!subUrl) return null;
|
||||
return {
|
||||
label: sub.label || sub.language || "Subtitle",
|
||||
label: sub.label || sub.language || 'Subtitle',
|
||||
file: subUrl,
|
||||
type: "vtt",
|
||||
source: "3eyedraven",
|
||||
type: 'vtt',
|
||||
source: '3eyedraven',
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
const allUrls = [{
|
||||
url: streamUrl,
|
||||
headers: {
|
||||
"User-Agent": getUA(),
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
},
|
||||
server: "3eyedraven",
|
||||
quality: "1080p",
|
||||
type: stream.format === "hls" ? "hls" : "mp4",
|
||||
headers,
|
||||
server: '3eyedraven',
|
||||
quality: '1080p',
|
||||
type: stream.format === 'hls' ? 'hls' : 'mp4',
|
||||
subtitles,
|
||||
skipProxy: true,
|
||||
}];
|
||||
|
||||
return {
|
||||
url: streamUrl,
|
||||
headers,
|
||||
server: '3eyedraven',
|
||||
quality: '1080p',
|
||||
type: stream.format === 'hls' ? 'hls' : 'mp4',
|
||||
subtitles,
|
||||
skipProxy: true,
|
||||
allUrls,
|
||||
};
|
||||
} catch (err) {
|
||||
if (process.env.DEBUG_CAUSE) {
|
||||
console.error(`[3eyedraven] fetch error for ${url}:`, err.message);
|
||||
if (err.cause) console.error(" cause:", err.cause.code, err.cause.message);
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,38 @@
|
||||
import { fetchJson } from '../utils/helpers.js';
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
const BASE_URL = 'https://zcdn.bluevelvet.space';
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
|
||||
const url = isTv
|
||||
? `https://zcdn.bluevelvet.space/cdn?tmdb=${id}&type=tv&season=${s}&episode=${e}`
|
||||
: `https://zcdn.bluevelvet.space/cdn?tmdb=${id}&type=movie`;
|
||||
? `${BASE_URL}/cdn?tmdb=${id}&type=tv&season=${s}&episode=${e}`
|
||||
: `${BASE_URL}/cdn?tmdb=${id}&type=movie`;
|
||||
|
||||
const res = await fetchJson(url);
|
||||
if (!res || !res.ready || !res.url) return null;
|
||||
try {
|
||||
const res = await fetchJson(url);
|
||||
if (!res || !res.ready || !res.url) return null;
|
||||
|
||||
return {
|
||||
url: res.url,
|
||||
headers: {
|
||||
Referer: 'https://zcdn.bluevelvet.space/',
|
||||
Origin: 'https://zcdn.bluevelvet.space',
|
||||
}
|
||||
};
|
||||
const headers = {
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
};
|
||||
|
||||
const allUrls = [{
|
||||
url: res.url,
|
||||
server: 'CineCat',
|
||||
quality: 'Auto',
|
||||
type: res.url.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers,
|
||||
}];
|
||||
|
||||
return {
|
||||
url: res.url,
|
||||
headers,
|
||||
allUrls,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
import crypto from "crypto";
|
||||
import { Session, ClientIdentifier, initTLS, destroyTLS } from "node-tls-client";
|
||||
import crypto from 'node:crypto';
|
||||
import { ClientIdentifier, destroyTLS, initTLS, Session } from 'node-tls-client';
|
||||
|
||||
const BASE_URL = "https://cinema.army";
|
||||
const API_BASE = "https://cinema.army/api";
|
||||
|
||||
const XOR_KEY = "f16a6b92fcd6eed3e9476297f893f2505c32f79bc6efef9b6c95496aa5914548";
|
||||
const HMAC_KEY = "a2cfc9eaa2a3690b5d4f6b5f958dbdeae52a097283d375bd253ee7c7062524f5";
|
||||
|
||||
const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
||||
const BASE_URL = 'https://cinema.army';
|
||||
const API_BASE = `${BASE_URL}/api`;
|
||||
const HMAC_KEY = 'a2cfc9eaa2a3690b5d4f6b5f958dbdeae52a097283d375bd253ee7c7062524f5';
|
||||
const XOR_KEY = 'f16a6b92fcd6eed3e9476297f893f2505c32f79bc6efef9b6c95496aa5914548';
|
||||
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36';
|
||||
|
||||
const SERVERS = [
|
||||
{ key: "cookiebakers", method: "POST", label: "CookieBakers" },
|
||||
{ key: "tcloud", method: "GET", label: "TCloud" },
|
||||
{ key: "ipcloud", method: "GET", label: "IPcloud" },
|
||||
{ key: "dcloud", method: "GET", label: "Dcloud" },
|
||||
{ key: 'cookiebakers', label: 'CookieBakers', method: 'POST' },
|
||||
{ key: 'tcloud', label: 'TCloud', method: 'POST' },
|
||||
{ key: 'ipcloud', label: 'IPCloud', method: 'POST' },
|
||||
{ key: 'dcloud', label: 'DCloud', method: 'POST' },
|
||||
{ key: 'tidb', label: 'TIDB', method: 'POST' },
|
||||
];
|
||||
|
||||
let tlsInitialized = false;
|
||||
@@ -40,22 +39,22 @@ function getHeaders(token, id, isTv, s, e) {
|
||||
: `${BASE_URL}/watch?id=${id}&type=movie`;
|
||||
|
||||
return {
|
||||
"User-Agent": UA,
|
||||
"Accept": "application/json",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"Cache-Control": "no-cache",
|
||||
"Pragma": "no-cache",
|
||||
"Referer": referer,
|
||||
"Origin": BASE_URL,
|
||||
"Sec-Ch-Ua": '"Chromium";v="131", "Not_A Brand";v="24"',
|
||||
"Sec-Ch-Ua-Mobile": "?0",
|
||||
"Sec-Ch-Ua-Platform": '"Windows"',
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Site": "same-origin",
|
||||
"Priority": "u=1, i",
|
||||
"X-Stream-Token": token,
|
||||
'User-Agent': UA,
|
||||
Accept: 'application/json',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Cache-Control': 'no-cache',
|
||||
Pragma: 'no-cache',
|
||||
Referer: referer,
|
||||
Origin: BASE_URL,
|
||||
'Sec-Ch-Ua': '"Chromium";v="131", "Not_A Brand";v="24"',
|
||||
'Sec-Ch-Ua-Mobile': '?0',
|
||||
'Sec-Ch-Ua-Platform': '"Windows"',
|
||||
'Sec-Fetch-Dest': 'empty',
|
||||
'Sec-Fetch-Mode': 'cors',
|
||||
'Sec-Fetch-Site': 'same-origin',
|
||||
Priority: 'u=1, i',
|
||||
'X-Stream-Token': token,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -63,20 +62,20 @@ function generateToken(id) {
|
||||
try {
|
||||
const minute = Math.floor(Date.now() / 1000 / 60);
|
||||
return crypto
|
||||
.createHmac("sha256", HMAC_KEY)
|
||||
.createHmac('sha256', HMAC_KEY)
|
||||
.update(`${id}:${minute}`)
|
||||
.digest("hex");
|
||||
.digest('hex');
|
||||
} catch {
|
||||
return "";
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function decryptStreamUrl(encoded) {
|
||||
if (!encoded) return "";
|
||||
if (encoded.startsWith("http://") || encoded.startsWith("https://")) return encoded;
|
||||
if (!encoded) return '';
|
||||
if (encoded.startsWith('http://') || encoded.startsWith('https://')) return encoded;
|
||||
try {
|
||||
const decoded = Buffer.from(encoded, "base64").toString("binary");
|
||||
let out = "";
|
||||
const decoded = Buffer.from(encoded, 'base64').toString('binary');
|
||||
let out = '';
|
||||
for (let i = 0; i < decoded.length; i++) {
|
||||
out += String.fromCharCode(
|
||||
decoded.charCodeAt(i) ^ XOR_KEY.charCodeAt(i % XOR_KEY.length)
|
||||
@@ -89,7 +88,7 @@ function decryptStreamUrl(encoded) {
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s = null, e = null } = args;
|
||||
const { id, s, e } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
|
||||
const token = generateToken(id);
|
||||
@@ -107,62 +106,62 @@ export async function getStream(args) {
|
||||
const results = await Promise.allSettled(
|
||||
requests.map(async ({ url, server }) => {
|
||||
try {
|
||||
const res = server.method === "POST"
|
||||
? await sess.post(url, { headers })
|
||||
const res = server.method === 'POST'
|
||||
? await sess.post(url, { headers, body: '' })
|
||||
: await sess.get(url, { headers });
|
||||
|
||||
if (res.status !== 200) {
|
||||
if (process.env.DEBUG_CAUSE) {
|
||||
console.error(`[CinemaArmy ${server.key}] HTTP ${res.status}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (res?.status !== 200) return null;
|
||||
|
||||
const rawBody = typeof res.text === 'function' ? await res.text() : res.body;
|
||||
|
||||
const text = await res.text();
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch {
|
||||
data = typeof rawBody === 'object' && rawBody !== null ? rawBody : JSON.parse(rawBody);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const encrypted = data?.stream?.url;
|
||||
if (!encrypted) return null;
|
||||
if (!encrypted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const streamUrl = decryptStreamUrl(encrypted);
|
||||
if (!streamUrl || !/^https?:\/\//.test(streamUrl)) return null;
|
||||
|
||||
if (!streamUrl || !/^https?:\/\//.test(streamUrl)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
url: streamUrl,
|
||||
headers: {
|
||||
"User-Agent": UA,
|
||||
'User-Agent': UA,
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
},
|
||||
server: `CinemaArmy (${server.label})`,
|
||||
quality: "1080p",
|
||||
type: streamUrl.includes(".m3u8") ? "hls" : "mp4",
|
||||
quality: '1080p',
|
||||
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
};
|
||||
} catch (err) {
|
||||
if (process.env.DEBUG_CAUSE) {
|
||||
console.error(`[CinemaArmy ${server.key}] ${err.message}`);
|
||||
if (err.cause) {
|
||||
console.error(` cause.code: ${err.cause.code || "n/a"}`);
|
||||
console.error(` cause.message: ${err.cause.message || err.cause}`);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const allUrls = results
|
||||
.filter(r => r.status === "fulfilled" && r.value)
|
||||
.filter(r => r.status === 'fulfilled' && r.value)
|
||||
.map(r => r.value);
|
||||
|
||||
if (!allUrls.length) return null;
|
||||
if (!allUrls.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { allUrls };
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
|
||||
export async function cleanup() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { fetchJson, fetchText } from '../utils/helpers.js';
|
||||
|
||||
const API = "https://enc-dec.app/api";
|
||||
const API = 'https://enc-dec.app/api';
|
||||
|
||||
async function solveStage1(challenge1, seed) {
|
||||
const K = [
|
||||
@@ -210,91 +210,93 @@ async function solveStage2(data) {
|
||||
throw new Error('no solution found');
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const mediaType = isTv ? 'tv' : 'movie';
|
||||
const type = isTv ? 'tv' : 'movie';
|
||||
const url = isTv
|
||||
? `https://cinesrc.st/embed/tv/${id}?s=${s}&e=${e}`
|
||||
: `https://cinesrc.st/embed/movie/${id}`;
|
||||
|
||||
const headers = {
|
||||
"Origin": "https://cinesrc.st",
|
||||
"Referer": "https://cinesrc.st/",
|
||||
"Content-Type": "text/plain;charset=UTF-8",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.37 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.37"
|
||||
Origin: 'https://cinesrc.st',
|
||||
Referer: 'https://cinesrc.st/',
|
||||
'Content-Type': 'text/plain;charset=UTF-8',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.37 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.37',
|
||||
};
|
||||
|
||||
const fields = [type, id, s ? String(s) : null, e ? String(e) : null];
|
||||
const encoded = btoa(JSON.stringify(fields).replace(/ /g, '')).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
|
||||
const bootstrapHeaders = { ...headers, "x-cs-q": encoded };
|
||||
const bootstrap = await fetchJson("https://cinesrc.st/api/c/bootstrap", {
|
||||
method: "POST",
|
||||
const bootstrapHeaders = { ...headers, 'x-cs-q': encoded };
|
||||
const bootstrap = await fetchJson('https://cinesrc.st/api/c/bootstrap', {
|
||||
method: 'POST',
|
||||
headers: bootstrapHeaders,
|
||||
body: "{}"
|
||||
body: '{}',
|
||||
});
|
||||
|
||||
const cookies = {
|
||||
"x-cs-b": "20261014-a",
|
||||
"x-cs-q": encoded,
|
||||
"x-cs-r": bootstrap.r,
|
||||
"x-cs-p": bootstrap.p,
|
||||
'x-cs-b': '20261014-a',
|
||||
'x-cs-q': encoded,
|
||||
'x-cs-r': bootstrap.r,
|
||||
'x-cs-p': bootstrap.p,
|
||||
};
|
||||
|
||||
const challenge1 = await fetchJson("https://cinesrc.st/api/c/issue", {
|
||||
headers: { ...headers, ...cookies }
|
||||
const challenge1 = await fetchJson('https://cinesrc.st/api/c/issue', {
|
||||
headers: { ...headers, ...cookies },
|
||||
});
|
||||
|
||||
const payload1 = {
|
||||
data: {
|
||||
url,
|
||||
user_agent: headers["User-Agent"],
|
||||
challenge_data: challenge1
|
||||
user_agent: headers['User-Agent'],
|
||||
challenge_data: challenge1,
|
||||
},
|
||||
type: "seed"
|
||||
type: 'seed',
|
||||
};
|
||||
|
||||
const seedRes = await fetchJson(`${API}/enc-cinesrc`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload1)
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload1),
|
||||
});
|
||||
|
||||
const seedData = seedRes.result;
|
||||
const solution1 = await solveStage1(challenge1, seedData.seed);
|
||||
|
||||
const challenge2 = await fetchJson("https://cinesrc.st/api/c/stage2/issue", {
|
||||
headers: { ...headers, ...cookies }
|
||||
const challenge2 = await fetchJson('https://cinesrc.st/api/c/stage2/issue', {
|
||||
headers: { ...headers, ...cookies },
|
||||
});
|
||||
const solution2 = await solveStage2(challenge2);
|
||||
|
||||
const payload2 = {
|
||||
data: {
|
||||
url,
|
||||
user_agent: headers["User-Agent"],
|
||||
user_agent: headers['User-Agent'],
|
||||
challenge_data: {
|
||||
stage1: { challenge: challenge1, solution: solution1 },
|
||||
stage2: { challenge: challenge2, solution: solution2 }
|
||||
stage2: { challenge: challenge2, solution: solution2 },
|
||||
},
|
||||
state: seedData.state
|
||||
state: seedData.state,
|
||||
},
|
||||
type: "encrypt"
|
||||
type: 'encrypt',
|
||||
};
|
||||
|
||||
const encryptRes = await fetchJson(`${API}/enc-cinesrc`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload2)
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload2),
|
||||
});
|
||||
|
||||
const encData = encryptRes.result;
|
||||
const token = `${encData.token}::c3::${cookies["x-cs-r"]}`;
|
||||
const token = `${encData.token}::c3::${cookies['x-cs-r']}`;
|
||||
const nextActions = encData.headers;
|
||||
|
||||
const providersText = await fetchText(url, {
|
||||
method: "POST",
|
||||
headers: { ...headers, ...cookies, "Next-Action": nextActions.getProviderList },
|
||||
body: JSON.stringify([])
|
||||
method: 'POST',
|
||||
headers: { ...headers, ...cookies, 'Next-Action': nextActions.getProviderList },
|
||||
body: JSON.stringify([]),
|
||||
});
|
||||
|
||||
const lines = providersText.split('\n');
|
||||
@@ -320,19 +322,19 @@ export async function getStream({ id, s, e }) {
|
||||
const provider = providerObj.id;
|
||||
const streamPayload = [
|
||||
id,
|
||||
isTv ? "show" : type,
|
||||
isTv ? String(s) : "$undefined",
|
||||
isTv ? String(e) : "$undefined",
|
||||
isTv ? 'show' : type,
|
||||
isTv ? String(s) : '$undefined',
|
||||
isTv ? String(e) : '$undefined',
|
||||
token,
|
||||
provider
|
||||
provider,
|
||||
];
|
||||
|
||||
let streamRes;
|
||||
try {
|
||||
streamRes = await fetchText(url, {
|
||||
method: "POST",
|
||||
headers: { ...headers, ...cookies, "Next-Action": nextActions.getStream },
|
||||
body: JSON.stringify(streamPayload)
|
||||
method: 'POST',
|
||||
headers: { ...headers, ...cookies, 'Next-Action': nextActions.getStream },
|
||||
body: JSON.stringify(streamPayload),
|
||||
});
|
||||
} catch {
|
||||
continue;
|
||||
@@ -343,9 +345,9 @@ export async function getStream({ id, s, e }) {
|
||||
let decryptRes;
|
||||
try {
|
||||
decryptRes = await fetchJson(`${API}/dec-cinesrc`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ text: streamRes, state: encData.state })
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: streamRes, state: encData.state }),
|
||||
});
|
||||
} catch {
|
||||
continue;
|
||||
@@ -378,10 +380,12 @@ export async function getStream({ id, s, e }) {
|
||||
allUrls.push({
|
||||
url: streamUrl,
|
||||
server: `CineSrc - ${providerObj.name || provider}`,
|
||||
quality: 'Auto',
|
||||
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: resultData.headers || {
|
||||
Referer: "https://cinesrc.st/",
|
||||
Origin: "https://cinesrc.st"
|
||||
}
|
||||
Referer: 'https://cinesrc.st/',
|
||||
Origin: 'https://cinesrc.st',
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -393,6 +397,6 @@ export async function getStream({ id, s, e }) {
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { fetchJson } from '../utils/helpers.js';
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
const isTv = s != null && e != null;
|
||||
const referer = isTv
|
||||
? `https://player.cinezo.live/embed/tv/${id}?s=${s}&e=${e}`
|
||||
: `https://player.cinezo.live/embed/movie/${id}`;
|
||||
|
||||
const headers = {
|
||||
'Accept': '*/*',
|
||||
'Origin': 'https://player.cinezo.live',
|
||||
'Referer': referer,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36 Edg/155.0.0.0'
|
||||
};
|
||||
|
||||
const hosts = [
|
||||
'https://proxy.yenime.net',
|
||||
'https://proxy1.flikhub.net'
|
||||
];
|
||||
|
||||
const sources = ['zendaya', 'berlin', 'jennifer', 'scarlet'];
|
||||
const allUrls = [];
|
||||
|
||||
for (const host of hosts) {
|
||||
for (const source of sources) {
|
||||
const url = isTv
|
||||
? `${host}/tv?id=${id}&season=${s}&episode=${e}&mode=json&sources=${source}&hevc=1`
|
||||
: `${host}/movie?id=${id}&mode=json&sources=${source}&hevc=1`;
|
||||
|
||||
try {
|
||||
const res = await fetchJson(url, { headers });
|
||||
const streamUrl = res?.source?.url;
|
||||
if (streamUrl) {
|
||||
allUrls.push({
|
||||
url: streamUrl,
|
||||
server: `Cinezo - ${res.source.label || source}`,
|
||||
headers: {
|
||||
Referer: 'https://player.cinezo.live/',
|
||||
Origin: 'https://player.cinezo.live'
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
if (allUrls.length) break;
|
||||
}
|
||||
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls
|
||||
};
|
||||
}
|
||||
@@ -1,20 +1,23 @@
|
||||
import { fetchText } from '../utils/helpers.js';
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
const BASE_URL = 'https://cno.to';
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const serverIndices = Array.from({ length: 50 }, (_, i) => i + 1);
|
||||
|
||||
const headers = {
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Referer': 'https://cno.to/',
|
||||
'Origin': 'https://cno.to',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
|
||||
};
|
||||
|
||||
const promises = serverIndices.map(async (serverNum) => {
|
||||
const url = isTv
|
||||
? `https://cno.to/api/s/${serverNum}/tv/${id}/${s}/${e}`
|
||||
: `https://cno.to/api/s/${serverNum}/movie/${id}?v=3`;
|
||||
? `${BASE_URL}/api/s/${serverNum}/tv/${id}/${s}/${e}`
|
||||
: `${BASE_URL}/api/s/${serverNum}/movie/${id}?v=3`;
|
||||
|
||||
try {
|
||||
const raw = await fetchText(url, { headers });
|
||||
@@ -50,16 +53,18 @@ export async function getStream({ id, s, e }) {
|
||||
|
||||
if (streamUrl) {
|
||||
if (streamUrl.startsWith('/')) {
|
||||
streamUrl = `https://cno.to${streamUrl}`;
|
||||
streamUrl = `${BASE_URL}${streamUrl}`;
|
||||
}
|
||||
|
||||
return {
|
||||
url: streamUrl,
|
||||
server: `Cno - Server ${serverNum} (${quality})`,
|
||||
quality,
|
||||
type: 'hls',
|
||||
headers: {
|
||||
Referer: 'https://cno.to/',
|
||||
Origin: 'https://cno.to'
|
||||
}
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -71,10 +76,12 @@ export async function getStream({ id, s, e }) {
|
||||
return {
|
||||
url: jsonUrl,
|
||||
server: `Cno - Server ${serverNum} (${json.quality || 'Auto'})`,
|
||||
quality: json.quality || 'Auto',
|
||||
type: jsonUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: {
|
||||
Referer: 'https://cno.to/',
|
||||
Origin: 'https://cno.to'
|
||||
}
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
},
|
||||
};
|
||||
}
|
||||
} catch { }
|
||||
@@ -90,6 +97,6 @@ export async function getStream({ id, s, e }) {
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
@@ -1,28 +1,8 @@
|
||||
import { getUA } from '../utils/helpers.js';
|
||||
import { getTmdbMetadata, getUA, resolveTmdbKey } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = 'https://dulo.mov';
|
||||
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
async function resolveDetails(sdk, id, isTv) {
|
||||
const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY;
|
||||
if (!apiKey) return null;
|
||||
const type = isTv ? 'tv' : 'movie';
|
||||
try {
|
||||
const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`, {
|
||||
signal: AbortSignal.timeout(6000),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
const title = isTv ? data.name : data.title;
|
||||
const releaseDate = isTv ? data.first_air_date : data.release_date;
|
||||
const year = releaseDate ? releaseDate.split('-')[0] : '';
|
||||
const runtime = isTv ? (data.episode_run_time?.[0] || 24) : (data.runtime || 90);
|
||||
return { title, year, runtime };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
async function fetchSource(url, headers) {
|
||||
try {
|
||||
@@ -37,15 +17,17 @@ async function fetchSource(url, headers) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e, sdk }) {
|
||||
const isTv = Boolean(s && e);
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, sdk } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const mediaType = isTv ? 'tv' : 'movie';
|
||||
const apiKey = resolveTmdbKey(args, sdk);
|
||||
const ua = getUA();
|
||||
|
||||
const details = await resolveDetails(sdk, id, isTv);
|
||||
const title = details?.title || '';
|
||||
const year = details?.year || '';
|
||||
const runtime = details?.runtime || '';
|
||||
const meta = await getTmdbMetadata(id, isTv, apiKey);
|
||||
const title = meta?.title || '';
|
||||
const year = meta?.year || '';
|
||||
const runtime = meta?.runtime || '';
|
||||
|
||||
const referer = isTv
|
||||
? `${BASE_URL}/watch/tv/${id}/${s}/${e}`
|
||||
@@ -97,7 +79,7 @@ export async function getStream({ id, s, e, sdk }) {
|
||||
|
||||
while (pendingUrls.length > 0 && Date.now() < maxPollTime) {
|
||||
const results = await Promise.all(
|
||||
pendingUrls.map((url) => fetchSource(url, defaultHeaders))
|
||||
pendingUrls.map(url => fetchSource(url, defaultHeaders))
|
||||
);
|
||||
|
||||
const nextPending = [];
|
||||
@@ -119,7 +101,10 @@ export async function getStream({ id, s, e, sdk }) {
|
||||
|
||||
allUrls.push({
|
||||
url: streamUrl,
|
||||
server: src.label || 'Dulo',
|
||||
label: src.label || 'Dulo',
|
||||
quality: 'Auto',
|
||||
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: {
|
||||
'User-Agent': ua,
|
||||
Referer: referer,
|
||||
@@ -144,7 +129,7 @@ export async function getStream({ id, s, e, sdk }) {
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
allUrls,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
@@ -1,106 +1,115 @@
|
||||
import { fetchJson } from '../utils/helpers.js';
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
const isTv = s != null && e != null;
|
||||
const allUrls = [];
|
||||
|
||||
const endpoints = [
|
||||
async () => {
|
||||
const url = isTv
|
||||
? `https://themoviebox.uk/api/resolve?type=tv&id=${id}&season=${s}&episode=${e}`
|
||||
: `https://themoviebox.uk/api/resolve?type=movie&id=${id}`;
|
||||
const res = await fetchJson(url);
|
||||
if (res?.success && Array.isArray(res.sources)) {
|
||||
for (const item of res.sources) {
|
||||
const streamUrl = item.url || item.original_url;
|
||||
if (streamUrl) {
|
||||
allUrls.push({
|
||||
url: streamUrl,
|
||||
server: `TheMovieBox - ${item.server || item.provider || 'Server'}`,
|
||||
headers: {
|
||||
Referer: 'https://themoviebox.uk/',
|
||||
Origin: 'https://themoviebox.uk'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async () => {
|
||||
const url = isTv
|
||||
? `https://pro2.nabilekson.workers.dev/tv/${id}/${s}/${e}`
|
||||
: `https://pro2.nabilekson.workers.dev/movie/${id}`;
|
||||
const res = await fetchJson(url);
|
||||
if (res?.success && Array.isArray(res.primarySources)) {
|
||||
for (const item of res.primarySources) {
|
||||
const streamUrl = item.url || item.originalUrl;
|
||||
if (streamUrl) {
|
||||
allUrls.push({
|
||||
url: streamUrl,
|
||||
server: `CineBros - ${item.name || item.quality || 'Server'}`,
|
||||
headers: item.headers || {
|
||||
Referer: 'https://pro2.nabilekson.workers.dev/',
|
||||
Origin: 'https://pro2.nabilekson.workers.dev'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async () => {
|
||||
const url = isTv
|
||||
? `https://praxi.nabilekson.workers.dev/tv/${id}/${s}/${e}`
|
||||
: `https://praxi.nabilekson.workers.dev/movie/${id}`;
|
||||
const res = await fetchJson(url);
|
||||
if (res?.success && Array.isArray(res.primarySources)) {
|
||||
for (const item of res.primarySources) {
|
||||
const streamUrl = item.url || item.originalUrl;
|
||||
if (streamUrl) {
|
||||
allUrls.push({
|
||||
url: streamUrl,
|
||||
server: `Praxi - ${item.name || item.providerName || 'Server'}`,
|
||||
headers: item.headers || {
|
||||
Referer: 'https://praxi.nabilekson.workers.dev/',
|
||||
Origin: 'https://praxi.nabilekson.workers.dev'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async () => {
|
||||
const url = isTv
|
||||
? `https://freakyniki.elaxo.lol/?tmdb_id=${id}&season=${s}&episode=${e}`
|
||||
: `https://freakyniki.elaxo.lol/?tmdb_id=${id}`;
|
||||
const res = await fetchJson(url);
|
||||
if (Array.isArray(res?.streams)) {
|
||||
for (const item of res.streams) {
|
||||
const streamUrl = item.url || item.raw_url;
|
||||
if (streamUrl) {
|
||||
allUrls.push({
|
||||
url: streamUrl,
|
||||
server: `FreakyNiki - ${item.server || item.provider || 'Server'}`,
|
||||
headers: {
|
||||
Referer: 'https://freakyniki.elaxo.lol/',
|
||||
Origin: 'https://freakyniki.elaxo.lol'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
await Promise.allSettled(endpoints.map(fn => fn().catch(() => { })));
|
||||
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls
|
||||
};
|
||||
import { fetchJson } from '../utils/helpers.js';
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const allUrls = [];
|
||||
|
||||
const endpoints = [
|
||||
async () => {
|
||||
const url = isTv
|
||||
? `https://themoviebox.uk/api/resolve?type=tv&id=${id}&season=${s}&episode=${e}`
|
||||
: `https://themoviebox.uk/api/resolve?type=movie&id=${id}`;
|
||||
const res = await fetchJson(url);
|
||||
if (res?.success && Array.isArray(res.sources)) {
|
||||
for (const item of res.sources) {
|
||||
const streamUrl = item.url || item.original_url;
|
||||
if (streamUrl) {
|
||||
allUrls.push({
|
||||
url: streamUrl,
|
||||
server: `TheMovieBox - ${item.server || item.provider || 'Server'}`,
|
||||
quality: 'Auto',
|
||||
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: {
|
||||
Referer: 'https://themoviebox.uk/',
|
||||
Origin: 'https://themoviebox.uk',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async () => {
|
||||
const url = isTv
|
||||
? `https://pro2.nabilekson.workers.dev/tv/${id}/${s}/${e}`
|
||||
: `https://pro2.nabilekson.workers.dev/movie/${id}`;
|
||||
const res = await fetchJson(url);
|
||||
if (res?.success && Array.isArray(res.primarySources)) {
|
||||
for (const item of res.primarySources) {
|
||||
const streamUrl = item.url || item.originalUrl;
|
||||
if (streamUrl) {
|
||||
allUrls.push({
|
||||
url: streamUrl,
|
||||
server: `CineBros - ${item.name || item.quality || 'Server'}`,
|
||||
quality: item.quality || 'Auto',
|
||||
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: item.headers || {
|
||||
Referer: 'https://pro2.nabilekson.workers.dev/',
|
||||
Origin: 'https://pro2.nabilekson.workers.dev',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async () => {
|
||||
const url = isTv
|
||||
? `https://praxi.nabilekson.workers.dev/tv/${id}/${s}/${e}`
|
||||
: `https://praxi.nabilekson.workers.dev/movie/${id}`;
|
||||
const res = await fetchJson(url);
|
||||
if (res?.success && Array.isArray(res.primarySources)) {
|
||||
for (const item of res.primarySources) {
|
||||
const streamUrl = item.url || item.originalUrl;
|
||||
if (streamUrl) {
|
||||
allUrls.push({
|
||||
url: streamUrl,
|
||||
server: `Praxi - ${item.name || item.providerName || 'Server'}`,
|
||||
quality: 'Auto',
|
||||
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: item.headers || {
|
||||
Referer: 'https://praxi.nabilekson.workers.dev/',
|
||||
Origin: 'https://praxi.nabilekson.workers.dev',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async () => {
|
||||
const url = isTv
|
||||
? `https://freakyniki.elaxo.lol/?tmdb_id=${id}&season=${s}&episode=${e}`
|
||||
: `https://freakyniki.elaxo.lol/?tmdb_id=${id}`;
|
||||
const res = await fetchJson(url);
|
||||
if (Array.isArray(res?.streams)) {
|
||||
for (const item of res.streams) {
|
||||
const streamUrl = item.url || item.raw_url;
|
||||
if (streamUrl) {
|
||||
allUrls.push({
|
||||
url: streamUrl,
|
||||
server: `FreakyNiki - ${item.server || item.provider || 'Server'}`,
|
||||
quality: 'Auto',
|
||||
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: {
|
||||
Referer: 'https://freakyniki.elaxo.lol/',
|
||||
Origin: 'https://freakyniki.elaxo.lol',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
await Promise.allSettled(endpoints.map(fn => fn().catch(() => { })));
|
||||
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
@@ -1,30 +1,39 @@
|
||||
import { fetchJson, resolveImdbId } from '../utils/helpers.js';
|
||||
import { fetchJson, resolveImdbId, resolveTmdbKey } from '../utils/helpers.js';
|
||||
|
||||
export async function getStream({ id, s, e, tmdbApiKey, sdk }) {
|
||||
const BASE_URL = 'https://froststream.cloutteam.com';
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, sdk } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const type = isTv ? 'series' : 'movie';
|
||||
const imdb = await resolveImdbId(id, isTv, tmdbApiKey || sdk?.tmdbApiKey);
|
||||
const apiKey = resolveTmdbKey(args, sdk);
|
||||
|
||||
const imdb = await resolveImdbId(id, isTv, apiKey);
|
||||
if (!imdb) return null;
|
||||
|
||||
const url = isTv
|
||||
? `https://froststream.cloutteam.com/stream/series/${imdb}:${s}:${e}.json`
|
||||
: `https://froststream.cloutteam.com/stream/movie/${imdb}.json`;
|
||||
? `${BASE_URL}/stream/series/${imdb}:${s}:${e}.json`
|
||||
: `${BASE_URL}/stream/movie/${imdb}.json`;
|
||||
|
||||
const res = await fetchJson(url).catch(() => null);
|
||||
if (!res || !Array.isArray(res.streams) || !res.streams.length) return null;
|
||||
|
||||
const allUrls = res.streams.filter(st => st.url).map(st => ({
|
||||
url: st.url,
|
||||
server: `Frost - ${st.name || 'Server'} ${st.title ? `- ${st.title}` : ''}`,
|
||||
headers: st.behaviorHints?.proxyHeaders?.request || undefined,
|
||||
skipProxy: st.behaviorHints?.notWebReady === false
|
||||
}));
|
||||
const allUrls = res.streams
|
||||
.filter(st => st.url)
|
||||
.map(st => ({
|
||||
url: st.url,
|
||||
server: `Frost - ${st.name || 'Server'}${st.title ? ` - ${st.title}` : ''}`,
|
||||
quality: 'Auto',
|
||||
type: st.url.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: st.behaviorHints?.proxyHeaders?.request || undefined,
|
||||
skipProxy: st.behaviorHints?.notWebReady === false,
|
||||
}));
|
||||
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
@@ -1,48 +1,96 @@
|
||||
import { fetchText, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
|
||||
|
||||
const ORIGIN = 'https://www3.fsonline.app';
|
||||
const AJAX_URL = `${ORIGIN}/wp-admin/admin-ajax.php`;
|
||||
const HEADERS = { 'User-Agent': USER_AGENT, 'Origin': ORIGIN, 'Referer': `${ORIGIN}/` };
|
||||
|
||||
export async function getStream({ id, s, e, sdk }) {
|
||||
const tmdbApiKey = sdk?.tmdbApiKey || null;
|
||||
if (!tmdbApiKey) return null;
|
||||
try {
|
||||
const isTv = s != null && e != null;
|
||||
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';
|
||||
const linkMatch = searchHtml.match(new RegExp(`href=["'](https?://www3\\.fsonline\\.app/${typeFolder}/([^"'/]+)/)["']`, 'i'));
|
||||
if (!linkMatch) return null;
|
||||
const targetPageUrl = isTv ? `${ORIGIN}/episoade/${linkMatch[2].replace(/-\d{4}$/, '')}-sezonul-${s}-episodul-${e}/` : linkMatch[1];
|
||||
const pageHtml = await fetchText(targetPageUrl, { headers: HEADERS });
|
||||
const movieId = (pageHtml.match(/movie-id=['"]([^'"]+)['"]/) || pageHtml.match(/movie-id=([^ >]+)/))?.[1];
|
||||
if (!movieId) return null;
|
||||
const ajaxHtml = await fetchText(AJAX_URL, { method: 'POST', headers: { ...HEADERS, 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest', 'Referer': targetPageUrl }, body: `action=lazy_player&movieID=${movieId}` });
|
||||
const results = [];
|
||||
let idx = 0;
|
||||
while ((idx = ajaxHtml.indexOf('data-vs="', idx)) !== -1) {
|
||||
const embedStart = idx + 9;
|
||||
const embedEnd = ajaxHtml.indexOf('"', embedStart);
|
||||
const embedUrl = ajaxHtml.slice(embedStart, embedEnd);
|
||||
const spanStart = ajaxHtml.indexOf('<span>', embedEnd);
|
||||
const spanEnd = ajaxHtml.indexOf('</span>', spanStart);
|
||||
const serverLabel = ajaxHtml.slice(spanStart + 6, spanEnd).trim().toLowerCase();
|
||||
if (serverLabel.includes('filesun')) {
|
||||
try {
|
||||
const rHtml = await fetchText(embedUrl, { headers: { 'Referer': ORIGIN, 'User-Agent': USER_AGENT } });
|
||||
const m3u8Match = rHtml.match(/file:\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/) || rHtml.match(/["']?file["']?:\s*["'](https?:\/\/[^"']+)["']/);
|
||||
if (m3u8Match) results.push({ url: m3u8Match[1].replace(/\\\//g, '/'), server: 'FSOnline - FileSuN', headers: { 'Referer': embedUrl, 'Origin': 'https://player.fsonline.app', 'User-Agent': USER_AGENT } });
|
||||
} catch { }
|
||||
}
|
||||
idx = spanEnd;
|
||||
}
|
||||
return results.length ? { allUrls: results } : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
export async function getSources(args) {
|
||||
const res = await getStream(args);
|
||||
return res?.allUrls ? [...new Set(res.allUrls.map(u => u.server))] : [];
|
||||
import { fetchText, getTmdbMetadata, resolveTmdbKey, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const ORIGIN = 'https://www3.fsonline.app';
|
||||
const AJAX_URL = `${ORIGIN}/wp-admin/admin-ajax.php`;
|
||||
const HEADERS = {
|
||||
'User-Agent': USER_AGENT,
|
||||
Origin: ORIGIN,
|
||||
Referer: `${ORIGIN}/`,
|
||||
};
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, sdk } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const apiKey = resolveTmdbKey(args, sdk);
|
||||
if (!apiKey) return null;
|
||||
|
||||
try {
|
||||
const meta = await getTmdbMetadata(id, isTv, apiKey);
|
||||
if (!meta?.titles?.length) return null;
|
||||
|
||||
const query = meta.titles[0] + (meta.year ? ` ${meta.year}` : '');
|
||||
const searchHtml = await fetchText(`${ORIGIN}/?s=${encodeURIComponent(query)}`, { headers: HEADERS });
|
||||
const typeFolder = isTv ? 'seriale' : 'film';
|
||||
const linkMatch = searchHtml.match(new RegExp(`href=["'](https?://www3\\.fsonline\\.app/${typeFolder}/([^"'/]+)/)["']`, 'i'));
|
||||
if (!linkMatch) return null;
|
||||
|
||||
const targetPageUrl = isTv
|
||||
? `${ORIGIN}/episoade/${linkMatch[2].replace(/-\d{4}$/, '')}-sezonul-${s}-episodul-${e}/`
|
||||
: linkMatch[1];
|
||||
|
||||
const pageHtml = await fetchText(targetPageUrl, { headers: HEADERS });
|
||||
const movieId = (pageHtml.match(/movie-id=['"]([^'"]+)['"]/) || pageHtml.match(/movie-id=([^ >]+)/))?.[1];
|
||||
if (!movieId) return null;
|
||||
|
||||
const ajaxHtml = await fetchText(AJAX_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...HEADERS,
|
||||
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
Referer: targetPageUrl,
|
||||
},
|
||||
body: `action=lazy_player&movieID=${movieId}`,
|
||||
});
|
||||
|
||||
const allUrls = [];
|
||||
let idx = 0;
|
||||
while ((idx = ajaxHtml.indexOf('data-vs="', idx)) !== -1) {
|
||||
const embedStart = idx + 9;
|
||||
const embedEnd = ajaxHtml.indexOf('"', embedStart);
|
||||
const embedUrl = ajaxHtml.slice(embedStart, embedEnd);
|
||||
const spanStart = ajaxHtml.indexOf('<span>', embedEnd);
|
||||
const spanEnd = ajaxHtml.indexOf('</span>', spanStart);
|
||||
const serverLabel = ajaxHtml.slice(spanStart + 6, spanEnd).trim().toLowerCase();
|
||||
|
||||
if (serverLabel.includes('filesun')) {
|
||||
try {
|
||||
const rHtml = await fetchText(embedUrl, {
|
||||
headers: { Referer: ORIGIN, 'User-Agent': USER_AGENT },
|
||||
});
|
||||
const m3u8Match = rHtml.match(/file:\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/) || rHtml.match(/["']?file["']?:\s*["'](https?:\/\/[^"']+)["']/);
|
||||
if (m3u8Match) {
|
||||
const streamUrl = m3u8Match[1].replace(/\\\//g, '/');
|
||||
allUrls.push({
|
||||
url: streamUrl,
|
||||
server: 'FSOnline - FileSuN',
|
||||
quality: 'Auto',
|
||||
type: 'hls',
|
||||
headers: {
|
||||
Referer: embedUrl,
|
||||
Origin: 'https://player.fsonline.app',
|
||||
'User-Agent': USER_AGENT,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
idx = spanEnd;
|
||||
}
|
||||
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSources(args) {
|
||||
const stream = await getStream(args);
|
||||
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
import { fetchJson, resolveImdbId } from '../utils/helpers.js';
|
||||
import { fetchJson, resolveImdbId, resolveTmdbKey } from '../utils/helpers.js';
|
||||
|
||||
export async function getStream({ id, s, e, tmdbApiKey, sdk }) {
|
||||
const BASE_URL = 'https://hdhub.thevolecitor.qzz.io';
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, sdk } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const type = isTv ? 'series' : 'movie';
|
||||
const imdb = await resolveImdbId(id, isTv, tmdbApiKey || sdk?.tmdbApiKey);
|
||||
const apiKey = resolveTmdbKey(args, sdk);
|
||||
|
||||
const imdb = await resolveImdbId(id, isTv, apiKey);
|
||||
if (!imdb) return null;
|
||||
|
||||
const CONFIG = btoa(JSON.stringify({ "torbox": "unset", "qualities": "2160p,1080p,720p", "sort": "desc", "catalogs": "featured,popular" }));
|
||||
const url = `https://hdhub.thevolecitor.qzz.io/${CONFIG}/stream/${type}/${imdb}${isTv ? `:${s}:${e}` : ""}.json`;
|
||||
const config = btoa(JSON.stringify({ torbox: 'unset', qualities: '2160p,1080p,720p', sort: 'desc', catalogs: 'featured,popular' }));
|
||||
const url = `${BASE_URL}/${config}/stream/${type}/${imdb}${isTv ? `:${s}:${e}` : ''}.json`;
|
||||
|
||||
const res = await fetchJson(url).catch(() => null);
|
||||
if (!res || !Array.isArray(res.streams) || !res.streams.length) return null;
|
||||
@@ -24,13 +29,15 @@ export async function getStream({ id, s, e, tmdbApiKey, sdk }) {
|
||||
const allUrls = validStreams.map(st => ({
|
||||
url: st.url,
|
||||
server: `HDHub - ${st.name || 'Server'}`,
|
||||
quality: 'Auto',
|
||||
type: st.url.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: st.behaviorHints?.proxyHeaders?.request || undefined,
|
||||
skipProxy: st.behaviorHints?.notWebReady === false
|
||||
skipProxy: st.behaviorHints?.notWebReady === false,
|
||||
}));
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1,61 @@
|
||||
import { fetchJson, fetchText } from '../utils/helpers.js';
|
||||
import { fetchJson, fetchText, getRandomHex } from '../utils/helpers.js';
|
||||
|
||||
const API = "https://enc-dec.app/api";
|
||||
const API_BASE = 'https://enc-dec.app/api';
|
||||
const BASE_URL = 'https://hexa.su';
|
||||
const TMDB_PROXY_BASE = 'https://theemoviedb.hexa.su';
|
||||
|
||||
function getRandomHex(size = 32) {
|
||||
const array = new Uint8Array(size);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const key = getRandomHex(32);
|
||||
|
||||
const headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
|
||||
"Referer": "https://hexa.su/",
|
||||
"Accept": "text/plain",
|
||||
"X-Fingerprint-Lite": "e9136c41504646444",
|
||||
"X-Api-Key": key
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
|
||||
Referer: `${BASE_URL}/`,
|
||||
Accept: 'text/plain',
|
||||
'X-Fingerprint-Lite': 'e9136c41504646444',
|
||||
'X-Api-Key': key,
|
||||
};
|
||||
|
||||
const encHexaRes = await fetchJson(`${API}/enc-hexa`);
|
||||
const encHexaRes = await fetchJson(`${API_BASE}/enc-hexa`);
|
||||
if (!encHexaRes || encHexaRes.status !== 200) return null;
|
||||
headers["X-Cap-Token"] = encHexaRes.result.token;
|
||||
headers['X-Cap-Token'] = encHexaRes.result.token;
|
||||
|
||||
const url = isTv
|
||||
? `https://theemoviedb.hexa.su/api/tmdb/tv/${id}/season/${s}/episode/${e}/images`
|
||||
: `https://theemoviedb.hexa.su/api/tmdb/movie/${id}/images`;
|
||||
? `${TMDB_PROXY_BASE}/api/tmdb/tv/${id}/season/${s}/episode/${e}/images`
|
||||
: `${TMDB_PROXY_BASE}/api/tmdb/movie/${id}/images`;
|
||||
|
||||
const encrypted = await fetchText(url, { headers });
|
||||
if (!encrypted) return null;
|
||||
|
||||
const decRes = await fetchJson(`${API}/dec-hexa`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ text: encrypted, key })
|
||||
const decRes = await fetchJson(`${API_BASE}/dec-hexa`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: encrypted, key }),
|
||||
});
|
||||
|
||||
if (!decRes || decRes.status !== 200 || !decRes.result) return null;
|
||||
|
||||
const data = decRes.result;
|
||||
const streamUrl = data.url || data.file || (Array.isArray(data.sources) ? data.sources[0]?.url : null);
|
||||
|
||||
if (!streamUrl) return null;
|
||||
|
||||
const streamHeaders = {
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
};
|
||||
|
||||
const allUrls = [{
|
||||
url: streamUrl,
|
||||
server: 'Hexa',
|
||||
quality: 'Auto',
|
||||
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: streamHeaders,
|
||||
}];
|
||||
|
||||
return {
|
||||
url: streamUrl,
|
||||
headers: {
|
||||
Referer: "https://hexa.su/",
|
||||
Origin: "https://hexa.su"
|
||||
}
|
||||
headers: streamHeaders,
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
@@ -1,13 +1,18 @@
|
||||
import { fetchJson } from '../utils/helpers.js';
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
const BASE_URL = 'https://hyper.stravo.site';
|
||||
const DEFAULT_ORIGIN = 'https://mzfi.me';
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
|
||||
const path = isTv
|
||||
? `/local/stream/series/tmdb:${id}:${s}:${e}.json`
|
||||
: `/local/stream/movie/tmdb:${id}.json`;
|
||||
|
||||
const url = `https://hyper.stravo.site${path}`;
|
||||
const res = await fetchJson(url);
|
||||
const url = `${BASE_URL}${path}`;
|
||||
const res = await fetchJson(url).catch(() => null);
|
||||
if (!res || !Array.isArray(res.streams)) return null;
|
||||
|
||||
const validStreams = res.streams.filter(item => {
|
||||
@@ -21,16 +26,18 @@ export async function getStream({ id, s, e }) {
|
||||
const allUrls = validStreams.map(item => ({
|
||||
url: item.url,
|
||||
server: `Hyper - ${item.title?.replace(/[^\x20-\x7E]+/g, '').trim() || 'Server'}`,
|
||||
quality: 'Auto',
|
||||
type: item.url.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: item.behaviorHints?.proxyHeaders?.request || {
|
||||
Referer: 'https://mzfi.me/',
|
||||
Accept: '*/*'
|
||||
Referer: `${DEFAULT_ORIGIN}/`,
|
||||
Accept: '*/*',
|
||||
},
|
||||
skipProxy: item.behaviorHints?.notWebReady === false
|
||||
skipProxy: item.behaviorHints?.notWebReady === false,
|
||||
}));
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
||||
import { fetchJson, getTmdbMetadata, resolveTmdbKey, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const BASE = "https://kaa.lt";
|
||||
const HLS_BASE = "https://hls.krussdomi.com/manifest";
|
||||
const HEADERS = { "User-Agent": USER_AGENT, Accept: "application/json" };
|
||||
const BASE_URL = 'https://kaa.lt';
|
||||
const HLS_BASE = 'https://hls.krussdomi.com/manifest';
|
||||
const HEADERS = { 'User-Agent': USER_AGENT, Accept: 'application/json' };
|
||||
|
||||
async function kaaSearch(query) {
|
||||
const res = await fetchJson(`${BASE}/api/fsearch`, {
|
||||
method: "POST",
|
||||
headers: { ...HEADERS, "Content-Type": "application/json" },
|
||||
const res = await fetchJson(`${BASE_URL}/api/fsearch`, {
|
||||
method: 'POST',
|
||||
headers: { ...HEADERS, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ page: 1, query }),
|
||||
}).catch(() => null);
|
||||
return Array.isArray(res?.result) ? res.result : [];
|
||||
}
|
||||
|
||||
async function kaaShowInfo(showSlug) {
|
||||
return fetchJson(`${BASE}/api/show/${showSlug}`, { headers: HEADERS }).catch(() => null);
|
||||
return fetchJson(`${BASE_URL}/api/show/${showSlug}`, { headers: HEADERS }).catch(() => null);
|
||||
}
|
||||
|
||||
async function kaaEpisodePage(showSlug, ep) {
|
||||
return fetchJson(`${BASE}/api/show/${showSlug}/episodes?ep=${ep}&lang=ja-JP`, { headers: HEADERS }).catch(() => null);
|
||||
return fetchJson(`${BASE_URL}/api/show/${showSlug}/episodes?ep=${ep}&lang=ja-JP`, { headers: HEADERS }).catch(() => null);
|
||||
}
|
||||
|
||||
async function kaaAllEpisodes(showSlug) {
|
||||
@@ -29,7 +29,7 @@ async function kaaAllEpisodes(showSlug) {
|
||||
|
||||
if (pages.length > 1) {
|
||||
const rest = await Promise.all(
|
||||
pages.slice(1).map(async (pg) => {
|
||||
pages.slice(1).map(async pg => {
|
||||
const startEp = pg.eps?.[0];
|
||||
if (!startEp) return [];
|
||||
const d = await kaaEpisodePage(showSlug, startEp);
|
||||
@@ -43,35 +43,18 @@ async function kaaAllEpisodes(showSlug) {
|
||||
}
|
||||
|
||||
async function kaaEpisodeServers(showSlug, fullEpSlug) {
|
||||
return fetchJson(`${BASE}/api/show/${showSlug}/episode/${fullEpSlug}`, { headers: HEADERS }).catch(() => null);
|
||||
return fetchJson(`${BASE_URL}/api/show/${showSlug}/episode/${fullEpSlug}`, { headers: HEADERS }).catch(() => null);
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e, tmdbApiKey, sdk }) {
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, sdk } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const apiKey = resolveTmdbKey(args, sdk);
|
||||
const epNum = e ? Number(e) : 1;
|
||||
|
||||
try {
|
||||
const apiKey = tmdbApiKey || sdk?.tmdbApiKey || process.env.TMDB_API_KEY || '45dbdd5e714da212424bda0336167447';
|
||||
const isTv = !!s;
|
||||
const mediaType = isTv ? 'tv' : 'movie';
|
||||
const epNum = e ? Number(e) : 1;
|
||||
|
||||
let titles = [];
|
||||
try {
|
||||
const tmdbRes = await fetchJson(`https://api.themoviedb.org/3/${mediaType}/${id}?api_key=${apiKey}&append_to_response=alternative_titles`);
|
||||
if (tmdbRes) {
|
||||
const primary = tmdbRes.title || tmdbRes.name;
|
||||
const original = tmdbRes.original_title || tmdbRes.original_name;
|
||||
if (primary) titles.push(primary);
|
||||
if (original && !titles.includes(original)) titles.push(original);
|
||||
|
||||
const alts = tmdbRes.alternative_titles?.titles || tmdbRes.alternative_titles?.results || [];
|
||||
for (const alt of alts) {
|
||||
const candidate = alt.title || alt.name;
|
||||
if (candidate && !titles.includes(candidate)) {
|
||||
titles.push(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
|
||||
const meta = await getTmdbMetadata(id, isTv, apiKey);
|
||||
const titles = meta?.titles || [];
|
||||
if (!titles.length) return null;
|
||||
|
||||
let showSlug = null;
|
||||
@@ -88,12 +71,12 @@ export async function getStream({ id, s, e, tmdbApiKey, sdk }) {
|
||||
const showInfo = await kaaShowInfo(showSlug);
|
||||
let fullEpSlug = null;
|
||||
|
||||
if (showInfo?.type === "movie") {
|
||||
const m = (showInfo.watch_uri || "").match(/\/(ep-(\d+)-([a-f0-9]+))$/i);
|
||||
if (showInfo?.type === 'movie') {
|
||||
const m = (showInfo.watch_uri || '').match(/\/(ep-(\d+)-([a-f0-9]+))$/i);
|
||||
if (m) fullEpSlug = m[1];
|
||||
} else {
|
||||
const episodes = await kaaAllEpisodes(showSlug);
|
||||
const epItem = episodes.find((item) => item.episode_number === epNum);
|
||||
const epItem = episodes.find(item => item.episode_number === epNum);
|
||||
if (epItem) fullEpSlug = `ep-${epItem.episode_number}-${epItem.slug}`;
|
||||
}
|
||||
|
||||
@@ -102,24 +85,34 @@ export async function getStream({ id, s, e, tmdbApiKey, sdk }) {
|
||||
const episodeData = await kaaEpisodeServers(showSlug, fullEpSlug);
|
||||
const servers = Array.isArray(episodeData?.servers) ? episodeData.servers : [];
|
||||
|
||||
const urls = [];
|
||||
const allUrls = [];
|
||||
for (const srv of servers) {
|
||||
if (!srv.src) continue;
|
||||
const m = srv.src.match(/[?&]id=([^&]+)/);
|
||||
if (m) {
|
||||
urls.push({
|
||||
allUrls.push({
|
||||
url: `${HLS_BASE}/${m[1]}/master.m3u8`,
|
||||
server: `KickAssAnime (${srv.name || 'Server'})`,
|
||||
quality: 'Auto',
|
||||
type: 'hls',
|
||||
headers: HEADERS,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return urls.length ? { allUrls: urls } : null;
|
||||
} catch (err) {
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSources() {
|
||||
return ['KickAssAnime'];
|
||||
export async function getSources(args) {
|
||||
const stream = await getStream(args);
|
||||
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : ['KickAssAnime'];
|
||||
}
|
||||
@@ -1,60 +1,115 @@
|
||||
import { fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
|
||||
|
||||
export const SKIP_VERIFY = true;
|
||||
export const MULTI_URL = false;
|
||||
|
||||
const ENC_API = "https://enc-dec.app/api";
|
||||
const BASE = "https://kisskh.do";
|
||||
const HEADERS = { 'User-Agent': USER_AGENT, Accept: 'application/json' };
|
||||
|
||||
function cleanTitle(t) { return t ? t.toLowerCase().replace(/\(\d{4}\)/g, '').replace(/[^a-z0-9]/g, '') : ''; }
|
||||
|
||||
async function encKey(episodeId, type) {
|
||||
try {
|
||||
const data = await fetchJson(`${ENC_API}/enc-kisskh?text=${episodeId}&type=${type}`, { signal: AbortSignal.timeout(6000) });
|
||||
return data?.status === 200 ? data.result : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
try {
|
||||
const info = await getTmdbInfo(id, s ? 'tv' : 'movie', s);
|
||||
if (!info) return null;
|
||||
let drama = null, bestScore = -1;
|
||||
for (const title of (info.titles.length > 1 ? [...info.titles].reverse() : info.titles)) {
|
||||
const results = await fetchJson(`${BASE}/api/DramaList/Search?q=${encodeURIComponent(title)}`, { headers: HEADERS, signal: AbortSignal.timeout(6000) }).catch(() => []);
|
||||
const targetClean = cleanTitle(title);
|
||||
for (const item of results) {
|
||||
const parts = (item.title || '').split(/\s*-\s*/).map(cleanTitle);
|
||||
let score = 0;
|
||||
if (parts.some(p => p === targetClean)) score += 5;
|
||||
else if (parts.some(p => p.includes(targetClean) || targetClean.includes(p))) score += 3;
|
||||
else continue;
|
||||
if (score > bestScore) { bestScore = score; drama = item; }
|
||||
}
|
||||
if (drama && bestScore >= 5) break;
|
||||
}
|
||||
if (!drama) return null;
|
||||
const detail = await fetchJson(`${BASE}/api/DramaList/Drama/${drama.id}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
|
||||
const episodeId = detail?.episodes?.find(ep => Math.floor(ep.number) === Number(e || 1))?.id;
|
||||
if (!episodeId) return null;
|
||||
const vidKey = await encKey(episodeId, 'vid');
|
||||
if (!vidKey) return null;
|
||||
const videoData = await fetchJson(`${BASE}/api/DramaList/Episode/${episodeId}.png?err=false&ts=&time=&kkey=${vidKey}`, { headers: HEADERS, signal: AbortSignal.timeout(10000) });
|
||||
if (!videoData?.Video) return null;
|
||||
let subtitles = [];
|
||||
const subKey = await encKey(episodeId, 'sub');
|
||||
if (subKey) {
|
||||
try {
|
||||
const subList = await fetchJson(`${BASE}/api/Sub/${episodeId}?kkey=${subKey}`, { headers: HEADERS, signal: AbortSignal.timeout(8000) });
|
||||
if (Array.isArray(subList)) subtitles = subList.map(sub => ({ url: sub.src, label: sub.label, lang: sub.land, default: !!sub.default }));
|
||||
} catch { }
|
||||
}
|
||||
return { allUrls: [{ url: videoData.Video, type: videoData.Video.includes('.m3u8') ? 'hls' : 'mp4', audio: 'sub', server: 'KissKH', skipProxy: false, subtitles }] };
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
export async function getSources(args) {
|
||||
const stream = await getStream(args);
|
||||
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
|
||||
import { cleanTitle, fetchJson, getTmdbMetadata, resolveTmdbKey, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const ENC_API = 'https://enc-dec.app/api';
|
||||
const BASE_URL = 'https://kisskh.do';
|
||||
|
||||
const HEADERS = { 'User-Agent': USER_AGENT, Accept: 'application/json' };
|
||||
|
||||
async function encKey(episodeId, type) {
|
||||
try {
|
||||
const data = await fetchJson(`${ENC_API}/enc-kisskh?text=${episodeId}&type=${type}`, { signal: AbortSignal.timeout(6000) });
|
||||
return data?.status === 200 ? data.result : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, sdk } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const apiKey = resolveTmdbKey(args, sdk);
|
||||
|
||||
try {
|
||||
const meta = await getTmdbMetadata(id, isTv, apiKey);
|
||||
if (!meta?.titles?.length) return null;
|
||||
|
||||
let drama = null;
|
||||
let bestScore = -1;
|
||||
const searchTitles = meta.titles.length > 1 ? [...meta.titles].reverse() : meta.titles;
|
||||
|
||||
for (const title of searchTitles) {
|
||||
const results = await fetchJson(`${BASE_URL}/api/DramaList/Search?q=${encodeURIComponent(title)}`, {
|
||||
headers: HEADERS,
|
||||
signal: AbortSignal.timeout(6000),
|
||||
}).catch(() => []);
|
||||
|
||||
const targetClean = cleanTitle(title);
|
||||
for (const item of results) {
|
||||
const parts = (item.title || '').split(/\s*-\s*/).map(cleanTitle);
|
||||
let score = 0;
|
||||
if (parts.some(p => p === targetClean)) score += 5;
|
||||
else if (parts.some(p => p.includes(targetClean) || targetClean.includes(p))) score += 3;
|
||||
else continue;
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
drama = item;
|
||||
}
|
||||
}
|
||||
if (drama && bestScore >= 5) break;
|
||||
}
|
||||
|
||||
if (!drama) return null;
|
||||
|
||||
const detail = await fetchJson(`${BASE_URL}/api/DramaList/Drama/${drama.id}`, {
|
||||
headers: HEADERS,
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
|
||||
const episodeId = detail?.episodes?.find(ep => Math.floor(ep.number) === Number(e || 1))?.id;
|
||||
if (!episodeId) return null;
|
||||
|
||||
const vidKey = await encKey(episodeId, 'vid');
|
||||
if (!vidKey) return null;
|
||||
|
||||
const videoData = await fetchJson(`${BASE_URL}/api/DramaList/Episode/${episodeId}.png?err=false&ts=&time=&kkey=${vidKey}`, {
|
||||
headers: HEADERS,
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!videoData?.Video) return null;
|
||||
|
||||
let subtitles = [];
|
||||
const subKey = await encKey(episodeId, 'sub');
|
||||
if (subKey) {
|
||||
try {
|
||||
const subList = await fetchJson(`${BASE_URL}/api/Sub/${episodeId}?kkey=${subKey}`, {
|
||||
headers: HEADERS,
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
if (Array.isArray(subList)) {
|
||||
subtitles = subList.map(sub => ({
|
||||
url: sub.src,
|
||||
label: sub.label,
|
||||
lang: sub.land,
|
||||
default: Boolean(sub.default),
|
||||
}));
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
const streamUrl = videoData.Video;
|
||||
const allUrls = [{
|
||||
url: streamUrl,
|
||||
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
audio: 'sub',
|
||||
server: 'KissKH',
|
||||
quality: 'Auto',
|
||||
headers: HEADERS,
|
||||
skipProxy: false,
|
||||
subtitles,
|
||||
}];
|
||||
|
||||
return {
|
||||
url: streamUrl,
|
||||
headers: HEADERS,
|
||||
allUrls,
|
||||
subtitles,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSources(args) {
|
||||
const stream = await getStream(args);
|
||||
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : ['KissKH'];
|
||||
}
|
||||
@@ -1,25 +1,25 @@
|
||||
import { getTmdbInfo } from '../utils/helpers.js';
|
||||
import { fetchJson, getTmdbMetadata, resolveTmdbKey, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = 'https://lmscript.xyz';
|
||||
const REFERER = 'https://lookmovie2.to/';
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, tmdbApiKey, sdk } = args;
|
||||
const type = (s != null && e != null) ? 'tv' : 'movie';
|
||||
const apiKey = tmdbApiKey || sdk?.tmdbApiKey || process.env.TMDB_API_KEY || '45dbdd5e714da212424bda0336167447';
|
||||
const { id, s, e, sdk } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
if (isTv) return null;
|
||||
|
||||
const apiKey = resolveTmdbKey(args, sdk);
|
||||
|
||||
try {
|
||||
if (type === 'tv') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tmdbInfo = await getTmdbInfo(apiKey, id, type, s);
|
||||
const titles = tmdbInfo?.titles || [];
|
||||
const meta = await getTmdbMetadata(id, isTv, apiKey);
|
||||
const titles = meta?.titles || [];
|
||||
if (!titles.length) return null;
|
||||
|
||||
let movie = null;
|
||||
for (const title of titles) {
|
||||
const url = `https://lmscript.xyz/v1/movies?filters[q]=${encodeURIComponent(title)}&expand=streams`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) continue;
|
||||
const data = await response.json();
|
||||
const url = `${BASE_URL}/v1/movies?filters[q]=${encodeURIComponent(title)}&expand=streams`;
|
||||
const data = await fetchJson(url).catch(() => null);
|
||||
if (!data) continue;
|
||||
|
||||
const found = data.items?.find(item => {
|
||||
const tmdbPrefixMatch = `${item.tmdb_prefix}` === `${id}`;
|
||||
@@ -37,33 +37,35 @@ export async function getStream(args) {
|
||||
if (!movie) return null;
|
||||
|
||||
const streams = movie.streams || {};
|
||||
const SOURCE_ORDER = ["1080p", "720p", "480p", "360p"];
|
||||
const sourceOrder = ['1080p', '720p', '480p', '360p'];
|
||||
const sortedStreams = Object.keys(streams).sort((a, b) => {
|
||||
const indexA = SOURCE_ORDER.indexOf(a);
|
||||
const indexB = SOURCE_ORDER.indexOf(b);
|
||||
return (indexA === -1 ? 99 : indexA) - (indexB === -1 ? 99 : indexB);
|
||||
const idxA = sourceOrder.indexOf(a);
|
||||
const idxB = sourceOrder.indexOf(b);
|
||||
if (idxA !== -1 && idxB !== -1) return idxA - idxB;
|
||||
if (idxA !== -1) return -1;
|
||||
if (idxB !== -1) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (!sortedStreams.length) return null;
|
||||
const headers = {
|
||||
Referer: REFERER,
|
||||
'User-Agent': USER_AGENT,
|
||||
};
|
||||
|
||||
const allUrls = sortedStreams.map(quality => {
|
||||
const streamUrl = streams[quality];
|
||||
return {
|
||||
url: streamUrl,
|
||||
quality,
|
||||
server: `LMScript - ${quality}`,
|
||||
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: {
|
||||
Referer: 'https://lookmovie2.to/',
|
||||
Origin: 'https://lookmovie2.to'
|
||||
}
|
||||
};
|
||||
});
|
||||
const allUrls = sortedStreams.map(res => ({
|
||||
url: streams[res],
|
||||
quality: res,
|
||||
server: `LookMovie - ${res}`,
|
||||
type: streams[res].includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers,
|
||||
}));
|
||||
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls
|
||||
allUrls,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
|
||||
@@ -1,80 +1,81 @@
|
||||
import { USER_AGENT, fetchText, fetchJson } from '../utils/helpers.js';
|
||||
|
||||
const TMDB_BASE = 'https://api.themoviedb.org/3';
|
||||
const LM_DOMAINS = ['https://www.lookmovie2.to', 'https://lookmovie2.to', 'https://lookmovie.foundation'];
|
||||
const HEADERS_BASE = { 'User-Agent': USER_AGENT, 'Accept-Language': 'en-US,en;q=0.9' };
|
||||
|
||||
async function searchLookMovie(type, title, year, clientIP) {
|
||||
for (const base of LM_DOMAINS) {
|
||||
try {
|
||||
const headers = { ...HEADERS_BASE, 'Accept': 'application/json', 'Referer': `${base}/`, 'X-Requested-With': 'XMLHttpRequest', ...(clientIP && { 'X-Forwarded-For': clientIP }) };
|
||||
const data = await fetchJson(`${base}/api/v1/${type}/do-search/?q=${encodeURIComponent(title)}`, { headers, signal: AbortSignal.timeout(4000) });
|
||||
const results = data?.result;
|
||||
if (results?.length) {
|
||||
const match = results.find(r => String(r.year) === String(year)) ?? results.find(r => r.title?.toLowerCase() === title.toLowerCase()) ?? results[0];
|
||||
if (match) return { match, base };
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const STORAGE_RE = /window\[['"](?:movie|show)_storage['"]\]\s*=\s*\{([^}]+)\}/s;
|
||||
const ID_MOVIE_RE = /['"]?(?:id_movie|movieId)['"]?\s*[:=]\s*['"]?(\d+)['"]?/i;
|
||||
const SEASONS_RE = /seasons\s*:\s*(\[[\s\S]+?\])\s*[,}]/;
|
||||
|
||||
function getEpisodeIdFast(html, s, e) {
|
||||
const sMatch = html.match(STORAGE_RE);
|
||||
if (sMatch) {
|
||||
const block = sMatch[1], sm = block.match(SEASONS_RE);
|
||||
if (sm) {
|
||||
try {
|
||||
const seasons = JSON.parse(sm[1]);
|
||||
const season = seasons.find(x => String(x?.season ?? x?.meta?.season) === String(s));
|
||||
if (season) {
|
||||
const ep = Array.isArray(season.episodes) ? season.episodes.find(x => String(x.episode) === String(e)) : (season.episodes?.[String(e)] || Object.values(season.episodes || {}).find(x => String(x.episode) === String(e)));
|
||||
if (ep) return String(ep.id_episode ?? ep.id);
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
const parts = html.split(/id_episode['"]?\s*[:=]\s*['"]?(\d+)['"]?/i);
|
||||
for (let i = 1; i < parts.length; i += 2) {
|
||||
const context = parts[i - 1].slice(-300) + parts[i + 1].slice(0, 300);
|
||||
if (context.match(new RegExp(`['"]?episode['"]?\\s*[:=]\\s*['"]?${e}['"]?`, 'i')) && context.match(new RegExp(`['"]?season['"]?\\s*[:=]\\s*['"]?${s}['"]?`, 'i'))) return parts[i];
|
||||
}
|
||||
const am = html.match(new RegExp(`data-season=["']${s}["'][^>]*?data-episode=["']${e}["'][^>]*?data-id=["'](\\d+)["']`, 'i')) || html.match(new RegExp(`data-episode=["']${e}["'][^>]*?data-season=["']${s}["'][^>]*?data-id=["'](\\d+)["']`, 'i'));
|
||||
return am ? am[1] : null;
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e, sdk, clientIP }) {
|
||||
const tmdbApiKey = sdk?.tmdbApiKey || null;
|
||||
if (!tmdbApiKey) return null;
|
||||
try {
|
||||
const tmdbKey = tmdbApiKey;
|
||||
if (!tmdbKey) return null;
|
||||
const isTV = s != null && e != null;
|
||||
const typeStr = isTV ? 'shows' : 'movies';
|
||||
const tmdbData = await fetchJson(`${TMDB_BASE}/${isTV ? 'tv' : 'movie'}/${id}?api_key=${tmdbKey}`, { signal: AbortSignal.timeout(3000) });
|
||||
const title = tmdbData?.title || tmdbData?.name;
|
||||
const year = (tmdbData?.first_air_date || tmdbData?.release_date || '').slice(0, 4);
|
||||
if (!title) return null;
|
||||
const searchRes = await searchLookMovie(typeStr, title, year, clientIP);
|
||||
if (!searchRes?.match?.slug) return null;
|
||||
const { match, base } = searchRes;
|
||||
const headers = { ...HEADERS_BASE, 'Accept': 'text/html', 'Referer': `${base}/`, ...(clientIP && { 'X-Forwarded-For': clientIP }) };
|
||||
const html = await fetchText(`${base}/${typeStr}/play/${match.slug}`, { headers, signal: AbortSignal.timeout(8000) });
|
||||
const storageMatch = html.match(STORAGE_RE);
|
||||
if (!storageMatch) return null;
|
||||
const hashMatch = storageMatch[1].match(/hash\s*:\s*['"]([^'"]+)['"]/);
|
||||
const expiresMatch = storageMatch[1].match(/expires\s*:\s*(\d+)/);
|
||||
if (!hashMatch || !expiresMatch) return null;
|
||||
const streamId = isTV ? getEpisodeIdFast(html, s, e) : (match.id_movie || match.id || html.match(ID_MOVIE_RE)?.[1]);
|
||||
if (!streamId) return null;
|
||||
const accessHeaders = { ...HEADERS_BASE, 'Accept': 'application/json', 'Referer': `${base}/`, 'X-Requested-With': 'XMLHttpRequest', ...(clientIP && { 'X-Forwarded-For': clientIP }) };
|
||||
const data = await fetchJson(`${base}/api/v1/security/${isTV ? 'episode' : 'movie'}-access?id_${isTV ? 'episode' : 'movie'}=${streamId}&hash=${hashMatch[1]}&expires=${expiresMatch[1]}`, { headers: accessHeaders, signal: AbortSignal.timeout(8000) });
|
||||
const streams = data?.streams ?? data?.result?.streams ?? data?.data?.streams ?? data;
|
||||
const allUrls = Object.entries(streams || {}).filter(([, v]) => typeof v === 'string' && v.includes('.m3u8')).map(([quality, url]) => ({ url, quality }));
|
||||
return allUrls.length ? { allUrls } : null;
|
||||
} catch { return null; }
|
||||
import { USER_AGENT, fetchText, fetchJson } from '../utils/helpers.js';
|
||||
|
||||
const TMDB_BASE = 'https://api.themoviedb.org/3';
|
||||
const LM_DOMAINS = ['https://www.lookmovie2.to', 'https://lookmovie2.to', 'https://lookmovie.foundation'];
|
||||
|
||||
const HEADERS_BASE = { 'User-Agent': USER_AGENT, 'Accept-Language': 'en-US,en;q=0.9' };
|
||||
|
||||
async function searchLookMovie(type, title, year, clientIP) {
|
||||
for (const base of LM_DOMAINS) {
|
||||
try {
|
||||
const headers = { ...HEADERS_BASE, 'Accept': 'application/json', 'Referer': `${base}/`, 'X-Requested-With': 'XMLHttpRequest', ...(clientIP && { 'X-Forwarded-For': clientIP }) };
|
||||
const data = await fetchJson(`${base}/api/v1/${type}/do-search/?q=${encodeURIComponent(title)}`, { headers, signal: AbortSignal.timeout(4000) });
|
||||
const results = data?.result;
|
||||
if (results?.length) {
|
||||
const match = results.find(r => String(r.year) === String(year)) ?? results.find(r => r.title?.toLowerCase() === title.toLowerCase()) ?? results[0];
|
||||
if (match) return { match, base };
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const STORAGE_RE = /window\[['"](?:movie|show)_storage['"]\]\s*=\s*\{([^}]+)\}/s;
|
||||
const ID_MOVIE_RE = /['"]?(?:id_movie|movieId)['"]?\s*[:=]\s*['"]?(\d+)['"]?/i;
|
||||
const SEASONS_RE = /seasons\s*:\s*(\[[\s\S]+?\])\s*[,}]/;
|
||||
|
||||
function getEpisodeIdFast(html, s, e) {
|
||||
const sMatch = html.match(STORAGE_RE);
|
||||
if (sMatch) {
|
||||
const block = sMatch[1], sm = block.match(SEASONS_RE);
|
||||
if (sm) {
|
||||
try {
|
||||
const seasons = JSON.parse(sm[1]);
|
||||
const season = seasons.find(x => String(x?.season ?? x?.meta?.season) === String(s));
|
||||
if (season) {
|
||||
const ep = Array.isArray(season.episodes) ? season.episodes.find(x => String(x.episode) === String(e)) : (season.episodes?.[String(e)] || Object.values(season.episodes || {}).find(x => String(x.episode) === String(e)));
|
||||
if (ep) return String(ep.id_episode ?? ep.id);
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
const parts = html.split(/id_episode['"]?\s*[:=]\s*['"]?(\d+)['"]?/i);
|
||||
for (let i = 1; i < parts.length; i += 2) {
|
||||
const context = parts[i - 1].slice(-300) + parts[i + 1].slice(0, 300);
|
||||
if (context.match(new RegExp(`['"]?episode['"]?\\s*[:=]\\s*['"]?${e}['"]?`, 'i')) && context.match(new RegExp(`['"]?season['"]?\\s*[:=]\\s*['"]?${s}['"]?`, 'i'))) return parts[i];
|
||||
}
|
||||
const am = html.match(new RegExp(`data-season=["']${s}["'][^>]*?data-episode=["']${e}["'][^>]*?data-id=["'](\\d+)["']`, 'i')) || html.match(new RegExp(`data-episode=["']${e}["'][^>]*?data-season=["']${s}["'][^>]*?data-id=["'](\\d+)["']`, 'i'));
|
||||
return am ? am[1] : null;
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e, sdk, clientIP }) {
|
||||
const tmdbApiKey = sdk?.tmdbApiKey || null;
|
||||
if (!tmdbApiKey) return null;
|
||||
try {
|
||||
const tmdbKey = tmdbApiKey;
|
||||
if (!tmdbKey) return null;
|
||||
const isTV = s != null && e != null;
|
||||
const typeStr = isTV ? 'shows' : 'movies';
|
||||
const tmdbData = await fetchJson(`${TMDB_BASE}/${isTV ? 'tv' : 'movie'}/${id}?api_key=${tmdbKey}`, { signal: AbortSignal.timeout(3000) });
|
||||
const title = tmdbData?.title || tmdbData?.name;
|
||||
const year = (tmdbData?.first_air_date || tmdbData?.release_date || '').slice(0, 4);
|
||||
if (!title) return null;
|
||||
const searchRes = await searchLookMovie(typeStr, title, year, clientIP);
|
||||
if (!searchRes?.match?.slug) return null;
|
||||
const { match, base } = searchRes;
|
||||
const headers = { ...HEADERS_BASE, 'Accept': 'text/html', 'Referer': `${base}/`, ...(clientIP && { 'X-Forwarded-For': clientIP }) };
|
||||
const html = await fetchText(`${base}/${typeStr}/play/${match.slug}`, { headers, signal: AbortSignal.timeout(8000) });
|
||||
const storageMatch = html.match(STORAGE_RE);
|
||||
if (!storageMatch) return null;
|
||||
const hashMatch = storageMatch[1].match(/hash\s*:\s*['"]([^'"]+)['"]/);
|
||||
const expiresMatch = storageMatch[1].match(/expires\s*:\s*(\d+)/);
|
||||
if (!hashMatch || !expiresMatch) return null;
|
||||
const streamId = isTV ? getEpisodeIdFast(html, s, e) : (match.id_movie || match.id || html.match(ID_MOVIE_RE)?.[1]);
|
||||
if (!streamId) return null;
|
||||
const accessHeaders = { ...HEADERS_BASE, 'Accept': 'application/json', 'Referer': `${base}/`, 'X-Requested-With': 'XMLHttpRequest', ...(clientIP && { 'X-Forwarded-For': clientIP }) };
|
||||
const data = await fetchJson(`${base}/api/v1/security/${isTV ? 'episode' : 'movie'}-access?id_${isTV ? 'episode' : 'movie'}=${streamId}&hash=${hashMatch[1]}&expires=${expiresMatch[1]}`, { headers: accessHeaders, signal: AbortSignal.timeout(8000) });
|
||||
const streams = data?.streams ?? data?.result?.streams ?? data?.data?.streams ?? data;
|
||||
const allUrls = Object.entries(streams || {}).filter(([, v]) => typeof v === 'string' && v.includes('.m3u8')).map(([quality, url]) => ({ url, quality }));
|
||||
return allUrls.length ? { allUrls } : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
@@ -1,125 +1,75 @@
|
||||
import { getUA } from "../utils/helpers.js";
|
||||
import { getTmdbMetadata, getUA, resolveTmdbKey } from '../utils/helpers.js';
|
||||
|
||||
const API_BASE = "https://api.movenerds.space/api";
|
||||
|
||||
function getHeaders() {
|
||||
return {
|
||||
"User-Agent": getUA(),
|
||||
Accept: "application/json",
|
||||
Referer: "https://movenerds.space/",
|
||||
Origin: "https://movenerds.space",
|
||||
};
|
||||
}
|
||||
|
||||
function resolveApiKey(args) {
|
||||
return (
|
||||
args?.tmdbApiKey ||
|
||||
args?.sdk?.tmdbApiKey ||
|
||||
process.env.TMDB_API_KEY ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
async function getMeta(id, isTv, apiKey) {
|
||||
if (!apiKey) return null;
|
||||
const type = isTv ? "tv" : "movie";
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`,
|
||||
{ signal: AbortSignal.timeout(5000) }
|
||||
);
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
const title = isTv ? data.name : data.title;
|
||||
const date = isTv ? data.first_air_date : data.release_date;
|
||||
const year = date ? parseInt(date.slice(0, 4), 10) : null;
|
||||
return { title, year };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const BASE_URL = 'https://movenerds.space';
|
||||
const API_BASE = 'https://api.movenerds.space/api';
|
||||
|
||||
function mapSource(source) {
|
||||
if (!source || !source.url) return null;
|
||||
if (source.type !== "hls" && source.type !== "mp4") return null;
|
||||
if (source.type !== 'hls' && source.type !== 'mp4') return null;
|
||||
if (!/^https?:\/\//.test(source.url)) return null;
|
||||
|
||||
return {
|
||||
url: source.url,
|
||||
headers: {
|
||||
"User-Agent": source.headers?.["User-Agent"] || getUA(),
|
||||
Referer: source.headers?.Referer || "https://movenerds.space/",
|
||||
Origin: source.headers?.Origin || "https://movenerds.space",
|
||||
'User-Agent': source.headers?.['User-Agent'] || getUA(),
|
||||
Referer: source.headers?.Referer || `${BASE_URL}/`,
|
||||
Origin: source.headers?.Origin || BASE_URL,
|
||||
},
|
||||
server: source.name || source.providerName || "MoverNerds",
|
||||
quality: source.quality || "1080p",
|
||||
server: source.name || source.providerName || 'MoverNerds',
|
||||
quality: source.quality || '1080p',
|
||||
type: source.type,
|
||||
skipProxy: true,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s = null, e = null } = args;
|
||||
const { id, s, e, sdk } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const apiKey = resolveTmdbKey(args, sdk);
|
||||
|
||||
const apiKey = resolveApiKey(args);
|
||||
const meta = await getMeta(id, isTv, apiKey);
|
||||
|
||||
if (!meta?.title) {
|
||||
if (process.env.DEBUG_CAUSE) {
|
||||
console.error(
|
||||
`[movenerds] Could not resolve title for ${isTv ? "tv" : "movie"} ${id} ` +
|
||||
`(tmdbApiKey ${apiKey ? "present" : "MISSING"})`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const meta = await getTmdbMetadata(id, isTv, apiKey);
|
||||
if (!meta?.title) return null;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
tmdbId: String(id),
|
||||
type: isTv ? "tv" : "movie",
|
||||
type: isTv ? 'tv' : 'movie',
|
||||
title: meta.title,
|
||||
});
|
||||
if (meta.year) params.set("year", String(meta.year));
|
||||
if (meta.year) params.set('year', String(meta.year));
|
||||
if (isTv) {
|
||||
params.set("season", String(s));
|
||||
params.set("episode", String(e));
|
||||
params.set("language", "en");
|
||||
params.set('season', String(s));
|
||||
params.set('episode', String(e));
|
||||
params.set('language', 'en');
|
||||
}
|
||||
|
||||
const url = `${API_BASE}/stream?${params.toString()}`;
|
||||
const headers = {
|
||||
'User-Agent': getUA(),
|
||||
Accept: 'application/json',
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: getHeaders(),
|
||||
headers,
|
||||
signal: AbortSignal.timeout(20000),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
if (process.env.DEBUG_CAUSE) {
|
||||
console.error(`[movenerds] HTTP ${res.status} for ${url}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (!res.ok) return null;
|
||||
|
||||
const data = await res.json();
|
||||
const primary = Array.isArray(data?.primarySources) ? data.primarySources : [];
|
||||
if (!primary.length) {
|
||||
if (process.env.DEBUG_CAUSE) {
|
||||
console.error(`[movenerds] No primarySources in response for ${url}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (!primary.length) return null;
|
||||
|
||||
const allUrls = primary.map(mapSource).filter(Boolean);
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return { allUrls };
|
||||
} catch (err) {
|
||||
if (process.env.DEBUG_CAUSE) {
|
||||
console.error(`[movenerds] fetch error for ${url}:`, err.message);
|
||||
if (err.cause) console.error(" cause:", err.cause.code, err.cause.message);
|
||||
}
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { fetchJson, fetchText, getTmdbInfo } from '../utils/helpers.js';
|
||||
import { fetchJson, fetchText, getTmdbMetadata, resolveTmdbKey } from '../utils/helpers.js';
|
||||
|
||||
const K = [
|
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
|
||||
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
||||
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174
|
||||
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
];
|
||||
const MAGIC = [109, 118, 109, 49];
|
||||
|
||||
@@ -62,7 +62,7 @@ function initCipherState(keyStr, seedStr) {
|
||||
}
|
||||
return {
|
||||
S,
|
||||
acc: scramble(0xa5a5a5a5 ^ n) >>> 0
|
||||
acc: scramble(0xa5a5a5a5 ^ n) >>> 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ function nextKeystreamWord(state, step) {
|
||||
}
|
||||
|
||||
function decodeBase64Url(str) {
|
||||
const padded = str.replace(/-/g, "+").replace(/_/g, "/").padEnd(4 * Math.ceil(str.length / 4), "=");
|
||||
const padded = str.replace(/-/g, '+').replace(/_/g, '/').padEnd(4 * Math.ceil(str.length / 4), '=');
|
||||
const bin = atob(padded);
|
||||
const bytes = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
||||
@@ -111,45 +111,31 @@ function decryptPayload(ciphertext, seed, mediaId) {
|
||||
|
||||
for (let i = 0; i < MAGIC.length; i++) {
|
||||
if (rawBytes[i] !== MAGIC[i]) {
|
||||
throw new Error("decrypt failed: bad seed or tampered payload");
|
||||
throw new Error('decrypt failed: bad seed or tampered payload');
|
||||
}
|
||||
}
|
||||
|
||||
const payload = rawBytes.subarray(MAGIC.length);
|
||||
return new TextDecoder("utf-8").decode(payload);
|
||||
return new TextDecoder('utf-8').decode(payload);
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e, tmdbApiKey, sdk }) {
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, sdk } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const type = isTv ? 'tv' : 'movie';
|
||||
const apiKey = tmdbApiKey || sdk?.tmdbApiKey || process.env.TMDB_API_KEY || '45dbdd5e714da212424bda0336167447';
|
||||
const apiKey = resolveTmdbKey(args, sdk);
|
||||
|
||||
const headers = {
|
||||
'Accept': '*/*',
|
||||
'Origin': 'https://www.movy.sx',
|
||||
'Referer': 'https://www.movy.sx/',
|
||||
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1'
|
||||
Accept: '*/*',
|
||||
Origin: 'https://www.movy.sx',
|
||||
Referer: 'https://www.movy.sx/',
|
||||
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Mobile/15E148 Safari/604.1',
|
||||
};
|
||||
|
||||
let title = '';
|
||||
let year = '';
|
||||
let imdbId = '';
|
||||
|
||||
try {
|
||||
const info = await getTmdbInfo(apiKey, id, type, s);
|
||||
title = info?.titles?.[0] || '';
|
||||
year = info?.year || '';
|
||||
imdbId = info?.imdbId || '';
|
||||
} catch { }
|
||||
|
||||
if (!title) {
|
||||
try {
|
||||
const tmdbRes = await fetchJson(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&append_to_response=external_ids`);
|
||||
title = tmdbRes?.title || tmdbRes?.name || '';
|
||||
year = (tmdbRes?.release_date || tmdbRes?.first_air_date || '').split('-')[0] || '';
|
||||
imdbId = tmdbRes?.imdb_id || tmdbRes?.external_ids?.imdb_id || '';
|
||||
} catch { }
|
||||
}
|
||||
const meta = await getTmdbMetadata(id, isTv, apiKey);
|
||||
const title = meta?.title || '';
|
||||
const year = meta?.year ? String(meta.year) : '';
|
||||
const imdbId = meta?.imdbId || '';
|
||||
|
||||
if (!title) return null;
|
||||
|
||||
@@ -170,7 +156,7 @@ export async function getStream({ id, s, e, tmdbApiKey, sdk }) {
|
||||
tmdbId: String(id),
|
||||
imdbId: String(imdbId || ''),
|
||||
enc: '2',
|
||||
seed
|
||||
seed,
|
||||
});
|
||||
|
||||
const url = `https://api.wecollege.net/${srv}/sources?${params.toString()}`;
|
||||
@@ -187,10 +173,12 @@ export async function getStream({ id, s, e, tmdbApiKey, sdk }) {
|
||||
allUrls.push({
|
||||
url: src.url,
|
||||
server: `Movy - ${srv.toUpperCase()} (${src.quality || 'Auto'})`,
|
||||
quality: src.quality || 'Auto',
|
||||
type: src.url.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: {
|
||||
Referer: 'https://www.movy.sx/',
|
||||
Origin: 'https://www.movy.sx'
|
||||
}
|
||||
Origin: 'https://www.movy.sx',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -205,6 +193,6 @@ export async function getStream({ id, s, e, tmdbApiKey, sdk }) {
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
@@ -1,16 +1,20 @@
|
||||
import { fetchJson } from '../utils/helpers.js';
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
const BASE_URL = 'https://net79.cc';
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
|
||||
const url = isTv
|
||||
? `https://net79.cc/api/embed-tmdb/${id}?type=tv&se=${s}&ep=${e}`
|
||||
: `https://net79.cc/api/embed-tmdb/${id}?type=movie`;
|
||||
? `${BASE_URL}/api/embed-tmdb/${id}?type=tv&se=${s}&ep=${e}`
|
||||
: `${BASE_URL}/api/embed-tmdb/${id}?type=movie`;
|
||||
|
||||
const headers = {
|
||||
'Accept': 'application/json',
|
||||
'Referer': 'https://net79.cc/',
|
||||
'Origin': 'https://net79.cc',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'
|
||||
Accept: 'application/json',
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
|
||||
};
|
||||
|
||||
const res = await fetchJson(url, { headers }).catch(() => null);
|
||||
@@ -27,9 +31,9 @@ export async function getStream({ id, s, e }) {
|
||||
server: `Net79 - ${stream.resolution ? `${stream.resolution}p` : 'MP4'}`,
|
||||
type: stream.url.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: {
|
||||
Referer: 'https://net79.cc/',
|
||||
Origin: 'https://net79.cc'
|
||||
}
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -40,9 +44,9 @@ export async function getStream({ id, s, e }) {
|
||||
server: `Net79 - ${res.resolution ? `${res.resolution}p` : 'MP4'}`,
|
||||
type: res.mp4.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: {
|
||||
Referer: 'https://net79.cc/',
|
||||
Origin: 'https://net79.cc'
|
||||
}
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,6 +55,6 @@ export async function getStream({ id, s, e }) {
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { USER_AGENT, fetchJson, getTmdbInfo } from '../utils/helpers.js';
|
||||
import { fetchJson, getTmdbMetadata, resolveTmdbKey, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = 'https://providers.peestream.in';
|
||||
|
||||
@@ -6,58 +6,40 @@ const PROVIDERS = {
|
||||
purstream: 'Athena',
|
||||
vidrock: 'Thor',
|
||||
vaplayer: 'Poseidon',
|
||||
netmirror: 'Ra'
|
||||
netmirror: 'Ra',
|
||||
};
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, server, tmdbApiKey } = args;
|
||||
const { id, s, e, server, sdk } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const type = isTv ? 'tv' : 'movie';
|
||||
const apiKey = resolveTmdbKey(args, sdk);
|
||||
|
||||
let title = args.title || '';
|
||||
let releaseYear = args.year || '';
|
||||
let imdbId = args.imdbId || '';
|
||||
let seasonTmdbId = args.seasonTmdbId || '';
|
||||
let episodeTmdbId = args.episodeTmdbId || '';
|
||||
let title = args?.title || '';
|
||||
let releaseYear = args?.year || '';
|
||||
let imdbId = args?.imdbId || '';
|
||||
let seasonTmdbId = args?.seasonTmdbId || '';
|
||||
let episodeTmdbId = args?.episodeTmdbId || '';
|
||||
|
||||
try {
|
||||
const info = await getTmdbInfo(id, type, s, tmdbApiKey);
|
||||
if (info) {
|
||||
title = title || info.name || info.title || '';
|
||||
|
||||
if (!releaseYear) {
|
||||
const date = info.release_date || info.first_air_date || '';
|
||||
releaseYear = date ? date.split('-')[0] : (info.year || '');
|
||||
}
|
||||
|
||||
if (!imdbId && info.external_ids?.imdb_id) {
|
||||
imdbId = info.external_ids.imdb_id;
|
||||
} else if (!imdbId && info.imdb_id) {
|
||||
imdbId = info.imdb_id;
|
||||
}
|
||||
const meta = await getTmdbMetadata(id, isTv, apiKey);
|
||||
if (meta) {
|
||||
title = title || meta.title || '';
|
||||
if (!releaseYear && meta.year) releaseYear = String(meta.year);
|
||||
if (!imdbId && meta.imdbId) imdbId = meta.imdbId;
|
||||
|
||||
if (isTv) {
|
||||
if (!seasonTmdbId && Array.isArray(info.seasons)) {
|
||||
const matchedSeason = info.seasons.find(
|
||||
item => item.season_number === Number(s)
|
||||
);
|
||||
if (matchedSeason) {
|
||||
seasonTmdbId = String(matchedSeason.id);
|
||||
}
|
||||
if (!seasonTmdbId && Array.isArray(meta.seasons)) {
|
||||
const matchedSeason = meta.seasons.find(item => item.season_number === Number(s));
|
||||
if (matchedSeason) seasonTmdbId = String(matchedSeason.id);
|
||||
}
|
||||
|
||||
if (!episodeTmdbId && tmdbApiKey) {
|
||||
if (!episodeTmdbId && apiKey) {
|
||||
try {
|
||||
const seasonData = await fetchJson(
|
||||
`https://api.themoviedb.org/3/tv/${id}/season/${s}?api_key=${tmdbApiKey}`
|
||||
);
|
||||
const seasonData = await fetchJson(`https://api.themoviedb.org/3/tv/${id}/season/${s}?api_key=${apiKey}`);
|
||||
if (seasonData?.episodes) {
|
||||
const matchedEp = seasonData.episodes.find(
|
||||
ep => ep.episode_number === Number(e)
|
||||
);
|
||||
if (matchedEp) {
|
||||
episodeTmdbId = String(matchedEp.id);
|
||||
}
|
||||
const matchedEp = seasonData.episodes.find(ep => ep.episode_number === Number(e));
|
||||
if (matchedEp) episodeTmdbId = String(matchedEp.id);
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
@@ -68,7 +50,7 @@ export async function getStream(args) {
|
||||
const queryParams = new URLSearchParams({
|
||||
type,
|
||||
tmdbId: String(id),
|
||||
title: title || String(id)
|
||||
title: title || String(id),
|
||||
});
|
||||
|
||||
if (releaseYear) queryParams.set('releaseYear', String(releaseYear));
|
||||
@@ -88,10 +70,10 @@ export async function getStream(args) {
|
||||
const res = await fetch(scrapeUrl, {
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Accept': 'text/event-stream',
|
||||
'Referer': `${BASE_URL}/`
|
||||
Accept: 'text/event-stream',
|
||||
Referer: `${BASE_URL}/`,
|
||||
},
|
||||
signal: AbortSignal.timeout(20000)
|
||||
signal: AbortSignal.timeout(20000),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
@@ -114,8 +96,8 @@ export async function getStream(args) {
|
||||
quality: 'Auto',
|
||||
type: stream.type === 'hls' || streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: stream.headers || {
|
||||
'User-Agent': USER_AGENT
|
||||
}
|
||||
'User-Agent': USER_AGENT,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch { }
|
||||
@@ -128,10 +110,16 @@ export async function getStream(args) {
|
||||
if (server && server !== 'all') {
|
||||
const clean = server.toLowerCase().replace('peestream - ', '').trim();
|
||||
const filtered = allUrls.filter(u => u.server.toLowerCase().includes(clean));
|
||||
if (filtered.length) return { allUrls: filtered };
|
||||
if (filtered.length) return { url: filtered[0].url, headers: filtered[0].headers, allUrls: filtered };
|
||||
}
|
||||
|
||||
return allUrls.length ? { allUrls } : null;
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getSources() {
|
||||
|
||||
@@ -1,50 +1,47 @@
|
||||
import { fetchJson } from '../utils/helpers.js';
|
||||
|
||||
// const PENGU_CONFIG = '%7B%22source_4khdhub%22%3A%22on%22%2C%22source_moviebox%22%3A%22on%22%2C%22source_moviesdrives%22%3A%22on%22%2C%22source_vaplayer%22%3A%22on%22%2C%22source_hdghartv%22%3A%22on%22%2C%22res_2160%22%3A%22on%22%2C%22res_1080%22%3A%22on%22%2C%22res_720%22%3A%22on%22%2C%22disable_direct%22%3A%22on%22%7D';
|
||||
// const BASE_URL = `https://pengu.uk/${PENGU_CONFIG}`;
|
||||
|
||||
async function getImdbId(tmdbApiKey, tmdbId, s, e) {
|
||||
const key = tmdbApiKey;
|
||||
if (!key) return null;
|
||||
try {
|
||||
const url = s != null && e != null
|
||||
? `https://api.themoviedb.org/3/tv/${tmdbId}/external_ids?api_key=${key}`
|
||||
: `https://api.themoviedb.org/3/movie/${tmdbId}?api_key=${key}`;
|
||||
const res = await fetchJson(url);
|
||||
return res?.imdb_id || null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e, sdk }) {
|
||||
const tmdbApiKey = sdk?.tmdbApiKey || null;
|
||||
const manifest = sdk?.penguManifest || null;
|
||||
if (!tmdbApiKey || !manifest || (manifest && !manifest.endsWith('manifest.json'))) return null;
|
||||
const BASE_URL = manifest ? manifest.split('/manifest.json')[0] : null;
|
||||
if (!BASE_URL) return null;
|
||||
try {
|
||||
const imdbId = await getImdbId(tmdbApiKey, id, s, e);
|
||||
if (!imdbId) return null;
|
||||
|
||||
const path = s != null && e != null
|
||||
? `/stream/series/${imdbId}:${s}:${e}.json`
|
||||
: `/stream/movie/${imdbId}.json`;
|
||||
|
||||
const res = await fetchJson(`${BASE_URL}${path}`);
|
||||
if (!res?.streams?.length) return null;
|
||||
|
||||
const allUrls = res.streams.map(stream => {
|
||||
if (!stream.url) return null;
|
||||
const headers = stream.behaviorHints?.proxyHeaders?.request || {};
|
||||
return {
|
||||
url: stream.url,
|
||||
server: `Pengu - ${stream.name || 'Server'}`,
|
||||
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
||||
skipProxy: stream.behaviorHints?.notWebReady === false
|
||||
};
|
||||
}).filter(Boolean);
|
||||
|
||||
return allUrls.length ? { allUrls } : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
import { fetchJson, resolveImdbId, resolveTmdbKey } from '../utils/helpers.js';
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, sdk } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const apiKey = resolveTmdbKey(args, sdk);
|
||||
const manifest = sdk?.penguManifest || null;
|
||||
|
||||
if (!apiKey || !manifest || !manifest.endsWith('manifest.json')) return null;
|
||||
const baseUrl = manifest.split('/manifest.json')[0];
|
||||
if (!baseUrl) return null;
|
||||
|
||||
try {
|
||||
const imdbId = await resolveImdbId(id, isTv, apiKey);
|
||||
if (!imdbId) return null;
|
||||
|
||||
const path = isTv
|
||||
? `/stream/series/${imdbId}:${s}:${e}.json`
|
||||
: `/stream/movie/${imdbId}.json`;
|
||||
|
||||
const res = await fetchJson(`${baseUrl}${path}`);
|
||||
if (!res?.streams?.length) return null;
|
||||
|
||||
const allUrls = res.streams.map(stream => {
|
||||
if (!stream.url) return null;
|
||||
const headers = stream.behaviorHints?.proxyHeaders?.request || {};
|
||||
return {
|
||||
url: stream.url,
|
||||
server: `Pengu - ${stream.name || 'Server'}`,
|
||||
quality: 'Auto',
|
||||
type: stream.url.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
||||
skipProxy: stream.behaviorHints?.notWebReady === false,
|
||||
};
|
||||
}).filter(Boolean);
|
||||
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ async function safeImport(moduleName) {
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, clientIP, sdk } = args;
|
||||
const { id, s, e, clientIP, sdk } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const type = isTv ? 'tv' : 'movie';
|
||||
const allUrls = [];
|
||||
@@ -64,5 +64,11 @@ export async function getStream(args) {
|
||||
})
|
||||
);
|
||||
|
||||
return allUrls.length ? { allUrls } : null;
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
@@ -1,104 +1,96 @@
|
||||
import { fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
|
||||
|
||||
const DOMAIN = 'https://purstream.ad';
|
||||
const API_BASE = 'https://api.purstream.ad/api/v1';
|
||||
|
||||
const HEADERS = {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Referer': `${DOMAIN}/`,
|
||||
'Origin': DOMAIN,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Sec-Fetch-Dest': 'empty',
|
||||
'Sec-Fetch-Mode': 'cors',
|
||||
'Sec-Fetch-Site': 'same-site'
|
||||
};
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
try {
|
||||
const isTv = s != null;
|
||||
|
||||
const info = await getTmdbInfo(id, isTv ? 'tv' : 'movie');
|
||||
|
||||
|
||||
if (!info?.titles?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = info.titles[0];
|
||||
|
||||
const searchUrl = `${API_BASE}/search-bar/search/${encodeURIComponent(title)}`;
|
||||
|
||||
const searchData = await fetchJson(searchUrl, {
|
||||
headers: HEADERS,
|
||||
signal: AbortSignal.timeout(10000)
|
||||
});
|
||||
|
||||
const items = searchData?.data?.items?.movies?.items || [];
|
||||
|
||||
const type = isTv ? 'tv' : 'movie';
|
||||
const lowerTitle = title.toLowerCase();
|
||||
|
||||
let match = items.find(item =>
|
||||
item.type === type &&
|
||||
item.title?.toLowerCase() === lowerTitle &&
|
||||
(!info.year || item.release_date?.startsWith(String(info.year)))
|
||||
);
|
||||
|
||||
if (!match) {
|
||||
match = items.find(item => item.type === type);
|
||||
}
|
||||
|
||||
if (!match?.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const streamUrl = isTv
|
||||
? `${API_BASE}/stream/${match.id}/episode?season=${s}&episode=${e}`
|
||||
: `${API_BASE}/stream/${match.id}`;
|
||||
|
||||
const json = await fetchJson(streamUrl, {
|
||||
headers: HEADERS,
|
||||
signal: AbortSignal.timeout(10000)
|
||||
});
|
||||
|
||||
const sources = json?.data?.items?.sources;
|
||||
|
||||
if (json?.type !== 'success' || !Array.isArray(sources) || !sources.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const chosen = sources.find(src => src.stream_url);
|
||||
|
||||
if (!chosen?.stream_url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = {
|
||||
url: chosen.stream_url,
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Referer': `${DOMAIN}/`
|
||||
},
|
||||
server: 'purstream',
|
||||
quality: chosen.source_name || 'Auto',
|
||||
type: chosen.format || 'hls',
|
||||
skipProxy: true
|
||||
};
|
||||
|
||||
return result;
|
||||
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSources(args) {
|
||||
const stream = await getStream(args);
|
||||
|
||||
return stream ? [stream] : [];
|
||||
}
|
||||
|
||||
export const VERIFY_HEADERS = {
|
||||
...HEADERS
|
||||
};
|
||||
import { fetchJson, getTmdbMetadata, resolveTmdbKey, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const DOMAIN = 'https://purstream.ad';
|
||||
const API_BASE = 'https://api.purstream.ad/api/v1';
|
||||
|
||||
const HEADERS = {
|
||||
'User-Agent': USER_AGENT,
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
Referer: `${DOMAIN}/`,
|
||||
Origin: DOMAIN,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Sec-Fetch-Dest': 'empty',
|
||||
'Sec-Fetch-Mode': 'cors',
|
||||
'Sec-Fetch-Site': 'same-site',
|
||||
};
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, sdk } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const type = isTv ? 'tv' : 'movie';
|
||||
const apiKey = resolveTmdbKey(args, sdk);
|
||||
|
||||
try {
|
||||
const meta = await getTmdbMetadata(id, isTv, apiKey);
|
||||
if (!meta?.titles?.length) return null;
|
||||
|
||||
const title = meta.titles[0];
|
||||
const searchUrl = `${API_BASE}/search-bar/search/${encodeURIComponent(title)}`;
|
||||
|
||||
const searchData = await fetchJson(searchUrl, {
|
||||
headers: HEADERS,
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
|
||||
const items = searchData?.data?.items?.movies?.items || [];
|
||||
const lowerTitle = title.toLowerCase();
|
||||
|
||||
let match = items.find(item =>
|
||||
item.type === type &&
|
||||
item.title?.toLowerCase() === lowerTitle &&
|
||||
(!meta.year || item.release_date?.startsWith(String(meta.year)))
|
||||
);
|
||||
|
||||
if (!match) {
|
||||
match = items.find(item => item.type === type);
|
||||
}
|
||||
|
||||
if (!match?.id) return null;
|
||||
|
||||
const streamUrl = isTv
|
||||
? `${API_BASE}/stream/${match.id}/episode?season=${s}&episode=${e}`
|
||||
: `${API_BASE}/stream/${match.id}`;
|
||||
|
||||
const json = await fetchJson(streamUrl, {
|
||||
headers: HEADERS,
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
|
||||
const sources = json?.data?.items?.sources;
|
||||
if (json?.type !== 'success' || !Array.isArray(sources) || !sources.length) return null;
|
||||
|
||||
const chosen = sources.find(src => src.stream_url);
|
||||
if (!chosen?.stream_url) return null;
|
||||
|
||||
const streamHeaders = {
|
||||
'User-Agent': USER_AGENT,
|
||||
Referer: `${DOMAIN}/`,
|
||||
};
|
||||
|
||||
const allUrls = [{
|
||||
url: chosen.stream_url,
|
||||
headers: streamHeaders,
|
||||
server: 'purstream',
|
||||
quality: chosen.source_name || 'Auto',
|
||||
type: chosen.format || 'hls',
|
||||
skipProxy: true,
|
||||
}];
|
||||
|
||||
return {
|
||||
url: chosen.stream_url,
|
||||
headers: streamHeaders,
|
||||
server: 'purstream',
|
||||
quality: chosen.source_name || 'Auto',
|
||||
type: chosen.format || 'hls',
|
||||
skipProxy: true,
|
||||
allUrls,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSources(args) {
|
||||
const stream = await getStream(args);
|
||||
return stream?.allUrls ? stream.allUrls.map(u => u.server) : ['purstream'];
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { USER_AGENT, fetchJson } from '../utils/helpers.js';
|
||||
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const C_ARRAY = [
|
||||
'4Z7lUo', 'gwIVSMD', 'PLmz2elE2v', 'Z4OFV0', 'SZ6RZq6Zc', 'zhJEFYxrz8', 'FOm7b0', 'axHS3q4KDq', 'o9zuXQ', '4Aebt',
|
||||
@@ -7,12 +7,12 @@ const C_ARRAY = [
|
||||
'W81hjts92', 'rJhAT', 'NON7LKoMQ', 'NMdY3nsKzI', 't4En5v', 'Qq5cOQ9H', 'Y9nwrp', 'VX5FYVfsf', 'cE5SJG', 'x1vj1',
|
||||
'HegbLe', 'zJ3nmt4OA', 'gt7rxW57dq', 'clIE9b', 'jyJ9g', 'B5jXjMCSx', 'cOzZBZTV', 'FTXGy', 'Dfh1q1', 'ny9jqZ2POI',
|
||||
'X2NnMn', 'MBtoyD', 'qz4Ilys7wB', '68lbOMye', '3YUJnmxp', '1fv5Imona', 'PlfvvXD7mA', 'ZarKfHCaPR', 'owORnX', 'dQP1YU',
|
||||
'dVdkx', 'qgiK0E', 'cx9wQ', '5F9bGa', '7UjkKrp', 'Yvhrj', 'wYXez5Dg3', 'pG4GMU', 'MwMAu', 'rFRD5wlM'
|
||||
'dVdkx', 'qgiK0E', 'cx9wQ', '5F9bGa', '7UjkKrp', 'Yvhrj', 'wYXez5Dg3', 'pG4GMU', 'MwMAu', 'rFRD5wlM',
|
||||
];
|
||||
|
||||
const DEFAULT_SERVICES = [
|
||||
'apex', 'pulse', 'solstice', 'quasar', 'horizon', 'primevids',
|
||||
'flowcast', 'asiacloud', 'citadel', 'hindicast', 'guru'
|
||||
'flowcast', 'asiacloud', 'citadel', 'hindicast', 'guru',
|
||||
];
|
||||
|
||||
function generateRiveSecretKey(e) {
|
||||
@@ -80,10 +80,10 @@ async function fetchRiveServices() {
|
||||
const res = await fetchJson(url, {
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Referer': 'https://www.rivestream.app/',
|
||||
'Accept': 'application/json, text/plain, */*'
|
||||
Referer: 'https://www.rivestream.app/',
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
},
|
||||
signal: AbortSignal.timeout(6000)
|
||||
signal: AbortSignal.timeout(6000),
|
||||
});
|
||||
return Array.isArray(res?.data) && res.data.length ? res.data : DEFAULT_SERVICES;
|
||||
} catch {
|
||||
@@ -106,10 +106,10 @@ async function fetchServiceStreams(service, isMovie, id, s, e, secretKey, refere
|
||||
const res = await fetchJson(url, {
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Referer': referer,
|
||||
'Accept': 'application/json, text/plain, */*'
|
||||
Referer: referer,
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
},
|
||||
signal: AbortSignal.timeout(10000)
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
|
||||
if (!res?.data?.sources || !Array.isArray(res.data.sources)) return [];
|
||||
@@ -129,8 +129,8 @@ async function fetchServiceStreams(service, isMovie, id, s, e, secretKey, refere
|
||||
type: isM3U8 ? 'hls' : 'mp4',
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Referer': referer
|
||||
}
|
||||
Referer: referer,
|
||||
},
|
||||
});
|
||||
}
|
||||
return streams;
|
||||
@@ -140,7 +140,7 @@ async function fetchServiceStreams(service, isMovie, id, s, e, secretKey, refere
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, server } = args;
|
||||
const { id, s, e, server } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const isMovie = !isTv;
|
||||
|
||||
@@ -168,7 +168,13 @@ export async function getStream(args) {
|
||||
}
|
||||
}
|
||||
|
||||
return allUrls.length ? { allUrls } : null;
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getSources() {
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import { fetchJson } from '../utils/helpers.js';
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
const BASE_URL = 'https://f.rivo.st';
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const separator = isTv ? '&' : '?';
|
||||
const basePath = isTv
|
||||
? `https://f.rivo.st/api/source/tv/${id}?s=${s}&e=${e}`
|
||||
: `https://f.rivo.st/api/source/movie/${id}`;
|
||||
? `${BASE_URL}/api/source/tv/${id}?s=${s}&e=${e}`
|
||||
: `${BASE_URL}/api/source/movie/${id}`;
|
||||
|
||||
const headers = {
|
||||
'Accept': '*/*',
|
||||
'Referer': 'https://f.rivo.st/',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'
|
||||
Accept: '*/*',
|
||||
Referer: `${BASE_URL}/`,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
|
||||
};
|
||||
|
||||
const serverIndices = Array.from({ length: 26 }, (_, i) => i + 1);
|
||||
const delimiter = basePath.includes('?') ? '&' : '?';
|
||||
|
||||
const responses = await Promise.allSettled(
|
||||
serverIndices.map(serverNum =>
|
||||
fetchJson(`${basePath}&server=${serverNum}`, { headers })
|
||||
fetchJson(`${basePath}${delimiter}server=${serverNum}`, { headers })
|
||||
.catch(() => null)
|
||||
)
|
||||
);
|
||||
@@ -34,10 +37,12 @@ export async function getStream({ id, s, e }) {
|
||||
allUrls.push({
|
||||
url: src.url,
|
||||
server: `Rivo - ${data.server || src.subProvider || 'Server'}`,
|
||||
quality: src.quality || 'Auto',
|
||||
type: src.kind || (src.url.includes('.m3u8') ? 'hls' : 'mp4'),
|
||||
headers: {
|
||||
Referer: 'https://f.rivo.st/',
|
||||
Origin: 'https://f.rivo.st'
|
||||
}
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -49,6 +54,6 @@ export async function getStream({ id, s, e }) {
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
@@ -1,34 +1,17 @@
|
||||
import { getUA } from '../utils/helpers.js';
|
||||
import { getTmdbMetadata, getUA, resolveTmdbKey } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = 'https://streamaggregator.in';
|
||||
|
||||
async function resolveDetails(sdk, id, isTv) {
|
||||
const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY;
|
||||
if (!apiKey) return null;
|
||||
const type = isTv ? 'tv' : 'movie';
|
||||
try {
|
||||
const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`, {
|
||||
signal: AbortSignal.timeout(6000),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
const title = isTv ? data.name : data.title;
|
||||
const releaseDate = isTv ? data.first_air_date : data.release_date;
|
||||
const year = releaseDate ? releaseDate.split('-')[0] : null;
|
||||
return { title, year };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e, sdk }) {
|
||||
const isTv = Boolean(s && e);
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, sdk } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const mediaType = isTv ? 'tv' : 'movie';
|
||||
const apiKey = resolveTmdbKey(args, sdk);
|
||||
const ua = getUA();
|
||||
|
||||
const details = await resolveDetails(sdk, id, isTv);
|
||||
const title = details?.title || String(id);
|
||||
const year = details?.year || '';
|
||||
const meta = await getTmdbMetadata(id, isTv, apiKey);
|
||||
const title = meta?.title || String(id);
|
||||
const year = meta?.year ? String(meta.year) : '';
|
||||
|
||||
const formattedTitle = encodeURIComponent(title).replaceAll('%20', '+');
|
||||
const params = new URLSearchParams({
|
||||
@@ -114,6 +97,9 @@ export async function getStream({ id, s, e, sdk }) {
|
||||
allUrls.push({
|
||||
url: streamUrl,
|
||||
label,
|
||||
server: serverName,
|
||||
quality: item.quality || 'Auto',
|
||||
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers,
|
||||
});
|
||||
} catch { }
|
||||
@@ -123,7 +109,7 @@ export async function getStream({ id, s, e, sdk }) {
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
allUrls,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
import { fetchJson } from '../utils/helpers.js';
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
const BASE_URL = 'https://thugtv.online';
|
||||
const API_BASE = 'https://api.thugtv.online/api';
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const path = isTv ? `/stream/tv/${id}/${s}/${e}` : `/stream/movie/${id}`;
|
||||
const mainUrl = `https://api.thugtv.online/api${path}`;
|
||||
const mainUrl = `${API_BASE}${path}`;
|
||||
|
||||
const headers = {
|
||||
'Accept': 'application/json',
|
||||
'Referer': 'https://thugtv.online/',
|
||||
'Origin': 'https://thugtv.online',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'
|
||||
Accept: 'application/json',
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
|
||||
};
|
||||
|
||||
const res = await fetchJson(mainUrl, { headers }).catch(() => null);
|
||||
@@ -24,17 +28,19 @@ export async function getStream({ id, s, e }) {
|
||||
allUrls.push({
|
||||
url: src.url,
|
||||
server: `ThugTV - ${src.name || src.label || 'Direct'} (${src.quality || 'Auto'})`,
|
||||
quality: src.quality || 'Auto',
|
||||
type: src.url.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: {
|
||||
Referer: 'https://thugtv.online/',
|
||||
Origin: 'https://thugtv.online'
|
||||
}
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (src.resolvePath) {
|
||||
const cleanPath = src.resolvePath.startsWith('/') ? src.resolvePath : `/${src.resolvePath}`;
|
||||
const resolveUrl = `https://thugtv.online/api${cleanPath}`;
|
||||
const resolveUrl = `${BASE_URL}/api${cleanPath}`;
|
||||
|
||||
try {
|
||||
const resolved = await fetchJson(resolveUrl, { headers });
|
||||
@@ -43,10 +49,12 @@ export async function getStream({ id, s, e }) {
|
||||
allUrls.push({
|
||||
url: directUrl,
|
||||
server: `ThugTV - ${resolved.source.name || resolved.source.label || src.name || 'Direct'}`,
|
||||
quality: resolved.source.quality || src.quality || 'Auto',
|
||||
type: directUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: {
|
||||
Referer: 'https://thugtv.online/',
|
||||
Origin: 'https://thugtv.online'
|
||||
}
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch { }
|
||||
@@ -58,6 +66,6 @@ export async function getStream({ id, s, e }) {
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
@@ -1,115 +1,123 @@
|
||||
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const API_BASE = 'https://streamdata.vaplayer.ru/api.php';
|
||||
const EMBED_ORIGIN = 'https://nextgencloudfabric.com';
|
||||
|
||||
export const VERIFY_HEADERS = {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Referer': `${EMBED_ORIGIN}/`,
|
||||
'Origin': EMBED_ORIGIN
|
||||
};
|
||||
|
||||
function extractStreamUrls(obj, urls = new Set()) {
|
||||
if (!obj) return urls;
|
||||
if (typeof obj === 'string') {
|
||||
if (obj.startsWith('http') && (obj.includes('.m3u8') || obj.includes('.mp4') || obj.includes('.mpd'))) {
|
||||
urls.add(obj);
|
||||
}
|
||||
} else if (Array.isArray(obj)) {
|
||||
for (const item of obj) extractStreamUrls(item, urls);
|
||||
} else if (typeof obj === 'object') {
|
||||
for (const val of Object.values(obj)) {
|
||||
extractStreamUrls(val, urls);
|
||||
}
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
try {
|
||||
const isTv = s != null && e != null;
|
||||
const type = isTv ? 'tv' : 'movie';
|
||||
|
||||
const url = `${API_BASE}?tmdb=${id}&type=${type}${isTv ? `&season=${s}&episode=${e}` : ''}`;
|
||||
|
||||
const data = await fetchJson(url, {
|
||||
headers: {
|
||||
'Accept': '*/*',
|
||||
'Origin': EMBED_ORIGIN,
|
||||
'Referer': `${EMBED_ORIGIN}/`,
|
||||
'User-Agent': USER_AGENT
|
||||
},
|
||||
signal: AbortSignal.timeout(10000)
|
||||
});
|
||||
|
||||
const streamUrls = Array.from(extractStreamUrls(data));
|
||||
if (!streamUrls.length) return null;
|
||||
|
||||
let subs = [];
|
||||
if (data.subtitles && Array.isArray(data.subtitles)) {
|
||||
subs = data.subtitles;
|
||||
} else if (data.captions && Array.isArray(data.captions)) {
|
||||
subs = data.captions;
|
||||
}
|
||||
|
||||
const subtitles = subs.map(sub => ({
|
||||
url: sub.url || sub.file || sub.src,
|
||||
lang: sub.display || sub.label || sub.language || sub.lang || 'Unknown'
|
||||
})).filter(s => s.url);
|
||||
|
||||
const allUrls = streamUrls.map(streamUrl => {
|
||||
let type = 'mp4';
|
||||
if (streamUrl.includes('.mpd')) type = 'dash';
|
||||
else if (streamUrl.includes('.m3u8')) type = 'hls';
|
||||
|
||||
return {
|
||||
url: streamUrl,
|
||||
server: 'VidAPI',
|
||||
quality: 'Auto',
|
||||
type,
|
||||
headers: VERIFY_HEADERS,
|
||||
subtitles: subtitles.length > 0 ? subtitles : undefined,
|
||||
skipProxy: false
|
||||
};
|
||||
});
|
||||
|
||||
return allUrls.length ? { allUrls } : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSources() {
|
||||
return ['VidAPI'];
|
||||
}
|
||||
|
||||
export async function proxyStream({ url, res, proxyUtils: { fetchUpstream, rewriteM3u8 } }) {
|
||||
try {
|
||||
const upstream = await fetchUpstream(url, 0, VERIFY_HEADERS);
|
||||
if (!upstream) return res.end();
|
||||
|
||||
const ct = (upstream.headers?.['content-type'] || '').toLowerCase();
|
||||
const isM3u8 = ct.includes('mpegurl') || ct.includes('m3u8') || /\.m3u8?(\?|$)/i.test(url);
|
||||
|
||||
if (isM3u8) {
|
||||
const chunks = [];
|
||||
for await (const c of upstream) chunks.push(c);
|
||||
let body = Buffer.concat(chunks).toString('utf8');
|
||||
|
||||
res.setHeader('Content-Type', 'application/vnd.apple.mpegurl');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
|
||||
return res.end(rewriteM3u8(body, url, '&va=1&tt=1'));
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', ct || 'application/octet-stream');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Cache-Control', 'public, max-age=3600');
|
||||
upstream.pipe(res);
|
||||
} catch {
|
||||
if (!res.headersSent) {
|
||||
res.statusCode = 502;
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const API_BASE = 'https://streamdata.vaplayer.ru/api.php';
|
||||
const EMBED_ORIGIN = 'https://nextgencloudfabric.com';
|
||||
|
||||
export const VERIFY_HEADERS = {
|
||||
'User-Agent': USER_AGENT,
|
||||
Referer: `${EMBED_ORIGIN}/`,
|
||||
Origin: EMBED_ORIGIN,
|
||||
};
|
||||
|
||||
function extractStreamUrls(obj, urls = new Set()) {
|
||||
if (!obj) return urls;
|
||||
if (typeof obj === 'string') {
|
||||
if (obj.startsWith('http') && (obj.includes('.m3u8') || obj.includes('.mp4') || obj.includes('.mpd'))) {
|
||||
urls.add(obj);
|
||||
}
|
||||
} else if (Array.isArray(obj)) {
|
||||
for (const item of obj) extractStreamUrls(item, urls);
|
||||
} else if (typeof obj === 'object') {
|
||||
for (const val of Object.values(obj)) {
|
||||
extractStreamUrls(val, urls);
|
||||
}
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const type = isTv ? 'tv' : 'movie';
|
||||
|
||||
const url = `${API_BASE}?tmdb=${id}&type=${type}${isTv ? `&season=${s}&episode=${e}` : ''}`;
|
||||
|
||||
try {
|
||||
const data = await fetchJson(url, {
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
Origin: EMBED_ORIGIN,
|
||||
Referer: `${EMBED_ORIGIN}/`,
|
||||
'User-Agent': USER_AGENT,
|
||||
},
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
|
||||
const streamUrls = Array.from(extractStreamUrls(data));
|
||||
if (!streamUrls.length) return null;
|
||||
|
||||
let subs = [];
|
||||
if (data.subtitles && Array.isArray(data.subtitles)) {
|
||||
subs = data.subtitles;
|
||||
} else if (data.captions && Array.isArray(data.captions)) {
|
||||
subs = data.captions;
|
||||
}
|
||||
|
||||
const subtitles = subs.map(sub => ({
|
||||
url: sub.url || sub.file || sub.src,
|
||||
lang: sub.display || sub.label || sub.language || sub.lang || 'Unknown',
|
||||
})).filter(subItem => subItem.url);
|
||||
|
||||
const allUrls = streamUrls.map(streamUrl => {
|
||||
let type = 'mp4';
|
||||
if (streamUrl.includes('.mpd')) type = 'dash';
|
||||
else if (streamUrl.includes('.m3u8')) type = 'hls';
|
||||
|
||||
return {
|
||||
url: streamUrl,
|
||||
server: 'VidAPI',
|
||||
quality: 'Auto',
|
||||
type,
|
||||
headers: VERIFY_HEADERS,
|
||||
subtitles: subtitles.length > 0 ? subtitles : undefined,
|
||||
skipProxy: false,
|
||||
};
|
||||
});
|
||||
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
...(subtitles.length > 0 ? { subtitles } : {}),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSources() {
|
||||
return ['VidAPI'];
|
||||
}
|
||||
|
||||
export async function proxyStream({ url, res, proxyUtils: { fetchUpstream, rewriteM3u8 } }) {
|
||||
try {
|
||||
const upstream = await fetchUpstream(url, 0, VERIFY_HEADERS);
|
||||
if (!upstream) return res.end();
|
||||
|
||||
const ct = (upstream.headers?.['content-type'] || '').toLowerCase();
|
||||
const isM3u8 = ct.includes('mpegurl') || ct.includes('m3u8') || /\.m3u8?(\?|$)/i.test(url);
|
||||
|
||||
if (isM3u8) {
|
||||
const chunks = [];
|
||||
for await (const c of upstream) chunks.push(c);
|
||||
let body = Buffer.concat(chunks).toString('utf8');
|
||||
|
||||
res.setHeader('Content-Type', 'application/vnd.apple.mpegurl');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
|
||||
return res.end(rewriteM3u8(body, url, '&va=1&tt=1'));
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', ct || 'application/octet-stream');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Cache-Control', 'public, max-age=3600');
|
||||
upstream.pipe(res);
|
||||
} catch {
|
||||
if (!res.headersSent) {
|
||||
res.statusCode = 502;
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,123 +1,15 @@
|
||||
import { getUA } from '../utils/helpers.js';
|
||||
import { scrapeVidcoreFamily } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = 'https://vidcore.io';
|
||||
const API = 'https://enc-dec.app/api';
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
const isTv = Boolean(s && e);
|
||||
const targetUrl = isTv
|
||||
? `${BASE_URL}/tv/${id}/${s}/${e}/`
|
||||
: `${BASE_URL}/movie/${id}`;
|
||||
|
||||
const ua = getUA();
|
||||
const baseHeaders = {
|
||||
'User-Agent': ua,
|
||||
'Referer': `${BASE_URL}/`,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
};
|
||||
|
||||
try {
|
||||
const pageRes = await fetch(targetUrl, {
|
||||
headers: { 'User-Agent': ua },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!pageRes.ok) return null;
|
||||
const html = await pageRes.text();
|
||||
|
||||
const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/);
|
||||
if (!match || !match[1]) return null;
|
||||
const enToken = match[1];
|
||||
|
||||
const s1Res = await fetch(`${API}/enc-vidcore?text=${encodeURIComponent(enToken)}&stage=1`, {
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
const s1Data = await s1Res.json();
|
||||
if (s1Data?.status !== 200 || !s1Data?.result?.stage1) return null;
|
||||
|
||||
const token1 = s1Data.result.token;
|
||||
const stage1Url = s1Data.result.stage1;
|
||||
|
||||
const post1Res = await fetch(stage1Url, {
|
||||
method: 'POST',
|
||||
headers: { ...baseHeaders, 'X-CSRF-Token': token1 },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!post1Res.ok) return null;
|
||||
const stage1Text = await post1Res.text();
|
||||
|
||||
const s2Res = await fetch(`${API}/enc-vidcore?text=${encodeURIComponent(stage1Text)}&stage=2`, {
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
const s2Data = await s2Res.json();
|
||||
if (s2Data?.status !== 200 || !s2Data?.result?.servers) return null;
|
||||
|
||||
const { servers: serversUrl, stream: streamBaseUrl, token: token2 } = s2Data.result;
|
||||
const currentHeaders = { ...baseHeaders, 'X-CSRF-Token': token2 };
|
||||
|
||||
const serversRes = await fetch(serversUrl, {
|
||||
method: 'POST',
|
||||
headers: currentHeaders,
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!serversRes.ok) return null;
|
||||
const serversEncrypted = await serversRes.text();
|
||||
|
||||
const decServersRes = await fetch(`${API}/dec-vidcore`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: serversEncrypted }),
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
const decServersData = await decServersRes.json();
|
||||
if (decServersData?.status !== 200 || !Array.isArray(decServersData.result)) return null;
|
||||
|
||||
const allUrls = [];
|
||||
|
||||
for (const server of decServersData.result) {
|
||||
if (!server?.data) continue;
|
||||
try {
|
||||
const streamUrl = `${streamBaseUrl}/${server.data}`;
|
||||
const streamEncRes = await fetch(streamUrl, {
|
||||
method: 'POST',
|
||||
headers: currentHeaders,
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
if (!streamEncRes.ok) continue;
|
||||
const streamEncText = await streamEncRes.text();
|
||||
|
||||
const decStreamRes = await fetch(`${API}/dec-vidcore`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: streamEncText }),
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
const decStreamData = await decStreamRes.json();
|
||||
if (decStreamData?.status !== 200 || !decStreamData.result) continue;
|
||||
|
||||
const streamResult = decStreamData.result;
|
||||
const resultUrl = typeof streamResult === 'string' ? streamResult : streamResult?.url || streamResult?.file;
|
||||
|
||||
if (typeof resultUrl === 'string' && resultUrl.startsWith('http')) {
|
||||
allUrls.push({
|
||||
url: resultUrl,
|
||||
label: server.name || server.title || 'VidCore',
|
||||
headers: {
|
||||
'User-Agent': ua,
|
||||
'Referer': `${BASE_URL}/`,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
if (allUrls.length === 0) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
allUrls,
|
||||
headers: allUrls[0].headers,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
return await scrapeVidcoreFamily({
|
||||
id,
|
||||
s,
|
||||
e,
|
||||
baseUrl: BASE_URL,
|
||||
apiName: 'vidcore',
|
||||
serverLabel: 'VidCore',
|
||||
});
|
||||
}
|
||||
@@ -1,123 +1,15 @@
|
||||
import { getUA } from '../utils/helpers.js';
|
||||
import { scrapeVidcoreFamily } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = 'https://vidfast.vc';
|
||||
const API = 'https://enc-dec.app/api';
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
const isTv = Boolean(s && e);
|
||||
const targetUrl = isTv
|
||||
? `${BASE_URL}/tv/${id}/${s}/${e}/`
|
||||
: `${BASE_URL}/movie/${id}`;
|
||||
|
||||
const ua = getUA();
|
||||
const baseHeaders = {
|
||||
'User-Agent': ua,
|
||||
'Referer': `${BASE_URL}/`,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
};
|
||||
|
||||
try {
|
||||
const pageRes = await fetch(targetUrl, {
|
||||
headers: { 'User-Agent': ua },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!pageRes.ok) return null;
|
||||
const html = await pageRes.text();
|
||||
|
||||
const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/);
|
||||
if (!match || !match[1]) return null;
|
||||
const enToken = match[1];
|
||||
|
||||
const s1Res = await fetch(`${API}/enc-vidfast?text=${encodeURIComponent(enToken)}&stage=1`, {
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
const s1Data = await s1Res.json();
|
||||
if (s1Data?.status !== 200 || !s1Data?.result?.stage1) return null;
|
||||
|
||||
const token1 = s1Data.result.token;
|
||||
const stage1Url = s1Data.result.stage1;
|
||||
|
||||
const post1Res = await fetch(stage1Url, {
|
||||
method: 'POST',
|
||||
headers: { ...baseHeaders, 'X-CSRF-Token': token1 },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!post1Res.ok) return null;
|
||||
const stage1Text = await post1Res.text();
|
||||
|
||||
const s2Res = await fetch(`${API}/enc-vidfast?text=${encodeURIComponent(stage1Text)}&stage=2`, {
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
const s2Data = await s2Res.json();
|
||||
if (s2Data?.status !== 200 || !s2Data?.result?.servers) return null;
|
||||
|
||||
const { servers: serversUrl, stream: streamBaseUrl, token: token2 } = s2Data.result;
|
||||
const currentHeaders = { ...baseHeaders, 'X-CSRF-Token': token2 };
|
||||
|
||||
const serversRes = await fetch(serversUrl, {
|
||||
method: 'POST',
|
||||
headers: currentHeaders,
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!serversRes.ok) return null;
|
||||
const serversEncrypted = await serversRes.text();
|
||||
|
||||
const decServersRes = await fetch(`${API}/dec-vidfast`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: serversEncrypted }),
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
const decServersData = await decServersRes.json();
|
||||
if (decServersData?.status !== 200 || !Array.isArray(decServersData.result)) return null;
|
||||
|
||||
const allUrls = [];
|
||||
|
||||
for (const server of decServersData.result) {
|
||||
if (!server?.data) continue;
|
||||
try {
|
||||
const streamUrl = `${streamBaseUrl}/${server.data}`;
|
||||
const streamEncRes = await fetch(streamUrl, {
|
||||
method: 'POST',
|
||||
headers: currentHeaders,
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
if (!streamEncRes.ok) continue;
|
||||
const streamEncText = await streamEncRes.text();
|
||||
|
||||
const decStreamRes = await fetch(`${API}/dec-vidfast`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: streamEncText }),
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
const decStreamData = await decStreamRes.json();
|
||||
if (decStreamData?.status !== 200 || !decStreamData.result) continue;
|
||||
|
||||
const streamResult = decStreamData.result;
|
||||
const resultUrl = typeof streamResult === 'string' ? streamResult : streamResult?.url || streamResult?.file;
|
||||
|
||||
if (typeof resultUrl === 'string' && resultUrl.startsWith('http')) {
|
||||
allUrls.push({
|
||||
url: resultUrl,
|
||||
label: server.name || server.title || 'VidFast',
|
||||
headers: {
|
||||
'User-Agent': ua,
|
||||
'Referer': `${BASE_URL}/`,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
if (allUrls.length === 0) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
allUrls,
|
||||
headers: allUrls[0].headers,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
return await scrapeVidcoreFamily({
|
||||
id,
|
||||
s,
|
||||
e,
|
||||
baseUrl: BASE_URL,
|
||||
apiName: 'vidfast',
|
||||
serverLabel: 'VidFast',
|
||||
});
|
||||
}
|
||||
@@ -1,76 +1,79 @@
|
||||
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const BASE = "https://vidlink.pro";
|
||||
const HEADERS = { 'User-Agent': USER_AGENT, Origin: BASE, Referer: `${BASE}/` };
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
try {
|
||||
const encData = await fetchJson(`https://enc-dec.app/api/enc-vidlink?text=${id}`, { signal: AbortSignal.timeout(6000) });
|
||||
if (encData?.status !== 200 || !encData?.result) return null;
|
||||
|
||||
const basePath = s ? `${BASE}/api/b/tv/${encData.result}/${s}/${e || 1}` : `${BASE}/api/b/movie/${encData.result}`;
|
||||
const responses = await Promise.allSettled([
|
||||
fetchJson(basePath, { headers: HEADERS, signal: AbortSignal.timeout(10000) }),
|
||||
fetchJson(`${basePath}?multiLang=0`, { headers: HEADERS, signal: AbortSignal.timeout(10000) })
|
||||
]);
|
||||
|
||||
const streamList = [];
|
||||
const seenUrls = new Set();
|
||||
const seenSubs = new Set();
|
||||
|
||||
for (const res of responses) {
|
||||
if (res.status !== 'fulfilled' || !res.value?.stream) continue;
|
||||
const stream = res.value.stream;
|
||||
|
||||
if (stream.playlist && !seenUrls.has(stream.playlist)) {
|
||||
seenUrls.add(stream.playlist);
|
||||
const isDash = stream.type === 'dash' || stream.deliveryType === 'dash' || stream.playlist.includes('.mpd');
|
||||
streamList.push({
|
||||
url: stream.playlist,
|
||||
quality: stream.playbackMetadata?.resolutions?.[0] ? `${stream.playbackMetadata.resolutions[0]}p` : 'Auto',
|
||||
label: `Vidlink ${isDash ? 'DASH' : 'HLS'}`,
|
||||
name: `Vidlink ${isDash ? 'DASH' : 'HLS'}`,
|
||||
type: isDash ? 'dash' : 'hls',
|
||||
headers: stream.playlistHeaders || {},
|
||||
server: 'Vidlink',
|
||||
});
|
||||
}
|
||||
|
||||
if (stream.qualities && typeof stream.qualities === 'object') {
|
||||
const qualityOrder = ['1080', '720', '480', '360', ...Object.keys(stream.qualities)];
|
||||
const seenQualities = new Set();
|
||||
|
||||
for (const q of qualityOrder) {
|
||||
if (seenQualities.has(q)) continue;
|
||||
seenQualities.add(q);
|
||||
const qData = stream.qualities[q];
|
||||
if (!qData?.url || seenUrls.has(qData.url)) continue;
|
||||
seenUrls.add(qData.url);
|
||||
streamList.push({
|
||||
url: qData.url,
|
||||
quality: `${q}p`,
|
||||
label: `Vidlink ${q}p`,
|
||||
name: `Vidlink ${q}p`,
|
||||
type: qData.type || 'mp4',
|
||||
headers: qData.headers || {},
|
||||
server: 'Vidlink',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!streamList.length) return null;
|
||||
|
||||
return {
|
||||
allUrls: streamList,
|
||||
streams: streamList,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSources(args) {
|
||||
const stream = await getStream(args);
|
||||
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server || 'Vidlink'))] : [];
|
||||
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = 'https://vidlink.pro';
|
||||
const HEADERS = { 'User-Agent': USER_AGENT, Origin: BASE_URL, Referer: `${BASE_URL}/` };
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
|
||||
try {
|
||||
const encData = await fetchJson(`https://enc-dec.app/api/enc-vidlink?text=${id}`, { signal: AbortSignal.timeout(6000) });
|
||||
if (encData?.status !== 200 || !encData?.result) return null;
|
||||
|
||||
const basePath = s ? `${BASE_URL}/api/b/tv/${encData.result}/${s}/${e || 1}` : `${BASE_URL}/api/b/movie/${encData.result}`;
|
||||
const responses = await Promise.allSettled([
|
||||
fetchJson(basePath, { headers: HEADERS, signal: AbortSignal.timeout(10000) }),
|
||||
fetchJson(`${basePath}?multiLang=0`, { headers: HEADERS, signal: AbortSignal.timeout(10000) }),
|
||||
]);
|
||||
|
||||
const streamList = [];
|
||||
const seenUrls = new Set();
|
||||
|
||||
for (const res of responses) {
|
||||
if (res.status !== 'fulfilled' || !res.value?.stream) continue;
|
||||
const stream = res.value.stream;
|
||||
|
||||
if (stream.playlist && !seenUrls.has(stream.playlist)) {
|
||||
seenUrls.add(stream.playlist);
|
||||
const isDash = stream.type === 'dash' || stream.deliveryType === 'dash' || stream.playlist.includes('.mpd');
|
||||
streamList.push({
|
||||
url: stream.playlist,
|
||||
quality: stream.playbackMetadata?.resolutions?.[0] ? `${stream.playbackMetadata.resolutions[0]}p` : 'Auto',
|
||||
label: `Vidlink ${isDash ? 'DASH' : 'HLS'}`,
|
||||
name: `Vidlink ${isDash ? 'DASH' : 'HLS'}`,
|
||||
server: 'Vidlink',
|
||||
type: isDash ? 'dash' : 'hls',
|
||||
headers: stream.playlistHeaders || {},
|
||||
});
|
||||
}
|
||||
|
||||
if (stream.qualities && typeof stream.qualities === 'object') {
|
||||
const qualityOrder = ['1080', '720', '480', '360', ...Object.keys(stream.qualities)];
|
||||
const seenQualities = new Set();
|
||||
|
||||
for (const q of qualityOrder) {
|
||||
if (seenQualities.has(q)) continue;
|
||||
seenQualities.add(q);
|
||||
const qData = stream.qualities[q];
|
||||
if (!qData?.url || seenUrls.has(qData.url)) continue;
|
||||
seenUrls.add(qData.url);
|
||||
streamList.push({
|
||||
url: qData.url,
|
||||
quality: `${q}p`,
|
||||
label: `Vidlink ${q}p`,
|
||||
name: `Vidlink ${q}p`,
|
||||
server: 'Vidlink',
|
||||
type: qData.type || 'mp4',
|
||||
headers: qData.headers || {},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!streamList.length) return null;
|
||||
|
||||
return {
|
||||
url: streamList[0].url,
|
||||
headers: streamList[0].headers,
|
||||
allUrls: streamList,
|
||||
streams: streamList,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSources(args) {
|
||||
const stream = await getStream(args);
|
||||
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server || 'Vidlink'))] : [];
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
import { webcrypto } from 'node:crypto';
|
||||
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const GCM_HEX_KEY = '7f3e9c2a8b5d1f4e6a9c3b7d2e5f8a1c4b6d9e2f5a8c1b4d7e9f2a5c8b1d4e7f';
|
||||
const BASE_URL = 'https://vidrock.ru/';
|
||||
const PROXY_PREFIX = 'https://proxy.vidrock.store/';
|
||||
|
||||
let cryptoKey = null;
|
||||
|
||||
function hexToBytes(hex) {
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function base64UrlToBytes(value) {
|
||||
let base64 = value.replace(/-/g, '+').replace(/_/g, '/');
|
||||
|
||||
const padding = base64.length % 4;
|
||||
|
||||
if (padding === 2) {
|
||||
base64 += '==';
|
||||
} else if (padding === 3) {
|
||||
base64 += '=';
|
||||
} else if (padding === 1) {
|
||||
throw new Error('Invalid base64url');
|
||||
}
|
||||
|
||||
return new Uint8Array(Buffer.from(base64, 'base64'));
|
||||
}
|
||||
|
||||
async function getKey() {
|
||||
if (cryptoKey) return cryptoKey;
|
||||
|
||||
const keyBytes = hexToBytes(GCM_HEX_KEY);
|
||||
|
||||
cryptoKey = await webcrypto.subtle.importKey(
|
||||
'raw',
|
||||
keyBytes,
|
||||
'AES-GCM',
|
||||
false,
|
||||
['decrypt']
|
||||
);
|
||||
|
||||
return cryptoKey;
|
||||
}
|
||||
|
||||
async function decryptStreamUrl(value) {
|
||||
try {
|
||||
const data = base64UrlToBytes(value);
|
||||
|
||||
if (data.length < 28) {
|
||||
throw new Error('Ciphertext too short');
|
||||
}
|
||||
|
||||
const iv = data.slice(0, 12);
|
||||
const ciphertext = data.slice(12);
|
||||
|
||||
const key = await getKey();
|
||||
|
||||
const decrypted = await webcrypto.subtle.decrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv
|
||||
},
|
||||
key,
|
||||
ciphertext
|
||||
);
|
||||
|
||||
return new TextDecoder().decode(decrypted);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
try {
|
||||
const path = s
|
||||
? `tv/${id}/${s}/${e || 1}`
|
||||
: `movie/${id}`;
|
||||
|
||||
const apiData = await fetchJson(`${BASE_URL}api/${path}`, {
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
Referer: BASE_URL,
|
||||
Origin: BASE_URL
|
||||
}
|
||||
});
|
||||
|
||||
if (!apiData || typeof apiData !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const streams = [];
|
||||
|
||||
for (const [provider, info] of Object.entries(apiData)) {
|
||||
if (!info?.url) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const decryptedUrl = await decryptStreamUrl(info.url);
|
||||
|
||||
if (!decryptedUrl) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let url = decryptedUrl;
|
||||
|
||||
if (url.startsWith(PROXY_PREFIX)) {
|
||||
url = decodeURIComponent(
|
||||
url.slice(PROXY_PREFIX.length).replace(/^\/+/, '')
|
||||
);
|
||||
}
|
||||
|
||||
const isXpass = url.includes('xpass.top');
|
||||
|
||||
streams.push({
|
||||
name: provider,
|
||||
url,
|
||||
file: url,
|
||||
stream: url,
|
||||
src: url,
|
||||
quality: info.type || 'auto',
|
||||
type: info.type || 'hls',
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
Referer: isXpass
|
||||
? 'https://play.xpass.top/'
|
||||
: BASE_URL,
|
||||
Origin: isXpass
|
||||
? 'https://play.xpass.top'
|
||||
: 'https://vidrock.ru'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return streams.length ? { streams } : null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,123 +1,15 @@
|
||||
import { getUA } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = 'https://vidup.to';
|
||||
const API = 'https://enc-dec.app/api';
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
const isTv = Boolean(s && e);
|
||||
const targetUrl = isTv
|
||||
? `${BASE_URL}/tv/${id}/${s}/${e}/`
|
||||
: `${BASE_URL}/movie/${id}`;
|
||||
|
||||
const ua = getUA();
|
||||
const baseHeaders = {
|
||||
'User-Agent': ua,
|
||||
'Referer': `${BASE_URL}/`,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
};
|
||||
|
||||
try {
|
||||
const pageRes = await fetch(targetUrl, {
|
||||
headers: { 'User-Agent': ua },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!pageRes.ok) return null;
|
||||
const html = await pageRes.text();
|
||||
|
||||
const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/);
|
||||
if (!match || !match[1]) return null;
|
||||
const enToken = match[1];
|
||||
|
||||
const s1Res = await fetch(`${API}/enc-vidup?text=${encodeURIComponent(enToken)}&stage=1`, {
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
const s1Data = await s1Res.json();
|
||||
if (s1Data?.status !== 200 || !s1Data?.result?.stage1) return null;
|
||||
|
||||
const token1 = s1Data.result.token;
|
||||
const stage1Url = s1Data.result.stage1;
|
||||
|
||||
const post1Res = await fetch(stage1Url, {
|
||||
method: 'POST',
|
||||
headers: { ...baseHeaders, 'X-CSRF-Token': token1 },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!post1Res.ok) return null;
|
||||
const stage1Text = await post1Res.text();
|
||||
|
||||
const s2Res = await fetch(`${API}/enc-vidup?text=${encodeURIComponent(stage1Text)}&stage=2`, {
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
const s2Data = await s2Res.json();
|
||||
if (s2Data?.status !== 200 || !s2Data?.result?.servers) return null;
|
||||
|
||||
const { servers: serversUrl, stream: streamBaseUrl, token: token2 } = s2Data.result;
|
||||
const currentHeaders = { ...baseHeaders, 'X-CSRF-Token': token2 };
|
||||
|
||||
const serversRes = await fetch(serversUrl, {
|
||||
method: 'POST',
|
||||
headers: currentHeaders,
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!serversRes.ok) return null;
|
||||
const serversEncrypted = await serversRes.text();
|
||||
|
||||
const decServersRes = await fetch(`${API}/dec-vidup`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: serversEncrypted }),
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
const decServersData = await decServersRes.json();
|
||||
if (decServersData?.status !== 200 || !Array.isArray(decServersData.result)) return null;
|
||||
|
||||
const allUrls = [];
|
||||
|
||||
for (const server of decServersData.result) {
|
||||
if (!server?.data) continue;
|
||||
try {
|
||||
const streamUrl = `${streamBaseUrl}/${server.data}`;
|
||||
const streamEncRes = await fetch(streamUrl, {
|
||||
method: 'POST',
|
||||
headers: currentHeaders,
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
if (!streamEncRes.ok) continue;
|
||||
const streamEncText = await streamEncRes.text();
|
||||
|
||||
const decStreamRes = await fetch(`${API}/dec-vidup`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: streamEncText }),
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
const decStreamData = await decStreamRes.json();
|
||||
if (decStreamData?.status !== 200 || !decStreamData.result) continue;
|
||||
|
||||
const streamResult = decStreamData.result;
|
||||
const resultUrl = typeof streamResult === 'string' ? streamResult : streamResult?.url || streamResult?.file;
|
||||
|
||||
if (typeof resultUrl === 'string' && resultUrl.startsWith('http')) {
|
||||
allUrls.push({
|
||||
url: resultUrl,
|
||||
label: server.name || server.title || 'VidUp',
|
||||
headers: {
|
||||
'User-Agent': ua,
|
||||
'Referer': `${BASE_URL}/`,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
if (allUrls.length === 0) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
allUrls,
|
||||
headers: allUrls[0].headers,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
import { scrapeVidcoreFamily } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = 'https://vidup.to';
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
return await scrapeVidcoreFamily({
|
||||
id,
|
||||
s,
|
||||
e,
|
||||
baseUrl: BASE_URL,
|
||||
apiName: 'vidup',
|
||||
serverLabel: 'VidUp',
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { USER_AGENT } from '../utils/helpers.js';
|
||||
import { formatBytes, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = 'https://vidvault.to';
|
||||
const API_BASE = `${BASE_URL}/api`;
|
||||
@@ -6,21 +6,13 @@ const DL_PROXY_BASE = 'https://dl.gemlelispe.workers.dev';
|
||||
const SUB_PROXY_BASE = 'https://sub.k5s7sjozpn.workers.dev';
|
||||
|
||||
const HEADERS = {
|
||||
'Referer': `${BASE_URL}/`,
|
||||
'Origin': BASE_URL,
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
'User-Agent': USER_AGENT,
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
};
|
||||
|
||||
function formatBytes(bytes, decimals = 2) {
|
||||
if (!bytes || bytes <= 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1);
|
||||
return `${(bytes / Math.pow(k, i)).toFixed(decimals)} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
async function getToken() {
|
||||
const res = await fetch(`${API_BASE}/get-token`, {
|
||||
method: 'GET',
|
||||
@@ -36,26 +28,73 @@ function getCaptionType(url) {
|
||||
return String(url).toLowerCase().includes('.vtt') ? 'vtt' : 'srt';
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args;
|
||||
const isTv = s != null && e != null;
|
||||
const type = isTv ? 'tv' : 'movie';
|
||||
|
||||
async function fetchJson(url, options = {}) {
|
||||
try {
|
||||
const token = await getToken();
|
||||
if (!token) return null;
|
||||
const res = await fetch(url, options);
|
||||
if (!res.ok) return null;
|
||||
return await res.json();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
type,
|
||||
tmdbId: String(id),
|
||||
};
|
||||
async function resolveDirect(directPath, headers) {
|
||||
const fullUrl = directPath.startsWith('http') ? directPath : `${BASE_URL}${directPath}`;
|
||||
try {
|
||||
const res = await fetch(fullUrl, {
|
||||
method: 'HEAD',
|
||||
redirect: 'manual',
|
||||
headers,
|
||||
});
|
||||
const loc = res.headers.get('location');
|
||||
if (loc) return loc;
|
||||
} catch { }
|
||||
return fullUrl;
|
||||
}
|
||||
|
||||
if (isTv) {
|
||||
payload.season = Number(s);
|
||||
payload.episode = Number(e);
|
||||
}
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
|
||||
const res = await fetch(`${API_BASE}/download-proxy`, {
|
||||
const mediaPath = isTv
|
||||
? `${BASE_URL}/media/tv/${id}/${s}/${e}`
|
||||
: `${BASE_URL}/media/movie/${id}`;
|
||||
|
||||
const [mediaData, tokenData] = await Promise.all([
|
||||
fetchJson(mediaPath, { headers: HEADERS }).catch(() => null),
|
||||
fetchJson(`${BASE_URL}/api/get-token`, { headers: HEADERS }).catch(() => null),
|
||||
]);
|
||||
|
||||
const allUrls = [];
|
||||
const seen = new Set();
|
||||
|
||||
if (Array.isArray(mediaData?.direct)) {
|
||||
await Promise.all(
|
||||
mediaData.direct.map(async (item) => {
|
||||
if (!item?.path) return;
|
||||
const streamUrl = await resolveDirect(item.path, HEADERS);
|
||||
if (streamUrl && !seen.has(streamUrl)) {
|
||||
seen.add(streamUrl);
|
||||
const format = item.format || (streamUrl.includes('.mkv') ? 'mkv' : 'mp4');
|
||||
allUrls.push({
|
||||
url: streamUrl,
|
||||
server: `VidVault - Direct ${format.toUpperCase()} (${item.quality || item.size || 'Auto'})`,
|
||||
quality: item.quality || 'Auto',
|
||||
type: format,
|
||||
headers: HEADERS,
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const token = tokenData?.t;
|
||||
if (token) {
|
||||
const payload = isTv
|
||||
? { type: 'tv', tmdbId: String(id), season: String(s), episode: String(e) }
|
||||
: { type: 'movie', tmdbId: String(id) };
|
||||
|
||||
const proxyData = await fetchJson(`${BASE_URL}/api/download-proxy`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...HEADERS,
|
||||
@@ -63,126 +102,33 @@ export async function getStream(args) {
|
||||
'x-request-token': token,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
}).catch(() => null);
|
||||
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
|
||||
const allUrls = [];
|
||||
const subtitles = [];
|
||||
const titleTag = encodeURIComponent(isTv ? `Show_${id}_S${s}E${e}` : `Movie_${id}`);
|
||||
|
||||
const mp4 = data?.mp4Data;
|
||||
const downloadData = mp4?.downloadInfo?.data || mp4?.data?.data || mp4?.data || mp4;
|
||||
|
||||
if (downloadData) {
|
||||
const streams = downloadData.streams || downloadData.downloads || [];
|
||||
for (const item of streams) {
|
||||
if (!item?.url) continue;
|
||||
|
||||
const res = item.resolutions || item.resolution || 'Auto';
|
||||
const sizeStr = item.size ? ` (${typeof item.size === 'number' ? formatBytes(item.size) : item.size})` : '';
|
||||
const proxiedUrl = `${DL_PROXY_BASE}/${encodeURIComponent(item.url)}?n=${titleTag}`;
|
||||
const candidates = [
|
||||
{ data: proxyData?.mp4Data, type: 'mp4', label: 'MP4' },
|
||||
{ data: proxyData?.mkvData, type: 'mkv', label: 'MKV' },
|
||||
{ data: proxyData?.mkvV2Data, type: 'mkv', label: 'MKV V2' },
|
||||
];
|
||||
|
||||
for (const { data, type, label } of candidates) {
|
||||
if (data?.url && !seen.has(data.url)) {
|
||||
seen.add(data.url);
|
||||
allUrls.push({
|
||||
url: proxiedUrl,
|
||||
server: `VidVault - MP4 ${res}p${sizeStr}`.trim(),
|
||||
quality: typeof res === 'number' ? `${res}p` : res,
|
||||
type: 'mp4',
|
||||
url: data.url,
|
||||
server: `VidVault - ${label} (${data.quality || data.size || 'Auto'})`,
|
||||
quality: data.quality || 'Auto',
|
||||
type,
|
||||
headers: HEADERS,
|
||||
});
|
||||
}
|
||||
|
||||
const captions = downloadData.captions || [];
|
||||
for (const cap of captions) {
|
||||
if (!cap?.url) continue;
|
||||
const subUrl = `${SUB_PROXY_BASE}/?url=${encodeURIComponent(cap.url)}&title=${titleTag}`;
|
||||
subtitles.push({
|
||||
label: cap.lanName || cap.lan || 'Unknown',
|
||||
type: getCaptionType(cap.url),
|
||||
language: cap.lan || '',
|
||||
url: subUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const mkvFiles = data?.mkvData?.files || [];
|
||||
for (const file of mkvFiles) {
|
||||
if (!file?.url) continue;
|
||||
const sizeStr = file.size ? ` (${typeof file.size === 'number' ? formatBytes(file.size) : file.size})` : '';
|
||||
allUrls.push({
|
||||
url: file.url,
|
||||
server: `VidVault - MKV 480p${sizeStr}`.trim(),
|
||||
quality: '480p',
|
||||
type: 'mp4',
|
||||
headers: HEADERS,
|
||||
});
|
||||
}
|
||||
|
||||
const mkvV2 = data?.mkvV2Data;
|
||||
const mkvV2List = Array.isArray(mkvV2)
|
||||
? mkvV2
|
||||
: Array.isArray(mkvV2?.files)
|
||||
? mkvV2.files
|
||||
: (mkvV2?.url ? [mkvV2] : []);
|
||||
|
||||
for (const file of mkvV2List) {
|
||||
if (!file?.url) continue;
|
||||
const quality = file.quality ? String(file.quality).replace(/p$/i, '') : 'Auto';
|
||||
const sizeStr = file.size ? ` (${typeof file.size === 'number' ? formatBytes(file.size) : file.size})` : '';
|
||||
const meta = [file.country, file.language].filter(Boolean).join(' • ');
|
||||
const metaStr = meta ? ` [${meta}]` : '';
|
||||
|
||||
allUrls.push({
|
||||
url: file.url,
|
||||
server: `VidVault - MKV v2 ${quality}p${sizeStr}${metaStr}`.trim(),
|
||||
quality: `${quality}p`,
|
||||
type: 'mp4',
|
||||
headers: HEADERS,
|
||||
});
|
||||
}
|
||||
|
||||
const mkvV3 = data?.mkvV3Data;
|
||||
if (mkvV3?.downloads && Array.isArray(mkvV3.downloads)) {
|
||||
const country = mkvV3.country || '';
|
||||
const language = mkvV3.language || '';
|
||||
|
||||
for (const down of mkvV3.downloads) {
|
||||
if (Array.isArray(down?.qualities)) {
|
||||
for (const qObj of down.qualities) {
|
||||
for (const ep of qObj?.episodes || []) {
|
||||
if (!ep?.url) continue;
|
||||
const quality = qObj.quality ? String(qObj.quality).replace(/p$/i, '') : 'Auto';
|
||||
const sizeStr = ep.size ? ` (${typeof ep.size === 'number' ? formatBytes(ep.size) : ep.size})` : '';
|
||||
const meta = [ep.country || country, ep.language || language].filter(Boolean).join(' • ');
|
||||
const metaStr = meta ? ` [${meta}]` : '';
|
||||
|
||||
allUrls.push({
|
||||
url: ep.url,
|
||||
server: `VidVault - MKV v3 ${quality}p${sizeStr}${metaStr}`.trim(),
|
||||
quality: `${quality}p`,
|
||||
type: 'mp4',
|
||||
headers: HEADERS,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (down?.url) {
|
||||
const quality = down.quality ? String(down.quality).replace(/p$/i, '') : 'Auto';
|
||||
const sizeStr = down.size ? ` (${typeof down.size === 'number' ? formatBytes(down.size) : down.size})` : '';
|
||||
allUrls.push({
|
||||
url: down.url,
|
||||
server: `VidVault - MKV v3 ${quality}p${sizeStr}`.trim(),
|
||||
quality: `${quality}p`,
|
||||
type: 'mp4',
|
||||
headers: HEADERS,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allUrls.length ? { allUrls, subtitles: subtitles.length ? subtitles : undefined } : null;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getUA } from "../utils/helpers.js";
|
||||
import { getUA } from '../utils/helpers.js';
|
||||
|
||||
const API_URL = "https://api.wecollege.net";
|
||||
const SERVERS = ["miami", "atlanta", "seattle", "denver", "dallas", "tampa"];
|
||||
const API_URL = 'https://api.wecollege.net';
|
||||
const SERVERS = ['miami', 'atlanta', 'seattle', 'denver', 'dallas', 'tampa'];
|
||||
const MAGIC = [109, 118, 109, 49];
|
||||
|
||||
function imul(a, b) {
|
||||
@@ -26,10 +26,10 @@ function gFn(e, t) {
|
||||
}
|
||||
|
||||
function decryptVidy(ciphertext, seedStr, mediaId) {
|
||||
let padded = ciphertext.replace(/-/g, "+").replace(/_/g, "/");
|
||||
while (padded.length % 4) padded += "=";
|
||||
let padded = ciphertext.replace(/-/g, '+').replace(/_/g, '/');
|
||||
while (padded.length % 4) padded += '=';
|
||||
|
||||
const raw = new Uint8Array(Buffer.from(padded, "base64"));
|
||||
const raw = new Uint8Array(Buffer.from(padded, 'base64'));
|
||||
const length = raw.length;
|
||||
|
||||
let tFnv = 2166136261;
|
||||
@@ -85,7 +85,7 @@ function decryptVidy(ciphertext, seedStr, mediaId) {
|
||||
|
||||
for (let i = 0; i < MAGIC.length; i++) {
|
||||
if (raw[i] !== MAGIC[i]) {
|
||||
throw new Error("Vidy decrypt failed: magic header mismatch");
|
||||
throw new Error('Vidy decrypt failed: magic header mismatch');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,19 +93,19 @@ function decryptVidy(ciphertext, seedStr, mediaId) {
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s = null, e = null } = args;
|
||||
const { id, s, e } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const mediaId = Number(id);
|
||||
|
||||
const headers = {
|
||||
"User-Agent": getUA(),
|
||||
Referer: "https://www.vidy.st/",
|
||||
Origin: "https://www.vidy.st",
|
||||
'User-Agent': getUA(),
|
||||
Referer: 'https://www.vidy.st/',
|
||||
Origin: 'https://www.vidy.st',
|
||||
};
|
||||
|
||||
try {
|
||||
const seedUrl = new URL(`${API_URL}/seed`);
|
||||
seedUrl.searchParams.set("mediaId", String(id));
|
||||
seedUrl.searchParams.set('mediaId', String(id));
|
||||
|
||||
const seedRes = await fetch(seedUrl.toString(), {
|
||||
headers,
|
||||
@@ -120,13 +120,13 @@ export async function getStream(args) {
|
||||
for (const server of SERVERS) {
|
||||
try {
|
||||
const srcUrl = new URL(`${API_URL}/${server}/sources`);
|
||||
srcUrl.searchParams.set("mediaType", isTv ? "tv" : "movie");
|
||||
srcUrl.searchParams.set("tmdbId", String(id));
|
||||
srcUrl.searchParams.set("enc", "2");
|
||||
srcUrl.searchParams.set("seed", seed);
|
||||
srcUrl.searchParams.set('mediaType', isTv ? 'tv' : 'movie');
|
||||
srcUrl.searchParams.set('tmdbId', String(id));
|
||||
srcUrl.searchParams.set('enc', '2');
|
||||
srcUrl.searchParams.set('seed', seed);
|
||||
if (isTv) {
|
||||
srcUrl.searchParams.set("seasonId", String(s));
|
||||
srcUrl.searchParams.set("episodeId", String(e));
|
||||
srcUrl.searchParams.set('seasonId', String(s));
|
||||
srcUrl.searchParams.set('episodeId', String(e));
|
||||
}
|
||||
|
||||
const res = await fetch(srcUrl.toString(), {
|
||||
@@ -145,35 +145,40 @@ export async function getStream(args) {
|
||||
}
|
||||
|
||||
const sources = data?.sources || [];
|
||||
const playlist = data?.playlist || "";
|
||||
const playlist = data?.playlist || '';
|
||||
if (!sources.length && !playlist) continue;
|
||||
|
||||
let streamUrl = playlist;
|
||||
let quality = "1080p";
|
||||
let quality = '1080p';
|
||||
|
||||
for (const src of sources) {
|
||||
if (src?.quality === "1080p" && src?.url) {
|
||||
if (src?.quality === '1080p' && src?.url) {
|
||||
streamUrl = src.url;
|
||||
quality = "1080p";
|
||||
quality = '1080p';
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!streamUrl && sources.length) {
|
||||
streamUrl = sources[0]?.url || "";
|
||||
quality = sources[0]?.quality || "";
|
||||
streamUrl = sources[0]?.url || '';
|
||||
quality = sources[0]?.quality || '';
|
||||
}
|
||||
if (!streamUrl) continue;
|
||||
|
||||
const allUrls = [{
|
||||
url: streamUrl,
|
||||
headers,
|
||||
server: 'Vidy',
|
||||
quality,
|
||||
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
}];
|
||||
|
||||
return {
|
||||
url: streamUrl,
|
||||
headers: {
|
||||
"User-Agent": getUA(),
|
||||
Referer: "https://www.vidy.st/",
|
||||
Origin: "https://www.vidy.st",
|
||||
},
|
||||
server: "Vidy",
|
||||
headers,
|
||||
server: 'Vidy',
|
||||
quality,
|
||||
type: streamUrl.includes(".m3u8") ? "hls" : "mp4",
|
||||
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
allUrls,
|
||||
};
|
||||
} catch {
|
||||
continue;
|
||||
@@ -184,4 +189,4 @@ export async function getStream(args) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,184 +1,190 @@
|
||||
import { USER_AGENT, fetchJson } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = 'https://core.vidzee.wtf';
|
||||
const PLAYER_URL = 'https://player.vidzee.wtf';
|
||||
const SERVICES = ['dcloud', 'ipcloud', 'tik', 'v6:Hindi', '3AHindi'];
|
||||
const WASM_B64 = 'AGFzbQEAAAABPgtgAX8Bf2ACf38Bf2ABfwBgAn9/AGADf39/AGAAAGAEf39/fwBgBH9/f38Bf2ADf39/AX9gA39/fgBgAAF/Ag0BA2VudgVhYm9ydAAGAygnAgEAAAQBAAEDBAADBQAAAgMHAAEAAAQIAwEJAgIBBQAAAgEABQoBBQMBAAEGJgd/AUEAC38BQQALfwFBAAt/AUEAC38BQQALfwFBAAt/AEGA2wALB0gHB2RlY3J5cHQAJwVfX25ldwAGBV9fcGluACQHX191bnBpbgAiCV9fY29sbGVjdAAlC19fcnR0aV9iYXNlAwYGbWVtb3J5AgAIAR8MAqkBCogkJyUAIABFBEAPCyAAQRRrIgAQByMCRgRAIAAQECAAIwUjAkUQCgsLCgAgACABai0AAAsKACAAKAIEQXxxCw0AIABBFGsoAhBBAXYLDAAgACABaiACOgAAC1MBAX8gAEHs////A0sEQEHwDEGwDUH9AEEeEAAACyMARQRAEA0LIwAgAEEQahAaIgIgATYCDCACIAA2AhAgAiMBIwIQCiACEAsjA2okAyACQRRqCwoAIAAoAgRBA3ELKQAgACAAIAFBCHZqLQAAQQV0aiABQf8BcUEDdmotAAAgAUEHcXZBAXELwwEBBH8gASgCAEF8cSIDQYACSQR/IANBBHYFQR9B/P///wMgAyADQfz///8DTxsiA2drIgRBB2shAiADIARBBGt2QRBzCyEEIAEoAgghBSABKAIEIgMEQCADIAU2AggLIAUEQCAFIAM2AgQLIAEgACACQQR0IARqQQJ0aiIBKAJgRgRAIAEgBTYCYCAFRQRAIAAgAkECdGoiASgCBEF+IAR3cSEDIAEgAzYCBCADRQRAIAAgACgCAEF+IAJ3cTYCAAsLCwspAQF/IAEoAgghAyAAIAEgAnI2AgQgACADNgIIIAMgABARIAEgADYCCAsNACAAKAIAQXxxQQRqC7wCAQV/IAEoAgAhAyABQQRqIAEoAgBBfHFqIgQoAgAiAkEBcQRAIAAgBBAJIAEgA0EEaiACQXxxaiIDNgIAIAFBBGogASgCAEF8cWoiBCgCACECCyADQQJxBEAgAUEEaygCACIBKAIAIQYgACABEAkgASAGQQRqIANBfHFqIgM2AgALIAQgAkECcjYCACAEQQRrIAE2AgAgACADQXxxIgJBgAJJBH8gAkEEdgVBH0H8////AyACIAJB/P///wNPGyICZ2siA0EHayEFIAIgA0EEa3ZBEHMLIgIgBUEEdGpBAnRqKAJgIQMgAUEANgIEIAEgAzYCCCADBEAgAyABNgIECyAAIAVBBHQgAmpBAnRqIAE2AmAgACAAKAIAQQEgBXRyNgIAIAAgBUECdGoiACAAKAIEQQEgAnRyNgIEC5cBAQJ/PwAiAEEATAR/QQEgAGtAAEEASAVBAAsEQAALQbDbAEEANgIAQdDnAEEANgIAA0AgAUEXSQRAIAFBAnRBsNsAakEANgIEQQAhAANAIABBEEkEQCABQQR0IABqQQJ0QbDbAGkBADYCYCAAQQFqIQAMAQsLIAFBAWohAQwBCwtBsNsAQdTnAD8ArEIQhhAbQbDbACQACyYBAX8gAEEEayEBIABBD3FBASAAGwR/QQEFIAEoAgBBAXELGiABCxIAIAAgADYCBCAAIAA2AgggAAsnAQF/IAAQAyIBRQRAIAAoAggaDwsgASAAKAIIIgA2AgggACABEBELEgAgACABIAAoAgRBA3FyNgIEC0kBAX8gACABQQF0aiEBA0AgAyIAQQFrIQMgAARAIAEvAQAiACACLwEAIgRHBEAgACAEaw8LIAFBAmohASACQQJqIQIMAQsLQQALbgECf0EMQQYQBiIBRQRAQQxBAxAGIQELIAFBADYCACABQQA2AgQgAUEANgIIIABB/P///wNLBEBB4NcAQZDYAEETQTkQAAALIABBARAGIgJBACAA/AsAIAEgAjYCACABIAI2AgQgASAANgIIIAELjgEBAn8gAUGAAkkEfyABQQR2BUEfIAEQFSIBZ2siA0EHayECIAEgA0EEa3ZBEHMLIgEgACACQQJ0aigCBEF/IAF0cSIBBH8gACABaCACQQR0akECdGooAmAFIAAoAgBBfyACQQFqdHEiAQR/IAAgACABaCIAQQJ0aigCBGggAEEEdGpBAnRqKAJgBUEACwsLHQAgAEEBQRsgAGdrdGpBAWsgACAAQf7///8BSRsLLwAgAEH8////A0sEQEHwDEHwDUHNA0EdEAAAC0EMIABBE2pBcHFBBGsgAEEMTRsLZwECfyABKAIAIgNBfHEgAmsiBEEQTwRAIAEgAiADQQJxcjYCACABQQRqIAJqIgEgBEEEa0EBcjYCACAAIAEQDAUgASADQX5xNgIAIAFBBGogASgCAEF8cWoiACAAKAIAQX1xNgIACwswACAAIAIQGiICQQRqIAFBBGogASgCAEF8cfwKAAAgAUGk2wBPBEAgACABEBkLIAILFQAgASABKAIAQQFyNgIAIAAgARAMC48BAQJ/IAAgARAWIgIQFCIBRQRAQQQgACgCoAw/ACIBQRB0QQRrR3QgAhAVIAIgAkGAAk8bakH//wNqQYCAfHFBEHYhAyABIAMgASADShtAAEEASARAIANAAEEASARAAAsLIAAgAUEQdD8ArEIQhhAbIAAgAhAUIQELIAEoAgAaIAAgARAJIAAgASACEBcgAQuGAQEDfyABQRNqQXBxQQRrIQEgACgCoAwiAwRAIAFBEGsiBSADRgRAIAMoAgAhBCAFIQELCyACp0FwcSABayIDQRRJBEAPCyABIARBAnEgA0EIayIDQQFycjYCACABQQA2AgQgAUEANgIIIAFBBGogA2oiA0ECNgIAIAAgAzYCoAwgACABEAwLhQEBA38CQAJAAkACQAJAAkACQAJAAkAgAEEIaygCAA4IAAECAwQFBgcICw8LDwsPCyAAEB0PCyAAKAIAEAEPCyAAKAIEIgEgACgCDEECdGohAgNAIAEgAkkEQCABKAIAIgMEQCADEAELIAFBBGohAQwBCwsgACgCABABDwsgABAdDwsPCwALCQAgACgCABABC0MAIAEgACgCDE8EQEHw1QBBsNYAQfIAQSoQAAALIAAoAgQgAUECdGooAgAiAEUEQEHg1gBBsNYAQfYAQSgQAAALIAALGQBBoA4QDyQBQYDaABAPJARB4NoAEA8kBQv9AQEGfyAAIgFBCHYiAkHMzgBqLQAAIAJBlDJqLQAAQdYAbEGUMmogAEH/AXEiA0EDbmotAAAgA0EDcEECdEGAxwBqKAIAbEELdkEGcGpBAnRBjbrowserigCACICQf8BcSEAIAJBCHUhAgJAIABBAkkNACACQf8BcSEAIAJBCHYhAgNAIAAEQCAAQQF2IgYgAmpBAXRBzNIAaiIELQAAIgUgA0YEfyAELQABQQJ0QYzHAGooAgAiAkH/AXEhACACQQh1IQIgAEECSQ0DIAFBAWoPBSADIAVJBH8gBgUgAiAGaiECIAAgBmsLCyEADAELCyABDwsgASACQQAgAGtxag==';
|
||||
|
||||
let wasmInstanceCache = null;
|
||||
|
||||
async function getWasmInstance() {
|
||||
if (wasmInstanceCache) return wasmInstanceCache;
|
||||
try {
|
||||
const wasmBuffer = Buffer.from(WASM_B64, 'base64');
|
||||
const wasmModule = await WebAssembly.compile(wasmBuffer);
|
||||
|
||||
let memory;
|
||||
let exports;
|
||||
|
||||
const env = {
|
||||
abort() {
|
||||
throw new Error('WASM abort');
|
||||
}
|
||||
};
|
||||
|
||||
const res = await WebAssembly.instantiate(wasmModule, { env });
|
||||
exports = res.exports;
|
||||
memory = exports.memory;
|
||||
|
||||
const refMap = new Map();
|
||||
function pin(ptr) {
|
||||
if (ptr) {
|
||||
const count = refMap.get(ptr);
|
||||
if (count) refMap.set(ptr, count + 1);
|
||||
else refMap.set(exports.__pin(ptr), 1);
|
||||
}
|
||||
return ptr;
|
||||
}
|
||||
function unpin(ptr) {
|
||||
if (ptr) {
|
||||
const count = refMap.get(ptr);
|
||||
if (count === 1) {
|
||||
exports.__unpin(ptr);
|
||||
refMap.delete(ptr);
|
||||
} else if (count) {
|
||||
refMap.set(ptr, count - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wasmInstanceCache = {
|
||||
decrypt(encryptedBytes, keyStr) {
|
||||
let dataPtr = 0;
|
||||
let keyPtr = 0;
|
||||
try {
|
||||
const len = encryptedBytes.length;
|
||||
const pinPtr = exports.__pin(exports.__new(len, 1)) >>> 0;
|
||||
dataPtr = exports.__new(12, 6) >>> 0;
|
||||
|
||||
const view = new DataView(memory.buffer);
|
||||
view.setUint32(dataPtr, pinPtr, true);
|
||||
view.setUint32(dataPtr + 4, pinPtr, true);
|
||||
view.setUint32(dataPtr + 8, len, true);
|
||||
new Uint8Array(memory.buffer, pinPtr, len).set(encryptedBytes);
|
||||
exports.__unpin(pinPtr);
|
||||
|
||||
pin(dataPtr);
|
||||
|
||||
const kLen = keyStr.length;
|
||||
keyPtr = exports.__new(kLen << 1, 2) >>> 0;
|
||||
const u16View = new Uint16Array(memory.buffer);
|
||||
for (let i = 0; i < kLen; i++) {
|
||||
u16View[(keyPtr >>> 1) + i] = keyStr.charCodeAt(i);
|
||||
}
|
||||
|
||||
const resPtr = exports.decrypt(dataPtr, keyPtr) >>> 0;
|
||||
if (!resPtr) return new Uint8Array(0);
|
||||
|
||||
const resView = new DataView(memory.buffer);
|
||||
const outPtr = resView.getUint32(resPtr + 4, true);
|
||||
const outLen = resView.getUint32(resPtr + 8, true);
|
||||
return new Uint8Array(memory.buffer, outPtr, outLen).slice();
|
||||
} finally {
|
||||
unpin(dataPtr);
|
||||
}
|
||||
}
|
||||
};
|
||||
return wasmInstanceCache;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function requestEndpoint(url, headers) {
|
||||
try {
|
||||
return await fetchJson(url, {
|
||||
headers,
|
||||
signal: AbortSignal.timeout(6000)
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveServiceStream(service, isMovie, id, s, e, headers) {
|
||||
const directEndpoint = isMovie
|
||||
? `${BASE_URL}/streams/movie/${id}?s=${service}`
|
||||
: `${BASE_URL}/streams/tv/${id}/${s || 1}/${e || 1}?s=${service}`;
|
||||
|
||||
let res = await requestEndpoint(directEndpoint, headers);
|
||||
|
||||
if (!res || !res.url) {
|
||||
const encEndpoint = `${directEndpoint}&e=1`;
|
||||
const encRes = await requestEndpoint(encEndpoint, headers);
|
||||
|
||||
if (encRes && encRes.c) {
|
||||
const instance = await getWasmInstance();
|
||||
if (instance) {
|
||||
const encryptedBytes = new Uint8Array(Buffer.from(encRes.c, 'base64'));
|
||||
const decryptedBytes = instance.decrypt(encryptedBytes, 'player.vidzee.wtf');
|
||||
if (decryptedBytes.length > 0) {
|
||||
const str = new TextDecoder().decode(decryptedBytes);
|
||||
try {
|
||||
res = JSON.parse(str);
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (res && res.url) {
|
||||
const format = res.url.includes('.m3u8') ? 'hls' : 'mp4';
|
||||
return {
|
||||
url: res.url,
|
||||
server: `VidZee (${service})`,
|
||||
quality: res.language || 'Auto',
|
||||
type: format,
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Referer': `${PLAYER_URL}/`,
|
||||
'Origin': PLAYER_URL,
|
||||
...(res.headers || {})
|
||||
}
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, server, clientIP } = args;
|
||||
const isTv = s != null && e != null;
|
||||
const isMovie = !isTv;
|
||||
|
||||
const headers = {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Referer': `${PLAYER_URL}/`,
|
||||
'Origin': PLAYER_URL,
|
||||
...(clientIP && { 'X-Forwarded-For': clientIP })
|
||||
};
|
||||
|
||||
let targets = SERVICES;
|
||||
if (server && server !== 'all') {
|
||||
const clean = server.toLowerCase().replace('vidzee (', '').replace(')', '').replace('vidzee', '').trim();
|
||||
targets = SERVICES.filter(srv => srv.toLowerCase().includes(clean));
|
||||
if (!targets.length) targets = SERVICES;
|
||||
}
|
||||
|
||||
const settled = await Promise.allSettled(
|
||||
targets.map(service => resolveServiceStream(service, isMovie, id, s, e, headers))
|
||||
);
|
||||
|
||||
const allUrls = [];
|
||||
for (const r of settled) {
|
||||
if (r.status === 'fulfilled' && r.value) {
|
||||
allUrls.push(r.value);
|
||||
}
|
||||
}
|
||||
|
||||
return allUrls.length ? { allUrls } : null;
|
||||
}
|
||||
|
||||
export async function getSources() {
|
||||
return SERVICES.map(s => `VidZee (${s})`);
|
||||
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = 'https://core.vidzee.wtf';
|
||||
const PLAYER_URL = 'https://player.vidzee.wtf';
|
||||
const SERVICES = ['dcloud', 'ipcloud', 'tik', 'v6:Hindi', '3AHindi'];
|
||||
const WASM_B64 = 'AGFzbQEAAAABPgtgAX8Bf2ACf38Bf2ABfwBgAn9/AGADf39/AGAAAGAEf39/fwBgBH9/f38Bf2ADf39/AX9gA39/fgBgAAF/Ag0BA2VudgVhYm9ydAAGAygnAgEAAAQBAAEDBAADBQAAAgMHAAEAAAQIAwEJAgIBBQAAAgEABQoBBQMBAAEGJgd/AUEAC38BQQALfwFBAAt/AUEAC38BQQALfwFBAAt/AEGA2wALB0gHB2RlY3J5cHQAJwVfX25ldwAGBV9fcGluACQHX191bnBpbgAiCV9fY29sbGVjdAAlC19fcnR0aV9iYXNlAwYGbWVtb3J5AgAIAR8MAqkBCogkJyUAIABFBEAPCyAAQRRrIgAQByMCRgRAIAAQECAAIwUjAkUQCgsLCgAgACABai0AAAsKACAAKAIEQXxxCw0AIABBFGsoAhBBAXYLDAAgACABaiACOgAAC1MBAX8gAEHs////A0sEQEHwDEGwDUH9AEEeEAAACyMARQRAEA0LIwAgAEEQahAaIgIgATYCDCACIAA2AhAgAiMBIwIQCiACEAsjA2okAyACQRRqCwoAIAAoAgRBA3ELKQAgACAAIAFBCHZqLQAAQQV0aiABQf8BcUEDdmotAAAgAUEHcXZBAXELwwEBBH8gASgCAEF8cSIDQYACSQR/IANBBHYFQR9B/P///wMgAyADQfz///8DTxsiA2drIgRBB2shAiADIARBBGt2QRBzCyEEIAEoAgghBSABKAIEIgMEQCADIAU2AggLIAUEQCAFIAM2AgQLIAEgACACQQR0IARqQQJ0aiIBKAJgRgRAIAEgBTYCYCAFRQRAIAAgAkECdGoiASgCBEF+IAR3cSEDIAEgAzYCBCADRQRAIAAgACgCAEF+IAJ3cTYCAAsLCwspAQF/IAEoAgghAyAAIAEgAnI2AgQgACADNgIIIAMgABARIAEgADYCCAsNACAAKAIAQXxxQQRqC7wCAQV/IAEoAgAhAyABQQRqIAEoAgBBfHFqIgQoAgAiAkEBcQRAIAAgBBAJIAEgA0EEaiACQXxxaiIDNgIAIAFBBGogASgCAEF8cWoiBCgCACECCyADQQJxBEAgAUEEaygCACIBKAIAIQYgACABEAkgASAGQQRqIANBfHFqIgM2AgALIAQgAkECcjYCACAEQQRrIAE2AgAgACADQXxxIgJBgAJJBH8gAkEEdgVBH0H8////AyACIAJB/P///wNPGyICZ2siA0EHayEFIAIgA0EEa3ZBEHMLIgIgBUEEdGpBAnRqKAJgIQMgAUEANgIEIAEgAzYCCCADBEAgAyABNgIECyAAIAVBBHQgAmpBAnRqIAE2AmAgACAAKAIAQQEgBXRyNgIAIAAgBUECdGoiACAAKAIEQQEgAnRyNgIEC5cBAQJ/PwAiAEEATAR/QQEgAGtAAEEASAVBAAsEQAALQbDbAEEANgIAQdDnAEEANgIAA0AgAUEXSQRAIAFBAnRBsNsAakEANgIEQQAhAANAIABBEEkEQCABQQR0IABqQQJ0QbDbAGkBADYCYCAAQQFqIQAMAQsLIAFBAWohAQwBCwtBsNsAQdTnAD8ArEIQhhAbQbDbACQACyYBAX8gAEEEayEBIABBD3FBASAAGwR/QQEFIAEoAgBBAXELGiABCxIAIAAgADYCBCAAIAA2AgggAAsnAQF/IAAQAyIBRQRAIAAoAggaDwsgASAAKAIIIgA2AgggACABEBELEgAgACABIAAoAgRBA3FyNgIEC0kBAX8gACABQQF0aiEBA0AgAyIAQQFrIQMgAARAIAEvAQAiACACLwEAIgRHBEAgACAEaw8LIAFBAmohASACQQJqIQIMAQsLQQALbgECf0EMQQYQBiIBRQRAQQxBAxAGIQELIAFBADYCACABQQA2AgQgAUEANgIIIABB/P///wNLBEBB4NcAQZDYAEETQTkQAAALIABBARAGIgJBACAA/AsAIAEgAjYCACABIAI2AgQgASAANgIIIAELjgEBAn8gAUGAAkkEfyABQQR2BUEfIAEQFSIBZ2siA0EHayECIAEgA0EEa3ZBEHMLIgEgACACQQJ0aigCBEF/IAF0cSIBBH8gACABaCACQQR0akECdGooAmAFIAAoAgBBfyACQQFqdHEiAQR/IAAgACABaCIAQQJ0aigCBGggAEEEdGpBAnRqKAJgBUEACwsLHQAgAEEBQRsgAGdrdGpBAWsgACAAQf7///8BSRsLLwAgAEH8////A0sEQEHwDEHwDUHNA0EdEAAAC0EMIABBE2pBcHFBBGsgAEEMTRsLZwECfyABKAIAIgNBfHEgAmsiBEEQTwRAIAEgAiADQQJxcjYCACABQQRqIAJqIgEgBEEEa0EBcjYCACAAIAEQDAUgASADQX5xNgIAIAFBBGogASgCAEF8cWoiACAAKAIAQX1xNgIACwswACAAIAIQGiICQQRqIAFBBGogASgCAEF8cfwKAAAgAUGk2wBPBEAgACABEBkLIAILFQAgASABKAIAQQFyNgIAIAAgARAMC48BAQJ/IAAgARAWIgIQFCIBRQRAQQQgACgCoAw/ACIBQRB0QQRrR3QgAhAVIAIgAkGAAk8bakH//wNqQYCAfHFBEHYhAyABIAMgASADShtAAEEASARAIANAAEEASARAAAsLIAAgAUEQdD8ArEIQhhAbIAAgAhAUIQELIAEoAgAaIAAgARAJIAAgASACEBcgAQuGAQEDfyABQRNqQXBxQQRrIQEgACgCoAwiAwRAIAFBEGsiBSADRgRAIAMoAgAhBCAFIQELCyACp0FwcSABayIDQRRJBEAPCyABIARBAnEgA0EIayIDQQFycjYCACABQQA2AgQgAUEANgIIIAFBBGogA2oiA0ECNgIAIAAgAzYCoAwgACABEAwLhQEBA38CQAJAAkACQAJAAkACQAJAAkAgAEEIaygCAA4IAAECAwQFBgcICw8LDwsPCyAAEB0PCyAAKAIAEAEPCyAAKAIEIgEgACgCDEECdGohAgNAIAEgAkkEQCABKAIAIgMEQCADEAELIAFBBGohAQwBCwsgACgCABABDwsgABAdDwsPCwALCQAgACgCABABC0MAIAEgACgCDE8EQEHw1QBBsNYAQfIAQSoQAAALIAAoAgQgAUECdGooAgAiAEUEQEHg1gBBsNYAQfYAQSgQAAALIAALGQBBoA4QDyQBQYDaABAPJARB4NoAEA8kBQv9AQEGfyAAIgFBCHYiAkHMzgBqLQAAIAJBlDJqLQAAQdYAbEGUMmogAEH/AXEiA0EDbmotAAAgA0EDcEECdEGAxwBqKAIAbEELdkEGcGpBAnRBjbrowserigCACICQf8BcSEAIAJBCHUhAgJAIABBAkkNACACQf8BcSEAIAJBCHYhAgNAIAAEQCAAQQF2IgYgAmpBAXRBzNIAaiIELQAAIgUgA0YEfyAELQABQQJ0QYzHAGooAgAiAkH/AXEhACACQQh1IQIgAEECSQ0DIAFBAWoPBSADIAVJBH8gBgUgAiAGaiECIAAgBmsLCyEADAELCyABDwsgASACQQAgAGtxag==';
|
||||
|
||||
let wasmInstanceCache = null;
|
||||
|
||||
async function getWasmInstance() {
|
||||
if (wasmInstanceCache) return wasmInstanceCache;
|
||||
try {
|
||||
const wasmBuffer = Buffer.from(WASM_B64, 'base64');
|
||||
const wasmModule = await WebAssembly.compile(wasmBuffer);
|
||||
|
||||
let memory;
|
||||
let exports;
|
||||
|
||||
const env = {
|
||||
abort() {
|
||||
throw new Error('WASM abort');
|
||||
},
|
||||
};
|
||||
|
||||
const res = await WebAssembly.instantiate(wasmModule, { env });
|
||||
exports = res.exports;
|
||||
memory = exports.memory;
|
||||
|
||||
const refMap = new Map();
|
||||
function pin(ptr) {
|
||||
if (ptr) {
|
||||
const count = refMap.get(ptr);
|
||||
if (count) refMap.set(ptr, count + 1);
|
||||
else refMap.set(exports.__pin(ptr), 1);
|
||||
}
|
||||
return ptr;
|
||||
}
|
||||
function unpin(ptr) {
|
||||
if (ptr) {
|
||||
const count = refMap.get(ptr);
|
||||
if (count === 1) {
|
||||
refMap.delete(ptr);
|
||||
exports.__unpin(ptr);
|
||||
} else if (count > 1) {
|
||||
refMap.set(ptr, count - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wasmInstanceCache = {
|
||||
decrypt(encryptedBytes, keyStr) {
|
||||
let dataPtr = 0;
|
||||
let keyPtr = 0;
|
||||
try {
|
||||
const len = encryptedBytes.length;
|
||||
const pinPtr = exports.__pin(exports.__new(len, 1)) >>> 0;
|
||||
dataPtr = exports.__new(12, 6) >>> 0;
|
||||
|
||||
const view = new DataView(memory.buffer);
|
||||
view.setUint32(dataPtr, pinPtr, true);
|
||||
view.setUint32(dataPtr + 4, pinPtr, true);
|
||||
view.setUint32(dataPtr + 8, len, true);
|
||||
new Uint8Array(memory.buffer, pinPtr, len).set(encryptedBytes);
|
||||
exports.__unpin(pinPtr);
|
||||
|
||||
pin(dataPtr);
|
||||
|
||||
const kLen = keyStr.length;
|
||||
keyPtr = exports.__new(kLen << 1, 2) >>> 0;
|
||||
const u16View = new Uint16Array(memory.buffer);
|
||||
for (let i = 0; i < kLen; i++) {
|
||||
u16View[(keyPtr >>> 1) + i] = keyStr.charCodeAt(i);
|
||||
}
|
||||
|
||||
const resPtr = exports.decrypt(dataPtr, keyPtr) >>> 0;
|
||||
if (!resPtr) return new Uint8Array(0);
|
||||
|
||||
const resView = new DataView(memory.buffer);
|
||||
const outPtr = resView.getUint32(resPtr + 4, true);
|
||||
const outLen = resView.getUint32(resPtr + 8, true);
|
||||
return new Uint8Array(memory.buffer, outPtr, outLen).slice();
|
||||
} finally {
|
||||
unpin(dataPtr);
|
||||
}
|
||||
},
|
||||
};
|
||||
return wasmInstanceCache;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function requestEndpoint(url, headers) {
|
||||
try {
|
||||
return await fetchJson(url, {
|
||||
headers,
|
||||
signal: AbortSignal.timeout(6000),
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveServiceStream(service, isMovie, id, s, e, headers) {
|
||||
const directEndpoint = isMovie
|
||||
? `${BASE_URL}/streams/movie/${id}?s=${service}`
|
||||
: `${BASE_URL}/streams/tv/${id}/${s || 1}/${e || 1}?s=${service}`;
|
||||
|
||||
let res = await requestEndpoint(directEndpoint, headers);
|
||||
|
||||
if (!res || !res.url) {
|
||||
const encEndpoint = `${directEndpoint}&e=1`;
|
||||
const encRes = await requestEndpoint(encEndpoint, headers);
|
||||
|
||||
if (encRes && encRes.c) {
|
||||
const instance = await getWasmInstance();
|
||||
if (instance) {
|
||||
const encryptedBytes = new Uint8Array(Buffer.from(encRes.c, 'base64'));
|
||||
const decryptedBytes = instance.decrypt(encryptedBytes, 'player.vidzee.wtf');
|
||||
if (decryptedBytes.length > 0) {
|
||||
const str = new TextDecoder().decode(decryptedBytes);
|
||||
try {
|
||||
res = JSON.parse(str);
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (res && res.url) {
|
||||
const format = res.url.includes('.m3u8') ? 'hls' : 'mp4';
|
||||
return {
|
||||
url: res.url,
|
||||
server: `VidZee (${service})`,
|
||||
quality: res.language || 'Auto',
|
||||
type: format,
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
Referer: `${PLAYER_URL}/`,
|
||||
Origin: PLAYER_URL,
|
||||
...(res.headers || {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, server, clientIP } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const isMovie = !isTv;
|
||||
|
||||
const headers = {
|
||||
'User-Agent': USER_AGENT,
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
Referer: `${PLAYER_URL}/`,
|
||||
Origin: PLAYER_URL,
|
||||
...(clientIP && { 'X-Forwarded-For': clientIP }),
|
||||
};
|
||||
|
||||
let targets = SERVICES;
|
||||
if (server && server !== 'all') {
|
||||
const clean = server.toLowerCase().replace('vidzee (', '').replace(')', '').replace('vidzee', '').trim();
|
||||
targets = SERVICES.filter(srv => srv.toLowerCase().includes(clean));
|
||||
if (!targets.length) targets = SERVICES;
|
||||
}
|
||||
|
||||
const settled = await Promise.allSettled(
|
||||
targets.map(service => resolveServiceStream(service, isMovie, id, s, e, headers))
|
||||
);
|
||||
|
||||
const allUrls = [];
|
||||
for (const r of settled) {
|
||||
if (r.status === 'fulfilled' && r.value) {
|
||||
allUrls.push(r.value);
|
||||
}
|
||||
}
|
||||
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getSources() {
|
||||
return SERVICES.map(s => `VidZee (${s})`);
|
||||
}
|
||||
@@ -1,114 +1,124 @@
|
||||
import { fetchText, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = 'https://vixsrc.to';
|
||||
|
||||
const HEADERS = {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Accept': 'application/json, text/javascript, */*; q=0.01',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'Referer': `${BASE_URL}/`,
|
||||
'Origin': BASE_URL
|
||||
};
|
||||
|
||||
function extract(regex, text) {
|
||||
const m = text?.match(regex);
|
||||
if (!m) return null;
|
||||
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 }) {
|
||||
try {
|
||||
const apiUrl = s && e
|
||||
? `${BASE_URL}/api/tv/${id}/${s}/${e}`
|
||||
: `${BASE_URL}/api/movie/${id}`;
|
||||
|
||||
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, { 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 && 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, { 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;
|
||||
|
||||
if (playlist.startsWith('/')) {
|
||||
playlist = `${BASE_URL}${playlist}`;
|
||||
}
|
||||
|
||||
const defaultLang = extract(/lang(?:uage)?["']\s*:\s*["']([a-z]{2,5})/i, html) || 'en';
|
||||
const rawMasterUrl = `${playlist}${playlist.includes('?') ? '&' : '?'}token=${token}&expires=${expires}&h=1&lang=${defaultLang}`;
|
||||
|
||||
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(masterProxy, { headers: { ...HEADERS, 'Referer': embedUrl } }).catch(() => null);
|
||||
if (!playlistText?.includes('#EXTM3U')) return null;
|
||||
|
||||
const lines = playlistText.split('\n');
|
||||
const audioTracks = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('#EXT-X-MEDIA') && line.includes('TYPE=AUDIO')) {
|
||||
const name = line.match(/NAME=["']([^"']+)["']/)?.[1] || 'Default';
|
||||
const rendition = line.match(/rendition=([a-zA-Z0-9_-]+)/)?.[1]
|
||||
|| line.match(/LANGUAGE=["']([^"']+)["']/)?.[1]
|
||||
|| name;
|
||||
const key = rendition.toLowerCase();
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
audioTracks.push({ name, lang: rendition });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const allUrls = [];
|
||||
|
||||
if (audioTracks.length > 0) {
|
||||
for (const track of audioTracks) {
|
||||
const streamUrl = `${playlist}${playlist.includes('?') ? '&' : '?'}token=${token}&expires=${expires}&h=1&lang=${track.lang}`;
|
||||
allUrls.push({
|
||||
url: streamUrl,
|
||||
server: `VixSrc (${track.name})`,
|
||||
skipProxy: true,
|
||||
headers: {
|
||||
...HEADERS,
|
||||
'Referer': `${BASE_URL}/`
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
allUrls.push({
|
||||
url: rawMasterUrl,
|
||||
server: 'VixSrc',
|
||||
skipProxy: true,
|
||||
headers: {
|
||||
...HEADERS,
|
||||
'Referer': `${BASE_URL}/`
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { allUrls };
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
import { fetchText, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = 'https://vixsrc.to';
|
||||
const HEADERS = {
|
||||
'User-Agent': USER_AGENT,
|
||||
Accept: 'application/json, text/javascript, */*; q=0.01',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
};
|
||||
|
||||
function extract(regex, text) {
|
||||
const m = text?.match(regex);
|
||||
if (!m) return null;
|
||||
let val = m[1];
|
||||
val = val.replace(/\\u0026/gi, '&');
|
||||
val = val.replace(/\\\//g, '/');
|
||||
val = val.replace(/\\/g, '');
|
||||
return val;
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, absoluteBase } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
|
||||
try {
|
||||
const apiUrl = isTv
|
||||
? `${BASE_URL}/api/tv/${id}/${s}/${e}`
|
||||
: `${BASE_URL}/api/movie/${id}`;
|
||||
|
||||
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, { 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 && 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, { 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;
|
||||
|
||||
if (playlist.startsWith('/')) {
|
||||
playlist = `${BASE_URL}${playlist}`;
|
||||
}
|
||||
|
||||
const defaultLang = extract(/lang(?:uage)?["']\s*:\s*["']([a-z]{2,5})/i, html) || 'en';
|
||||
const rawMasterUrl = `${playlist}${playlist.includes('?') ? '&' : '?'}token=${token}&expires=${expires}&h=1&lang=${defaultLang}`;
|
||||
|
||||
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(masterProxy, { headers: { ...HEADERS, Referer: embedUrl } }).catch(() => null);
|
||||
if (!playlistText?.includes('#EXTM3U')) return null;
|
||||
|
||||
const lines = playlistText.split('\n');
|
||||
const audioTracks = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('#EXT-X-MEDIA') && line.includes('TYPE=AUDIO')) {
|
||||
const name = line.match(/NAME=["']([^"']+)["']/)?.[1] || 'Default';
|
||||
const rendition = line.match(/rendition=([a-zA-Z0-9_-]+)/)?.[1]
|
||||
|| line.match(/LANGUAGE=["']([^"']+)["']/)?.[1]
|
||||
|| name;
|
||||
const key = rendition.toLowerCase();
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
audioTracks.push({ name, lang: rendition });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const allUrls = [];
|
||||
|
||||
if (audioTracks.length > 0) {
|
||||
for (const track of audioTracks) {
|
||||
const streamUrl = `${playlist}${playlist.includes('?') ? '&' : '?'}token=${token}&expires=${expires}&h=1&lang=${track.lang}`;
|
||||
allUrls.push({
|
||||
url: streamUrl,
|
||||
server: `VixSrc (${track.name})`,
|
||||
quality: 'Auto',
|
||||
type: 'hls',
|
||||
skipProxy: true,
|
||||
headers: {
|
||||
...HEADERS,
|
||||
Referer: `${BASE_URL}/`,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
allUrls.push({
|
||||
url: rawMasterUrl,
|
||||
server: 'VixSrc',
|
||||
quality: 'Auto',
|
||||
type: 'hls',
|
||||
skipProxy: true,
|
||||
headers: {
|
||||
...HEADERS,
|
||||
Referer: `${BASE_URL}/`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
import { fetchJson, fetchText } from '../utils/helpers.js';
|
||||
|
||||
const API_URL = "https://raw.githubusercontent.com/Watchout2025/api/refs/heads/main/hls/";
|
||||
const API_URL = 'https://raw.githubusercontent.com/Watchout2025/api/refs/heads/main/hls/';
|
||||
const HEADERS = {
|
||||
Origin: 'https://vidout.pages.dev',
|
||||
Referer: 'https://vidout.pages.dev',
|
||||
};
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const type = isTv ? 'tv' : 'movie';
|
||||
|
||||
@@ -10,37 +15,46 @@ export async function getStream({ id, s, e }) {
|
||||
? `${API_URL}tv/${id}/S${s}.json`
|
||||
: `${API_URL}movie/${id}`;
|
||||
|
||||
const headers = {
|
||||
'Origin': 'https://vidout.pages.dev',
|
||||
'Referer': 'https://vidout.pages.dev'
|
||||
};
|
||||
|
||||
if (type === 'movie') {
|
||||
const data = await fetchText(targetUrl).catch(() => null);
|
||||
if (!data || !data.includes('://')) return null;
|
||||
|
||||
return {
|
||||
url: data.trim(),
|
||||
headers,
|
||||
allUrls: [{
|
||||
url: data.trim(),
|
||||
server: 'Watchout - Direct',
|
||||
headers
|
||||
}]
|
||||
};
|
||||
} else {
|
||||
const data = await fetchJson(targetUrl).catch(() => null);
|
||||
const streamUrl = data?.[String(e)] || null;
|
||||
if (!streamUrl) return null;
|
||||
const streamUrl = data.trim();
|
||||
const isHls = streamUrl.includes('.m3u8') || streamUrl.includes('.txt') || streamUrl.includes('master') || !streamUrl.includes('.mp4');
|
||||
const allUrls = [{
|
||||
url: streamUrl,
|
||||
server: 'Watchout - Direct',
|
||||
quality: 'Auto',
|
||||
type: isHls ? 'hls' : 'mp4',
|
||||
headers: HEADERS,
|
||||
}];
|
||||
|
||||
return {
|
||||
url: streamUrl,
|
||||
headers,
|
||||
allUrls: [{
|
||||
url: streamUrl,
|
||||
server: 'Watchout - Direct',
|
||||
headers
|
||||
}]
|
||||
headers: HEADERS,
|
||||
allUrls,
|
||||
};
|
||||
} else {
|
||||
let data = await fetchJson(targetUrl).catch(() => null);
|
||||
if (!data) {
|
||||
data = await fetchJson(`${API_URL}tv/${id}/s${s}.json`).catch(() => null);
|
||||
}
|
||||
const streamUrl = data?.[String(e)] || null;
|
||||
if (!streamUrl) return null;
|
||||
|
||||
const isHls = streamUrl.includes('.m3u8') || streamUrl.includes('.txt') || streamUrl.includes('master') || !streamUrl.includes('.mp4');
|
||||
const allUrls = [{
|
||||
url: streamUrl,
|
||||
server: 'Watchout - Direct',
|
||||
quality: 'Auto',
|
||||
type: isHls ? 'hls' : 'mp4',
|
||||
headers: HEADERS,
|
||||
}];
|
||||
|
||||
return {
|
||||
url: streamUrl,
|
||||
headers: HEADERS,
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getTmdbMetadata, resolveTmdbKey } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = 'https://www.films365.org';
|
||||
const HEADERS = {
|
||||
Authorization: 'Bearer 79a02956be35835728a044b11e2ae793149d45fb2c89cb6d029ec01aac19bfdb',
|
||||
@@ -5,125 +7,122 @@ const HEADERS = {
|
||||
'User-Agent': 'MovieDownloader/1.0',
|
||||
};
|
||||
|
||||
async function resolveTitle(sdk, id, isTv) {
|
||||
const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY;
|
||||
if (!apiKey) return null;
|
||||
const type = isTv ? 'tv' : 'movie';
|
||||
try {
|
||||
const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}`, {
|
||||
signal: AbortSignal.timeout(6000),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
return isTv ? data.name : data.title;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e, sdk }) {
|
||||
const isTv = Boolean(s && e);
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, sdk } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
const targetType = isTv ? 'tv' : 'movie';
|
||||
const apiKey = resolveTmdbKey(args, sdk);
|
||||
|
||||
const title = await resolveTitle(sdk, id, isTv);
|
||||
const meta = await getTmdbMetadata(id, isTv, apiKey);
|
||||
const title = meta?.title;
|
||||
const query = title || String(id);
|
||||
|
||||
const searchRes = await fetch(`${BASE_URL}/api/mobile/search`, {
|
||||
method: 'POST',
|
||||
headers: HEADERS,
|
||||
body: JSON.stringify({ query }),
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
try {
|
||||
const searchRes = await fetch(`${BASE_URL}/api/mobile/search`, {
|
||||
method: 'POST',
|
||||
headers: HEADERS,
|
||||
body: JSON.stringify({ query }),
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
|
||||
if (!searchRes.ok) return null;
|
||||
const searchJson = await searchRes.json();
|
||||
const resultsObj = searchJson?.results;
|
||||
if (!resultsObj) return null;
|
||||
if (!searchRes.ok) return null;
|
||||
const searchJson = await searchRes.json();
|
||||
const resultsObj = searchJson?.results;
|
||||
if (!resultsObj) return null;
|
||||
|
||||
const items = resultsObj.all || (targetType === 'tv' ? resultsObj.tvs : resultsObj.movies) || [];
|
||||
if (!Array.isArray(items) || items.length === 0) return null;
|
||||
const items = resultsObj.all || (targetType === 'tv' ? resultsObj.tvs : resultsObj.movies) || [];
|
||||
if (!Array.isArray(items) || items.length === 0) return null;
|
||||
|
||||
const cleanSearchTitle = query.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
let matchedItem = null;
|
||||
const cleanSearchTitle = query.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
let matchedItem = null;
|
||||
|
||||
for (const item of items) {
|
||||
const itemType = String(item?.type || '').toLowerCase();
|
||||
if (itemType && itemType !== targetType) continue;
|
||||
for (const item of items) {
|
||||
const itemType = String(item?.type || '').toLowerCase();
|
||||
if (itemType && itemType !== targetType) continue;
|
||||
|
||||
const itemTitle = String(item?.title || '');
|
||||
const cleanItemTitle = itemTitle.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
const itemTitle = String(item?.title || '');
|
||||
const cleanItemTitle = itemTitle.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
|
||||
if (cleanItemTitle === cleanSearchTitle || cleanItemTitle.includes(cleanSearchTitle)) {
|
||||
matchedItem = item;
|
||||
break;
|
||||
if (cleanItemTitle === cleanSearchTitle || cleanItemTitle.includes(cleanSearchTitle)) {
|
||||
matchedItem = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchedItem) {
|
||||
matchedItem = items.find(i => String(i?.type || '').toLowerCase() === targetType) || items[0];
|
||||
}
|
||||
|
||||
const itemId = matchedItem?.id || matchedItem?.tmdbId;
|
||||
if (!itemId) return null;
|
||||
|
||||
const detailsRes = await fetch(`${BASE_URL}/api/mobile/details?id=${itemId}&type=${targetType}`, {
|
||||
headers: HEADERS,
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
|
||||
if (!detailsRes.ok) return null;
|
||||
const detailsJson = await detailsRes.json();
|
||||
const data = detailsJson?.data;
|
||||
if (!data) return null;
|
||||
|
||||
const spoken = Array.isArray(data.spokenLanguages)
|
||||
? data.spokenLanguages.map(l => String(l).trim()).filter(Boolean)
|
||||
: [];
|
||||
const langSuffix = spoken.length > 0 ? ` · ${spoken.join(', ')}` : '';
|
||||
|
||||
const allUrls = [];
|
||||
|
||||
if (!isTv) {
|
||||
const streamUrl = data.downloadUrl || data.videoUrl;
|
||||
if (streamUrl && typeof streamUrl === 'string') {
|
||||
allUrls.push({
|
||||
url: streamUrl.trim(),
|
||||
label: `X-Downloader${langSuffix}`,
|
||||
headers: {
|
||||
'User-Agent': HEADERS['User-Agent'],
|
||||
},
|
||||
});
|
||||
if (!matchedItem) {
|
||||
matchedItem = items.find(i => String(i?.type || '').toLowerCase() === targetType) || items[0];
|
||||
}
|
||||
} else {
|
||||
const seasonNum = Number(s);
|
||||
const episodeNum = Number(e);
|
||||
const seasons = Array.isArray(data.seasons) ? data.seasons : [];
|
||||
const targetSeason = seasons.find(sn => Number(sn.seasonNumber) === seasonNum);
|
||||
|
||||
if (targetSeason) {
|
||||
const episodes = Array.isArray(targetSeason.episodes) ? targetSeason.episodes : [];
|
||||
const targetEp = episodes.find(ep => Number(ep.episodeNumber) === episodeNum);
|
||||
const itemId = matchedItem?.id || matchedItem?.tmdbId;
|
||||
if (!itemId) return null;
|
||||
|
||||
if (targetEp) {
|
||||
const streamUrl = targetEp.downloadUrl || targetEp.videoUrl;
|
||||
if (streamUrl && typeof streamUrl === 'string') {
|
||||
allUrls.push({
|
||||
url: streamUrl.trim(),
|
||||
label: `X-Downloader (S${seasonNum}E${episodeNum})${langSuffix}`,
|
||||
headers: {
|
||||
'User-Agent': HEADERS['User-Agent'],
|
||||
},
|
||||
});
|
||||
const detailsRes = await fetch(`${BASE_URL}/api/mobile/details?id=${itemId}&type=${targetType}`, {
|
||||
headers: HEADERS,
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
|
||||
if (!detailsRes.ok) return null;
|
||||
const detailsJson = await detailsRes.json();
|
||||
const data = detailsJson?.data;
|
||||
if (!data) return null;
|
||||
|
||||
const spoken = Array.isArray(data.spokenLanguages)
|
||||
? data.spokenLanguages.map(l => String(l).trim()).filter(Boolean)
|
||||
: [];
|
||||
const langSuffix = spoken.length > 0 ? ` · ${spoken.join(', ')}` : '';
|
||||
|
||||
const allUrls = [];
|
||||
|
||||
if (!isTv) {
|
||||
const streamUrl = data.downloadUrl || data.videoUrl;
|
||||
if (streamUrl && typeof streamUrl === 'string') {
|
||||
allUrls.push({
|
||||
url: streamUrl.trim(),
|
||||
label: `X-Downloader${langSuffix}`,
|
||||
server: `X-Downloader${langSuffix}`,
|
||||
quality: 'Auto',
|
||||
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: {
|
||||
'User-Agent': HEADERS['User-Agent'],
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const seasonNum = Number(s);
|
||||
const episodeNum = Number(e);
|
||||
const seasons = Array.isArray(data.seasons) ? data.seasons : [];
|
||||
const targetSeason = seasons.find(sn => Number(sn.seasonNumber) === seasonNum);
|
||||
|
||||
if (targetSeason) {
|
||||
const episodes = Array.isArray(targetSeason.episodes) ? targetSeason.episodes : [];
|
||||
const targetEp = episodes.find(ep => Number(ep.episodeNumber) === episodeNum);
|
||||
|
||||
if (targetEp) {
|
||||
const streamUrl = targetEp.downloadUrl || targetEp.videoUrl;
|
||||
if (streamUrl && typeof streamUrl === 'string') {
|
||||
allUrls.push({
|
||||
url: streamUrl.trim(),
|
||||
label: `X-Downloader (S${seasonNum}E${episodeNum})${langSuffix}`,
|
||||
server: `X-Downloader (S${seasonNum}E${episodeNum})${langSuffix}`,
|
||||
quality: 'Auto',
|
||||
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: {
|
||||
'User-Agent': HEADERS['User-Agent'],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allUrls.length === 0) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (allUrls.length === 0) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
allUrls,
|
||||
headers: allUrls[0].headers,
|
||||
};
|
||||
}
|
||||
@@ -1,17 +1,22 @@
|
||||
import { fetchJson } from '../utils/helpers.js';
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
const BASE_URL = 'https://yapgrid.com';
|
||||
const EMBED_BASE = 'https://embedwave.cc';
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
|
||||
const targetEmbedUrl = isTv
|
||||
? `https://embedwave.cc/embed/tv/${id}/${s}/${e}`
|
||||
: `https://embedwave.cc/embed/movie/${id}`;
|
||||
? `${EMBED_BASE}/embed/tv/${id}/${s}/${e}`
|
||||
: `${EMBED_BASE}/embed/movie/${id}`;
|
||||
|
||||
const headers = {
|
||||
'Accept': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'Origin': 'https://yapgrid.com',
|
||||
'Referer': isTv ? `https://yapgrid.com/embed/tv/${id}/${s}/${e}` : `https://yapgrid.com/embed/movie/${id}`,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'
|
||||
Origin: BASE_URL,
|
||||
Referer: isTv ? `${BASE_URL}/embed/tv/${id}/${s}/${e}` : `${BASE_URL}/embed/movie/${id}`,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
|
||||
};
|
||||
|
||||
const payload = {
|
||||
@@ -20,13 +25,13 @@ export async function getStream({ id, s, e }) {
|
||||
strictSource: true,
|
||||
waitMs: 5000,
|
||||
nativeHls: false,
|
||||
cacheOnly: true
|
||||
cacheOnly: true,
|
||||
};
|
||||
|
||||
const res = await fetchJson('https://yapgrid.com/player/api/extract', {
|
||||
const res = await fetchJson(`${BASE_URL}/player/api/extract`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(payload)
|
||||
body: JSON.stringify(payload),
|
||||
}).catch(() => null);
|
||||
|
||||
if (!res?.success || !Array.isArray(res.found) || !res.found.length) return null;
|
||||
@@ -38,18 +43,19 @@ export async function getStream({ id, s, e }) {
|
||||
|
||||
let streamUrl = item.url;
|
||||
if (streamUrl.startsWith('/')) {
|
||||
streamUrl = `https://yapgrid.com${streamUrl}`;
|
||||
streamUrl = `${BASE_URL}${streamUrl}`;
|
||||
}
|
||||
|
||||
allUrls.push({
|
||||
url: streamUrl,
|
||||
server: `YapGrid - ${item.nxshaProvider || item.provider || 'Server'}`,
|
||||
quality: 'Auto',
|
||||
type: item.type === 'master' ? 'hls' : (streamUrl.includes('.m3u8') ? 'hls' : 'mp4'),
|
||||
headers: {
|
||||
Referer: 'https://yapgrid.com/',
|
||||
Origin: 'https://yapgrid.com',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'
|
||||
}
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,6 +64,6 @@ export async function getStream({ id, s, e }) {
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
import { fetchJson } from '../utils/helpers.js';
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
const BASE_URL = 'https://yastream.tamthai.de';
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
|
||||
const path = isTv
|
||||
? `/stream/series/tmdb:${id}:${s}:${e}.json`
|
||||
: `/stream/movie/tmdb:${id}.json`;
|
||||
|
||||
const url = `https://yastream.tamthai.de${path}`;
|
||||
const res = await fetchJson(url);
|
||||
const url = `${BASE_URL}${path}`;
|
||||
const res = await fetchJson(url).catch(() => null);
|
||||
if (!res || !Array.isArray(res.streams) || !res.streams.length) return null;
|
||||
|
||||
const allUrls = res.streams.map(item => {
|
||||
@@ -15,8 +19,10 @@ export async function getStream({ id, s, e }) {
|
||||
return {
|
||||
url: item.url,
|
||||
server: `YaStream - ${item.name?.replace(/\n/g, ' ') || 'Server'}`,
|
||||
quality: 'Auto',
|
||||
type: item.url.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: item.behaviorHints?.proxyHeaders?.request || undefined,
|
||||
skipProxy: item.behaviorHints?.notWebReady === false
|
||||
skipProxy: item.behaviorHints?.notWebReady === false,
|
||||
};
|
||||
}).filter(Boolean);
|
||||
|
||||
@@ -25,6 +31,6 @@ export async function getStream({ id, s, e }) {
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
227
src/subtitles.js
227
src/subtitles.js
@@ -1,122 +1,105 @@
|
||||
import { getUA } from './utils/helpers.js';
|
||||
|
||||
const SUBTITLE_BASES = [
|
||||
'https://sub.vdrk.site/v1',
|
||||
'https://sub.vdrk.site/v2',
|
||||
'https://opensubtitles-v3.strem.io'
|
||||
];
|
||||
|
||||
async function resolveImdbId(id, isTv) {
|
||||
if (typeof id === 'string' && id.startsWith('tt')) return id;
|
||||
const apiKey = process.env.TMDB_API_KEY;
|
||||
if (!apiKey) return `tt${id}`;
|
||||
const type = isTv ? 'tv' : 'movie';
|
||||
try {
|
||||
const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&append_to_response=external_ids`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (!res.ok) return `tt${id}`;
|
||||
const data = await res.json();
|
||||
return data.imdb_id || data.external_ids?.imdb_id || `tt${id}`;
|
||||
} catch {
|
||||
return `tt${id}`;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchSubtitles(paths = []) {
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
paths.map(async ({ base, path }) => {
|
||||
try {
|
||||
const res = await fetch(`${base}${path}`, {
|
||||
headers: { 'User-Agent': getUA() },
|
||||
signal: AbortSignal.timeout(6000),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
res.body?.cancel();
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (base.includes('/v2')) {
|
||||
return Array.isArray(data)
|
||||
? data.map(x => ({
|
||||
label: x.label,
|
||||
file: x.file || x.url,
|
||||
type: 'vtt',
|
||||
source: 'v2'
|
||||
}))
|
||||
: [];
|
||||
}
|
||||
|
||||
if (base.includes('opensubtitles-v3.strem.io')) {
|
||||
if (!Array.isArray(data?.subtitles)) return [];
|
||||
return data.subtitles.map(sub => {
|
||||
if (!sub?.url) return null;
|
||||
const isSrt = sub.url.toLowerCase().endsWith('.srt');
|
||||
return {
|
||||
label: sub.lang ? sub.lang.toUpperCase() : 'OpenSubtitles',
|
||||
file: sub.url,
|
||||
type: isSrt ? 'srt' : 'vtt',
|
||||
source: 'opensubtitles'
|
||||
};
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
if (base.includes('fed-subs.pstream.mov')) {
|
||||
if (!data?.subtitles || typeof data.subtitles !== 'object') return [];
|
||||
|
||||
return Object.entries(data.subtitles)
|
||||
.map(([language, sub]) => {
|
||||
if (!sub?.subtitle_link) return null;
|
||||
const ext = sub.subtitle_link.split('.').pop()?.toLowerCase();
|
||||
return {
|
||||
label: sub.subtitle_name || language,
|
||||
file: sub.subtitle_link,
|
||||
type: ext === 'vtt' ? 'vtt' : 'srt',
|
||||
source: 'febbox'
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
const v1 = Array.isArray(data) ? data : [];
|
||||
return v1.map(x => ({
|
||||
label: x.label,
|
||||
file: x.file || x.url,
|
||||
type: 'vtt',
|
||||
source: 'v1'
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return results.flat();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSubPathsMovie(id) {
|
||||
const imdbId = await resolveImdbId(id, false);
|
||||
return [
|
||||
{ base: SUBTITLE_BASES[0], path: `/movie/${id}` },
|
||||
{ base: SUBTITLE_BASES[1], path: `/movie/${id}` },
|
||||
{ base: SUBTITLE_BASES[2], path: `/subtitles/movie/${imdbId}.json` }
|
||||
];
|
||||
}
|
||||
|
||||
export async function getSubPathsTv(id, season, episode) {
|
||||
const imdbId = await resolveImdbId(id, true);
|
||||
return [
|
||||
{ base: SUBTITLE_BASES[0], path: `/tv/${id}/${season}/${episode}` },
|
||||
{ base: SUBTITLE_BASES[1], path: `/tv/${id}/${season}/${episode}` },
|
||||
{ base: SUBTITLE_BASES[2], path: `/subtitles/series/${imdbId}:${season}:${episode}.json` }
|
||||
];
|
||||
}
|
||||
|
||||
export { SUBTITLE_BASES };
|
||||
import { getUA, resolveImdbId } from './utils/helpers.js';
|
||||
|
||||
const SUBTITLE_BASES = [
|
||||
'https://sub.vdrk.site/v1',
|
||||
'https://sub.vdrk.site/v2',
|
||||
'https://opensubtitles-v3.strem.io'
|
||||
];
|
||||
|
||||
export async function fetchSubtitles(paths = []) {
|
||||
try {
|
||||
const results = await Promise.all(
|
||||
paths.map(async ({ base, path }) => {
|
||||
try {
|
||||
const res = await fetch(`${base}${path}`, {
|
||||
headers: { 'User-Agent': getUA() },
|
||||
signal: AbortSignal.timeout(6000),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
res.body?.cancel();
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (base.includes('/v2')) {
|
||||
return Array.isArray(data)
|
||||
? data.map(x => ({
|
||||
label: x.label,
|
||||
file: x.file || x.url,
|
||||
type: 'vtt',
|
||||
source: 'v2'
|
||||
}))
|
||||
: [];
|
||||
}
|
||||
|
||||
if (base.includes('opensubtitles-v3.strem.io')) {
|
||||
if (!Array.isArray(data?.subtitles)) return [];
|
||||
return data.subtitles.map(sub => {
|
||||
if (!sub?.url) return null;
|
||||
const isSrt = sub.url.toLowerCase().endsWith('.srt');
|
||||
return {
|
||||
label: sub.lang ? sub.lang.toUpperCase() : 'OpenSubtitles',
|
||||
file: sub.url,
|
||||
type: isSrt ? 'srt' : 'vtt',
|
||||
source: 'opensubtitles'
|
||||
};
|
||||
}).filter(Boolean);
|
||||
}
|
||||
|
||||
if (base.includes('fed-subs.pstream.mov')) {
|
||||
if (!data?.subtitles || typeof data.subtitles !== 'object') return [];
|
||||
|
||||
return Object.entries(data.subtitles)
|
||||
.map(([language, sub]) => {
|
||||
if (!sub?.subtitle_link) return null;
|
||||
const ext = sub.subtitle_link.split('.').pop()?.toLowerCase();
|
||||
return {
|
||||
label: sub.subtitle_name || language,
|
||||
file: sub.subtitle_link,
|
||||
type: ext === 'vtt' ? 'vtt' : 'srt',
|
||||
source: 'febbox'
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
const v1 = Array.isArray(data) ? data : [];
|
||||
return v1.map(x => ({
|
||||
label: x.label,
|
||||
file: x.file || x.url,
|
||||
type: 'vtt',
|
||||
source: 'v1'
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return results.flat();
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSubPathsMovie(id) {
|
||||
const imdbId = await resolveImdbId(id, false);
|
||||
return [
|
||||
{ base: SUBTITLE_BASES[0], path: `/movie/${id}` },
|
||||
{ base: SUBTITLE_BASES[1], path: `/movie/${id}` },
|
||||
{ base: SUBTITLE_BASES[2], path: `/subtitles/movie/${imdbId}.json` }
|
||||
];
|
||||
}
|
||||
|
||||
export async function getSubPathsTv(id, season, episode) {
|
||||
const imdbId = await resolveImdbId(id, true);
|
||||
return [
|
||||
{ base: SUBTITLE_BASES[0], path: `/tv/${id}/${season}/${episode}` },
|
||||
{ base: SUBTITLE_BASES[1], path: `/tv/${id}/${season}/${episode}` },
|
||||
{ base: SUBTITLE_BASES[2], path: `/subtitles/series/${imdbId}:${season}:${episode}.json` }
|
||||
];
|
||||
}
|
||||
|
||||
export { SUBTITLE_BASES };
|
||||
|
||||
@@ -1,247 +1,586 @@
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
export const UA_LIST = [
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
|
||||
'Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
|
||||
];
|
||||
|
||||
export const getUA = () => UA_LIST[Math.floor(Math.random() * UA_LIST.length)];
|
||||
export const USER_AGENT = UA_LIST[0];
|
||||
|
||||
let sharedDcKey = null;
|
||||
|
||||
export function decryptPartner(responseText, keyEnv) {
|
||||
try {
|
||||
const sep = responseText.indexOf(':');
|
||||
if (sep === -1) return null;
|
||||
const iv = Buffer.from(responseText.slice(0, sep), 'base64');
|
||||
const encrypted = responseText.slice(sep + 1);
|
||||
if (!sharedDcKey) sharedDcKey = crypto.createHash('sha256').update(keyEnv || '').digest();
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', sharedDcKey, iv);
|
||||
return JSON.parse(decipher.update(encrypted, 'base64', 'utf8') + decipher.final('utf8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchJson(url, options) {
|
||||
const res = await fetch(url, options);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function fetchText(url, options) {
|
||||
const res = await fetch(url, options);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.text();
|
||||
}
|
||||
|
||||
const tmdbInfoCache = new Map();
|
||||
const anilistCache = new Map();
|
||||
const tmdbValidationCache = new Map();
|
||||
|
||||
function pruneHelperCache(cache) {
|
||||
const now = Date.now();
|
||||
for (const [k, v] of cache) if (now - v.ts >= v.ttl) cache.delete(k);
|
||||
if (cache.size > 3000) {
|
||||
const overflow = cache.size - 3000;
|
||||
const it = cache.keys();
|
||||
for (let i = 0; i < overflow; i++) cache.delete(it.next().value);
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveImdbId(id, isTv, tmdbApiKey) {
|
||||
if (typeof id === 'string' && id.startsWith('tt')) return id;
|
||||
const apiKey = tmdbApiKey || process.env.TMDB_API_KEY;
|
||||
if (!apiKey) return `tt${id}`;
|
||||
const type = isTv ? 'tv' : 'movie';
|
||||
try {
|
||||
const res = await fetch(`https://api.themoviedb.org/3/${type}/${id}?api_key=${apiKey}&append_to_response=external_ids`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (!res.ok) return `tt${id}`;
|
||||
const data = await res.json();
|
||||
return data.imdb_id || data.external_ids?.imdb_id || `tt${id}`;
|
||||
} catch {
|
||||
return `tt${id}`;
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
pruneHelperCache(tmdbValidationCache);
|
||||
pruneHelperCache(tmdbInfoCache);
|
||||
pruneHelperCache(anilistCache);
|
||||
}, 60000).unref();
|
||||
|
||||
function cacheGet(cache, key) {
|
||||
const entry = cache.get(key);
|
||||
if (!entry) return undefined;
|
||||
if (Date.now() - entry.ts >= entry.ttl) { cache.delete(key); return undefined; }
|
||||
return entry.val;
|
||||
}
|
||||
|
||||
function cacheSet(cache, key, val, ttl) { cache.set(key, { val, ts: Date.now(), ttl }); }
|
||||
|
||||
export async function createStreamArgs(source, sdk, id, s, e, clientIP = null) {
|
||||
const audio = /dub$/.test(source.key) ? 'dub' : 'sub';
|
||||
return {
|
||||
id: id,
|
||||
s: s,
|
||||
e: e,
|
||||
clientIP: clientIP,
|
||||
absoluteBase: '/',
|
||||
audio,
|
||||
sdk,
|
||||
config: source,
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateTmdbId(tmdbApiKey, tmdbId, mediaType = 'movie') {
|
||||
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;
|
||||
if (!apiKey) return { valid: false, error: 'TMDB API key not configured' };
|
||||
try {
|
||||
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));
|
||||
const result = { valid: isValid, error: isValid ? null : 'Invalid TMDB ID' };
|
||||
cacheSet(tmdbValidationCache, key, result, 300000);
|
||||
return result;
|
||||
}
|
||||
const result = { valid: false, error: `TMDB API error: ${res.status}` };
|
||||
cacheSet(tmdbValidationCache, key, result, 60000);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const result = { valid: false, error: `Failed to validate TMDB ID: ${error.message}` };
|
||||
cacheSet(tmdbValidationCache, key, result, 60000);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTmdbInfo(tmdbApiKey, tmdbId, mediaType, season) {
|
||||
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;
|
||||
if (!apiKey) return { isAnime: false, titles: [], year: null, imdbId: null };
|
||||
try {
|
||||
const [mainRes, seasonRes] = await Promise.all([
|
||||
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();
|
||||
if (seasonRes) { if (seasonRes.ok) seasonData = await seasonRes.json(); else seasonRes.body?.cancel(); }
|
||||
const genres = mainData?.genres || [];
|
||||
const isAnime = genres.some(g => g.id === 16) && ((mainData?.origin_country || []).includes('JP') || mainData?.original_language === 'ja');
|
||||
const titles = [];
|
||||
if (seasonData?.name) titles.push(seasonData.name);
|
||||
const titleText = mainData?.title || mainData?.name || '';
|
||||
const ot = mainData?.original_title || mainData?.original_name || '';
|
||||
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);
|
||||
return result;
|
||||
} catch { return { isAnime: false, titles: [], year: null, imdbId: null }; }
|
||||
}
|
||||
|
||||
export async function tmdbToAnilist(tmdbId, mediaType, season, titles = [], year = null) {
|
||||
const key = `${tmdbId}-${mediaType}-${season || ''}`;
|
||||
const cached = cacheGet(anilistCache, key);
|
||||
if (cached !== undefined) return cached;
|
||||
try {
|
||||
const res = await fetch(`https://api.ani.zip/mappings?tmdb_id=${tmdbId}&type=${mediaType}&season=${season || 1}`, { signal: AbortSignal.timeout(6000) });
|
||||
if (res.ok) {
|
||||
const id = (await res.json())?.mappings?.[0]?.anilist_id;
|
||||
if (id) { cacheSet(anilistCache, key, id, 600000); return id; }
|
||||
} else res.body?.cancel();
|
||||
} catch { }
|
||||
if (!titles.length) return null;
|
||||
const query = `query ($s: String) { Page(page:1,perPage:10) { media(search:$s,type:ANIME) { id title { romaji english native } startDate { year } format } } }`;
|
||||
let bestId = null, bestScore = -1;
|
||||
for (const searchTitle of titles) {
|
||||
try {
|
||||
const res = await fetch('https://graphql.anilist.co', { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, body: JSON.stringify({ query, variables: { s: searchTitle } }), signal: AbortSignal.timeout(6000) });
|
||||
if (!res.ok) { res.body?.cancel(); continue; }
|
||||
for (const entry of (await res.json())?.data?.Page?.media || []) {
|
||||
const entryTitles = [entry.title?.romaji, entry.title?.english, entry.title?.native].filter(Boolean).map(t => t.toLowerCase());
|
||||
const sl = searchTitle.toLowerCase();
|
||||
let score = 0;
|
||||
if (entryTitles.some(t => t === sl)) score += 5;
|
||||
else if (entryTitles.some(t => t.includes(sl) || sl.includes(t))) score += 3;
|
||||
else if (entryTitles.some(t => { const w = sl.split(/\s+/).filter(x => x.length > 3); return w.length > 0 && w.every(x => t.includes(x)); })) score += 2;
|
||||
else continue;
|
||||
if (year && entry.startDate?.year) {
|
||||
const diff = Math.abs(entry.startDate.year - year);
|
||||
if (diff === 0) score += 3; else if (diff === 1) score += 1; else if (diff > 2) score -= 3;
|
||||
}
|
||||
if (mediaType === 'tv' && ['TV', 'TV_SHORT', 'ONA', 'OVA'].includes(entry.format)) score += 1;
|
||||
if (mediaType === 'movie' && entry.format === 'MOVIE') score += 1;
|
||||
if (score > bestScore) { bestScore = score; bestId = entry.id; }
|
||||
}
|
||||
if (bestScore >= 8) break;
|
||||
} catch { }
|
||||
}
|
||||
if (bestId) cacheSet(anilistCache, key, bestId, 600000);
|
||||
return bestId;
|
||||
}
|
||||
|
||||
export async function imdbToTmdb(tmdbApiKey, imdbId, mediaType = 'movie') {
|
||||
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;
|
||||
if (!apiKey) return null;
|
||||
try {
|
||||
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 (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; }
|
||||
}
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
export const UA_LIST = [
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
|
||||
'Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
|
||||
];
|
||||
|
||||
export const getUA = () => UA_LIST[Math.floor(Math.random() * UA_LIST.length)];
|
||||
export const USER_AGENT = UA_LIST[0];
|
||||
|
||||
const DEFAULT_TMDB_KEY = '45dbdd5e714da212424bda0336167447';
|
||||
|
||||
let sharedDcKey = null;
|
||||
|
||||
const tmdbInfoCache = new Map();
|
||||
const anilistCache = new Map();
|
||||
const tmdbValidationCache = new Map();
|
||||
const tmdbMetaCache = new Map();
|
||||
|
||||
function pruneCache(cache) {
|
||||
const now = Date.now();
|
||||
for (const [k, v] of cache) {
|
||||
if (now - v.ts >= v.ttl) cache.delete(k);
|
||||
}
|
||||
if (cache.size > 3000) {
|
||||
const overflow = cache.size - 3000;
|
||||
const it = cache.keys();
|
||||
for (let i = 0; i < overflow; i++) cache.delete(it.next().value);
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
pruneCache(tmdbValidationCache);
|
||||
pruneCache(tmdbInfoCache);
|
||||
pruneCache(anilistCache);
|
||||
pruneCache(tmdbMetaCache);
|
||||
}, 60000).unref();
|
||||
|
||||
function cacheGet(cache, key) {
|
||||
const entry = cache.get(key);
|
||||
if (!entry) return undefined;
|
||||
if (Date.now() - entry.ts >= entry.ttl) {
|
||||
cache.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
return entry.val;
|
||||
}
|
||||
|
||||
function cacheSet(cache, key, val, ttl) {
|
||||
cache.set(key, { val, ts: Date.now(), ttl });
|
||||
}
|
||||
|
||||
export function resolveTmdbKey(args = {}, sdk = null) {
|
||||
return (
|
||||
args?.tmdbApiKey ||
|
||||
args?.sdk?.tmdbApiKey ||
|
||||
sdk?.tmdbApiKey ||
|
||||
process.env.TMDB_API_KEY ||
|
||||
DEFAULT_TMDB_KEY
|
||||
);
|
||||
}
|
||||
|
||||
export function slugify(text) {
|
||||
if (!text) return '';
|
||||
return String(text)
|
||||
.toLowerCase()
|
||||
.normalize('NFKD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
}
|
||||
|
||||
export function cleanTitle(text) {
|
||||
if (!text) return '';
|
||||
return String(text)
|
||||
.toLowerCase()
|
||||
.replace(/\(\d{4}\)/g, '')
|
||||
.replace(/[^a-z0-9]/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function formatBytes(bytes, decimals = 2) {
|
||||
if (!bytes || bytes <= 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), sizes.length - 1);
|
||||
return `${(bytes / Math.pow(k, i)).toFixed(decimals)} ${sizes[i]}`;
|
||||
}
|
||||
|
||||
export function getRandomHex(size = 32) {
|
||||
return crypto.randomBytes(size).toString('hex');
|
||||
}
|
||||
|
||||
export async function fetchJson(url, options = {}) {
|
||||
const res = await fetch(url, options);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function fetchText(url, options = {}) {
|
||||
const res = await fetch(url, options);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.text();
|
||||
}
|
||||
|
||||
export function decryptPartner(responseText, keyEnv) {
|
||||
try {
|
||||
const sep = responseText.indexOf(':');
|
||||
if (sep === -1) return null;
|
||||
const iv = Buffer.from(responseText.slice(0, sep), 'base64');
|
||||
const encrypted = responseText.slice(sep + 1);
|
||||
if (!sharedDcKey) sharedDcKey = crypto.createHash('sha256').update(keyEnv || '').digest();
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', sharedDcKey, iv);
|
||||
return JSON.parse(decipher.update(encrypted, 'base64', 'utf8') + decipher.final('utf8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTmdbMetadata(id, isTv, apiKey = null) {
|
||||
const key = apiKey || process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY;
|
||||
const type = isTv ? 'tv' : 'movie';
|
||||
const cacheKey = `${id}-${type}-${key}`;
|
||||
const cached = cacheGet(tmdbMetaCache, cacheKey);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const fallback = {
|
||||
title: '',
|
||||
originalTitle: '',
|
||||
titles: [],
|
||||
year: null,
|
||||
releaseDate: '',
|
||||
imdbId: null,
|
||||
isAnime: false,
|
||||
runtime: 0,
|
||||
seasons: [],
|
||||
};
|
||||
|
||||
try {
|
||||
const url = `https://api.themoviedb.org/3/${type}/${id}?api_key=${key}&append_to_response=external_ids,alternative_titles`;
|
||||
const res = await fetch(url, { signal: AbortSignal.timeout(6000) });
|
||||
if (!res.ok) {
|
||||
cacheSet(tmdbMetaCache, cacheKey, fallback, 60000);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const title = data.name || data.title || '';
|
||||
const originalTitle = data.original_name || data.original_title || '';
|
||||
const releaseDate = data.first_air_date || data.release_date || '';
|
||||
const year = releaseDate ? parseInt(releaseDate.slice(0, 4), 10) : null;
|
||||
const imdbId = data.imdb_id || data.external_ids?.imdb_id || null;
|
||||
const runtime = isTv ? (data.episode_run_time?.[0] || 24) : (data.runtime || 90);
|
||||
|
||||
const titles = [];
|
||||
if (title) titles.push(title);
|
||||
if (originalTitle && !titles.includes(originalTitle)) titles.push(originalTitle);
|
||||
|
||||
const alts = data.alternative_titles?.titles || data.alternative_titles?.results || [];
|
||||
for (const alt of alts) {
|
||||
const altTitle = alt.title || alt.name;
|
||||
if (altTitle && !titles.includes(altTitle)) titles.push(altTitle);
|
||||
}
|
||||
|
||||
const genres = data.genres || [];
|
||||
const isAnime = genres.some(g => g.id === 16) && ((data.origin_country || []).includes('JP') || data.original_language === 'ja');
|
||||
|
||||
const result = {
|
||||
title,
|
||||
originalTitle,
|
||||
titles,
|
||||
year,
|
||||
releaseDate,
|
||||
imdbId,
|
||||
isAnime,
|
||||
runtime,
|
||||
seasons: Array.isArray(data.seasons) ? data.seasons : [],
|
||||
};
|
||||
|
||||
cacheSet(tmdbMetaCache, cacheKey, result, 600000);
|
||||
return result;
|
||||
} catch {
|
||||
cacheSet(tmdbMetaCache, cacheKey, fallback, 60000);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveImdbId(id, isTv, tmdbApiKey = null) {
|
||||
if (typeof id === 'string' && id.startsWith('tt')) return id;
|
||||
const meta = await getTmdbMetadata(id, isTv, tmdbApiKey);
|
||||
return meta?.imdbId || `tt${id}`;
|
||||
}
|
||||
|
||||
export async function getTmdbInfo(arg1, arg2, arg3, arg4) {
|
||||
let apiKey = null;
|
||||
let id = null;
|
||||
let type = null;
|
||||
let season = null;
|
||||
|
||||
if (arg1 === 'movie' || arg1 === 'tv') {
|
||||
type = arg1;
|
||||
id = arg2;
|
||||
season = arg3;
|
||||
apiKey = arg4 || process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY;
|
||||
} else if (arg2 === 'movie' || arg2 === 'tv') {
|
||||
if (typeof arg1 === 'string' && arg1.length > 20 && !/^\d+$/.test(arg1)) {
|
||||
apiKey = arg1;
|
||||
id = arg3;
|
||||
type = arg2;
|
||||
season = arg4;
|
||||
} else {
|
||||
id = arg1;
|
||||
type = arg2;
|
||||
season = arg3;
|
||||
apiKey = arg4 || process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY;
|
||||
}
|
||||
} else if (arg3 === 'movie' || arg3 === 'tv') {
|
||||
apiKey = arg1;
|
||||
id = arg2;
|
||||
type = arg3;
|
||||
season = arg4;
|
||||
} else {
|
||||
id = arg1;
|
||||
type = arg2 || 'movie';
|
||||
season = arg3;
|
||||
apiKey = arg4 || process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY;
|
||||
}
|
||||
|
||||
if (!apiKey) apiKey = process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY;
|
||||
|
||||
const isTv = type === 'tv';
|
||||
const key = `${id}-${type}-${season || ''}-${apiKey}`;
|
||||
const cached = cacheGet(tmdbInfoCache, key);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
try {
|
||||
const meta = await getTmdbMetadata(id, isTv, apiKey);
|
||||
let seasonTitle = null;
|
||||
|
||||
if (season && isTv) {
|
||||
try {
|
||||
const seasonRes = await fetch(`https://api.themoviedb.org/3/tv/${id}/season/${season}?api_key=${apiKey}`, { signal: AbortSignal.timeout(5000) });
|
||||
if (seasonRes.ok) {
|
||||
const seasonData = await seasonRes.json();
|
||||
if (seasonData?.name) seasonTitle = seasonData.name;
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
const titles = seasonTitle ? [seasonTitle, ...meta.titles] : meta.titles;
|
||||
const result = {
|
||||
name: meta.title,
|
||||
title: meta.title,
|
||||
isAnime: meta.isAnime,
|
||||
titles: [...new Set(titles.filter(Boolean))],
|
||||
year: meta.year,
|
||||
release_date: meta.releaseDate,
|
||||
first_air_date: meta.releaseDate,
|
||||
imdbId: meta.imdbId,
|
||||
imdb_id: meta.imdbId,
|
||||
external_ids: { imdb_id: meta.imdbId },
|
||||
seasons: meta.seasons,
|
||||
runtime: meta.runtime,
|
||||
};
|
||||
|
||||
cacheSet(tmdbInfoCache, key, result, 600000);
|
||||
return result;
|
||||
} catch {
|
||||
const fallback = {
|
||||
name: '',
|
||||
title: '',
|
||||
isAnime: false,
|
||||
titles: [],
|
||||
year: null,
|
||||
release_date: '',
|
||||
first_air_date: '',
|
||||
imdbId: null,
|
||||
imdb_id: null,
|
||||
external_ids: { imdb_id: null },
|
||||
seasons: [],
|
||||
runtime: 0,
|
||||
};
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateTmdbId(tmdbApiKey, tmdbId, mediaType = 'movie') {
|
||||
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 || DEFAULT_TMDB_KEY;
|
||||
}
|
||||
}
|
||||
if (!apiKey) apiKey = process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY;
|
||||
|
||||
const key = `${id}-${type}`;
|
||||
const cached = cacheGet(tmdbValidationCache, key);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
try {
|
||||
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 = Boolean(data?.id && (data?.title || data?.name));
|
||||
const result = { valid: isValid, error: isValid ? null : 'Invalid TMDB ID' };
|
||||
cacheSet(tmdbValidationCache, key, result, 300000);
|
||||
return result;
|
||||
}
|
||||
const result = { valid: false, error: `TMDB API error: ${res.status}` };
|
||||
cacheSet(tmdbValidationCache, key, result, 60000);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const result = { valid: false, error: `Failed to validate TMDB ID: ${error.message}` };
|
||||
cacheSet(tmdbValidationCache, key, result, 60000);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export async function imdbToTmdb(tmdbApiKey, imdbId, mediaType = 'movie') {
|
||||
let apiKey = tmdbApiKey;
|
||||
let id = imdbId;
|
||||
let type = mediaType;
|
||||
|
||||
if (id === 'movie' || id === 'tv') {
|
||||
type = id;
|
||||
id = apiKey;
|
||||
apiKey = process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY;
|
||||
}
|
||||
if (!apiKey) apiKey = process.env.TMDB_API_KEY || DEFAULT_TMDB_KEY;
|
||||
|
||||
const key = `imdb-${id}`;
|
||||
const cached = cacheGet(tmdbInfoCache, key);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
try {
|
||||
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 (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;
|
||||
}
|
||||
}
|
||||
|
||||
export async function tmdbToAnilist(tmdbId, mediaType, season, titles = [], year = null) {
|
||||
const key = `${tmdbId}-${mediaType}-${season || ''}`;
|
||||
const cached = cacheGet(anilistCache, key);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
try {
|
||||
const res = await fetch(`https://api.ani.zip/mappings?tmdb_id=${tmdbId}&type=${mediaType}&season=${season || 1}`, { signal: AbortSignal.timeout(6000) });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const id = data?.mappings?.[0]?.anilist_id;
|
||||
if (id) {
|
||||
cacheSet(anilistCache, key, id, 600000);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
|
||||
if (!titles.length) return null;
|
||||
|
||||
const query = `query ($s: String) { Page(page:1,perPage:10) { media(search:$s,type:ANIME) { id title { romaji english native } startDate { year } format } } }`;
|
||||
let bestId = null;
|
||||
let bestScore = -1;
|
||||
|
||||
for (const searchTitle of titles) {
|
||||
try {
|
||||
const res = await fetch('https://graphql.anilist.co', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({ query, variables: { s: searchTitle } }),
|
||||
signal: AbortSignal.timeout(6000)
|
||||
});
|
||||
if (!res.ok) continue;
|
||||
|
||||
const resJson = await res.json();
|
||||
for (const entry of resJson?.data?.Page?.media || []) {
|
||||
const entryTitles = [entry.title?.romaji, entry.title?.english, entry.title?.native].filter(Boolean).map(t => t.toLowerCase());
|
||||
const sl = searchTitle.toLowerCase();
|
||||
let score = 0;
|
||||
|
||||
if (entryTitles.some(t => t === sl)) score += 5;
|
||||
else if (entryTitles.some(t => t.includes(sl) || sl.includes(t))) score += 3;
|
||||
else if (entryTitles.some(t => {
|
||||
const w = sl.split(/\s+/).filter(x => x.length > 3);
|
||||
return w.length > 0 && w.every(x => t.includes(x));
|
||||
})) score += 2;
|
||||
else continue;
|
||||
|
||||
if (year && entry.startDate?.year) {
|
||||
const diff = Math.abs(entry.startDate.year - year);
|
||||
if (diff === 0) score += 3;
|
||||
else if (diff === 1) score += 1;
|
||||
else if (diff > 2) score -= 3;
|
||||
}
|
||||
|
||||
if (mediaType === 'tv' && ['TV', 'TV_SHORT', 'ONA', 'OVA'].includes(entry.format)) score += 1;
|
||||
if (mediaType === 'movie' && entry.format === 'MOVIE') score += 1;
|
||||
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestId = entry.id;
|
||||
}
|
||||
}
|
||||
if (bestScore >= 8) break;
|
||||
} catch { }
|
||||
}
|
||||
|
||||
if (bestId) cacheSet(anilistCache, key, bestId, 600000);
|
||||
return bestId;
|
||||
}
|
||||
|
||||
export async function createStreamArgs(source, sdk, id, s, e, clientIP = null) {
|
||||
const audio = /dub$/.test(source.key) ? 'dub' : 'sub';
|
||||
return {
|
||||
id,
|
||||
s,
|
||||
e,
|
||||
clientIP,
|
||||
absoluteBase: '/',
|
||||
audio,
|
||||
sdk,
|
||||
config: source,
|
||||
tmdbApiKey: resolveTmdbKey(null, sdk),
|
||||
};
|
||||
}
|
||||
|
||||
export function formatStreamResult(streams, defaultHeaders = {}, extra = {}) {
|
||||
if (!streams) return null;
|
||||
const list = Array.isArray(streams) ? streams.filter(Boolean) : [streams];
|
||||
if (!list.length) return null;
|
||||
|
||||
const allUrls = list.map(item => {
|
||||
const itemUrl = item.url || item.file || item.stream || item.src;
|
||||
if (!itemUrl) return null;
|
||||
return {
|
||||
url: itemUrl,
|
||||
server: item.server || item.label || item.name || 'Server',
|
||||
quality: item.quality || 'Auto',
|
||||
type: item.type || (itemUrl.includes('.m3u8') ? 'hls' : (itemUrl.includes('.mpd') ? 'dash' : 'mp4')),
|
||||
headers: item.headers || defaultHeaders,
|
||||
...(item.subtitles ? { subtitles: item.subtitles } : {}),
|
||||
...(item.skipProxy !== undefined ? { skipProxy: item.skipProxy } : {}),
|
||||
...(item.audio ? { audio: item.audio } : {}),
|
||||
...(item.label ? { label: item.label } : {}),
|
||||
};
|
||||
}).filter(Boolean);
|
||||
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
server: allUrls[0].server,
|
||||
quality: allUrls[0].quality,
|
||||
type: allUrls[0].type,
|
||||
allUrls,
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
export async function scrapeVidcoreFamily({ id, s, e, baseUrl, apiName, serverLabel }) {
|
||||
const isTv = Boolean(s && e);
|
||||
const targetUrl = isTv
|
||||
? `${baseUrl}/tv/${id}/${s}/${e}/`
|
||||
: `${baseUrl}/movie/${id}`;
|
||||
|
||||
const ua = getUA();
|
||||
const baseHeaders = {
|
||||
'User-Agent': ua,
|
||||
Referer: `${baseUrl}/`,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
};
|
||||
|
||||
try {
|
||||
const pageRes = await fetch(targetUrl, {
|
||||
headers: { 'User-Agent': ua },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!pageRes.ok) return null;
|
||||
const html = await pageRes.text();
|
||||
|
||||
const match = html.match(/\\"(?:en|token)\\":\\"(.*?)\\"/);
|
||||
if (!match || !match[1]) return null;
|
||||
const enToken = match[1];
|
||||
|
||||
const s1Res = await fetch(`https://enc-dec.app/api/enc-${apiName}?text=${encodeURIComponent(enToken)}&stage=1`, {
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
const s1Data = await s1Res.json();
|
||||
if (s1Data?.status !== 200 || !s1Data?.result?.stage1) return null;
|
||||
|
||||
const token1 = s1Data.result.token;
|
||||
const stage1Url = s1Data.result.stage1;
|
||||
|
||||
const post1Res = await fetch(stage1Url, {
|
||||
method: 'POST',
|
||||
headers: { ...baseHeaders, 'X-CSRF-Token': token1 },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!post1Res.ok) return null;
|
||||
const stage1Text = await post1Res.text();
|
||||
|
||||
const s2Res = await fetch(`https://enc-dec.app/api/enc-${apiName}?text=${encodeURIComponent(stage1Text)}&stage=2`, {
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
const s2Data = await s2Res.json();
|
||||
if (s2Data?.status !== 200 || !s2Data?.result?.servers) return null;
|
||||
|
||||
const { servers: serversUrl, stream: streamBaseUrl, token: token2 } = s2Data.result;
|
||||
const currentHeaders = { ...baseHeaders, 'X-CSRF-Token': token2 };
|
||||
|
||||
const serversRes = await fetch(serversUrl, {
|
||||
method: 'POST',
|
||||
headers: currentHeaders,
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!serversRes.ok) return null;
|
||||
const serversEncrypted = await serversRes.text();
|
||||
|
||||
const decServersRes = await fetch(`https://enc-dec.app/api/dec-${apiName}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: serversEncrypted }),
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
const decServersData = await decServersRes.json();
|
||||
if (decServersData?.status !== 200 || !Array.isArray(decServersData.result)) return null;
|
||||
|
||||
const allUrls = [];
|
||||
|
||||
for (const server of decServersData.result) {
|
||||
if (!server?.data) continue;
|
||||
try {
|
||||
const streamUrl = `${streamBaseUrl}/${server.data}`;
|
||||
const streamEncRes = await fetch(streamUrl, {
|
||||
method: 'POST',
|
||||
headers: currentHeaders,
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
if (!streamEncRes.ok) continue;
|
||||
const streamEncText = await streamEncRes.text();
|
||||
|
||||
const decStreamRes = await fetch(`https://enc-dec.app/api/dec-${apiName}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: streamEncText }),
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
const decStreamData = await decStreamRes.json();
|
||||
if (decStreamData?.status !== 200 || !decStreamData.result) continue;
|
||||
|
||||
const streamResult = decStreamData.result;
|
||||
const resultUrl = typeof streamResult === 'string' ? streamResult : streamResult?.url || streamResult?.file;
|
||||
|
||||
if (typeof resultUrl === 'string' && resultUrl.startsWith('http')) {
|
||||
allUrls.push({
|
||||
url: resultUrl,
|
||||
server: `${serverLabel} - ${server.name || server.title || 'Server'}`,
|
||||
label: server.name || server.title || serverLabel,
|
||||
type: resultUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: {
|
||||
'User-Agent': ua,
|
||||
Referer: `${baseUrl}/`,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
return formatStreamResult(allUrls);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ const CONFIG = {
|
||||
TEST_SUBTITLE: false,
|
||||
TEST_DOWNLOAD: false,
|
||||
TEST_MOVIE: true,
|
||||
TEST_TV: true,
|
||||
TEST_TV: false,
|
||||
TEST_MANGA: false,
|
||||
MANGA_QUERY: "Steel Ball Run",
|
||||
|
||||
@@ -32,7 +32,7 @@ dotenv.config({
|
||||
});
|
||||
|
||||
// debug
|
||||
if (CONFIG.DEBUG) {
|
||||
if (CONFIG.DEBUG && !process.argv.includes("all")) {
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
const debugFile = path.resolve("test/debug-output.json");
|
||||
@@ -118,6 +118,45 @@ const sdk = new VylaSDK({
|
||||
...CONFIG.EXTRA_SDK_PARAMS
|
||||
});
|
||||
|
||||
if (process.argv.includes("all")) {
|
||||
const sources = sdk.getSources(true);
|
||||
console.log(`Testing ${sources.length} sources in parallel...\n`);
|
||||
|
||||
const results = {};
|
||||
const failed = [];
|
||||
|
||||
await Promise.all(
|
||||
sources.map(async (cfg) => {
|
||||
try {
|
||||
const res = await sdk.probeSource(cfg.key);
|
||||
results[cfg.key] = res;
|
||||
if (res?.ok) {
|
||||
console.log(`[PASS] ${cfg.key} (${res.ms}ms)`);
|
||||
} else {
|
||||
failed.push(cfg.key);
|
||||
console.log(`[FAIL] ${cfg.key} (${res?.ms ?? 0}ms)`);
|
||||
}
|
||||
} catch (err) {
|
||||
results[cfg.key] = { ok: false, ms: null, error: err.message };
|
||||
failed.push(cfg.key);
|
||||
console.log(`[FAIL] ${cfg.key} (${err.message})`);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
console.log(`\nFinished testing. Total: ${sources.length}, Passed: ${sources.length - failed.length}, Failed: ${failed.length}`);
|
||||
console.log("Sources that returned nothing:", failed);
|
||||
|
||||
const debugFile = path.resolve("test/debug-output.json");
|
||||
fs.mkdirSync(path.dirname(debugFile), { recursive: true });
|
||||
fs.writeFileSync(debugFile, JSON.stringify({
|
||||
sourcesThatReturnedNothing: failed,
|
||||
results
|
||||
}, null, 2));
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log("Starting...");
|
||||
|
||||
if (CONFIG.TEST_SUBTITLE && CONFIG.TEST_TV) {
|
||||
|
||||
Reference in New Issue
Block a user