mirror of
https://gitlab.com/vyla-entertainment/TV.git
synced 2026-11-09 17:47:20 -05:00
Fixed player UI dismissal bug
This commit is contained in:
@@ -1,49 +0,0 @@
|
||||
import SOURCES from './sdk/config.js';
|
||||
import { createStreamArgs } from './sdk/utils/helpers.js';
|
||||
import { fetchSubtitles, getSubPathsMovie, getSubPathsTv } from './sdk/subtitles.js';
|
||||
import * as m0 from './sdk/sources/123anime.js';
|
||||
import * as m1 from './sdk/sources/111477.js';
|
||||
import * as m2 from './sdk/sources/anihq.js';
|
||||
import * as m3 from './sdk/sources/anineko.js';
|
||||
import * as m4 from './sdk/sources/bcine.js';
|
||||
import * as m5 from './sdk/sources/dulo.js';
|
||||
import * as m6 from './sdk/sources/flaxmovies.js';
|
||||
import * as m7 from './sdk/sources/fsonline.js';
|
||||
import * as m8 from './sdk/sources/kisskh.js';
|
||||
import * as m9 from './sdk/sources/lmscript.js';
|
||||
import * as m10 from './sdk/sources/lookmovie.js';
|
||||
import * as m11 from './sdk/sources/movienight.js';
|
||||
import * as m12 from './sdk/sources/movy.js';
|
||||
import * as m13 from './sdk/sources/pengu.js';
|
||||
import * as m14 from './sdk/sources/pstream.js';
|
||||
import * as m15 from './sdk/sources/purstream.js';
|
||||
import * as m16 from './sdk/sources/rivestream.js';
|
||||
import * as m17 from './sdk/sources/streamaggregator.js';
|
||||
import * as m18 from './sdk/sources/vidapi.js';
|
||||
import * as m19 from './sdk/sources/vidcore.js';
|
||||
import * as m20 from './sdk/sources/vidfast.js';
|
||||
import * as m21 from './sdk/sources/vidgod.js';
|
||||
import * as m22 from './sdk/sources/vidlink.js';
|
||||
import * as m23 from './sdk/sources/vidnest.js';
|
||||
import * as m24 from './sdk/sources/vidrock.js';
|
||||
import * as m25 from './sdk/sources/vidup.js';
|
||||
import * as m26 from './sdk/sources/vidvault.js';
|
||||
import * as m27 from './sdk/sources/vidzee.js';
|
||||
import * as m28 from './sdk/sources/vixsrc.js';
|
||||
import * as m29 from './sdk/sources/xdownloader.js';
|
||||
const MODS = { '123anime': m0, '111477': m1, 'anihq': m2, 'anineko': m3, 'bcine': m4, 'dulo': m5, 'flaxmovies': m6, 'fsonline': m7, 'kisskh': m8, 'lmscript': m9, 'lookmovie': m10, 'movienight': m11, 'movy': m12, 'pengu': m13, 'pstream': m14, 'purstream': m15, 'rivestream': m16, 'streamaggregator': m17, 'vidapi': m18, 'vidcore': m19, 'vidfast': m20, 'vidgod': m21, 'vidlink': m22, 'vidnest': m23, 'vidrock': m24, 'vidup': m25, 'vidvault': m26, 'vidzee': m27, 'vixsrc': m28, 'xdownloader': m29 };
|
||||
const core = { tmdbApiKey: null, penguManifest: null };
|
||||
const live = () => SOURCES.filter(c => !c.disabled && MODS[c.sourceFile]);
|
||||
globalThis.VylaLocal = {
|
||||
init(key) { core.tmdbApiKey = key; globalThis.process.env.TMDB_API_KEY = key; },
|
||||
sources() { return live(); },
|
||||
async stream(key, id, s, e, ip) {
|
||||
const cfg = live().find(c => c.key === key);
|
||||
if (!cfg) throw new Error('Source not found');
|
||||
return await MODS[cfg.sourceFile].getStream(await createStreamArgs(cfg, core, id, s, e, ip));
|
||||
},
|
||||
async subtitles(id, s, e) {
|
||||
const paths = s != null && e != null ? await getSubPathsTv(id, s, e) : await getSubPathsMovie(id);
|
||||
return await fetchSubtitles(paths);
|
||||
}
|
||||
};
|
||||
@@ -1,487 +0,0 @@
|
||||
// Currently active sources reported on 6:18:22 PM PST 9/13/2026
|
||||
|
||||
// None Anime Tested: 936075
|
||||
// Anime Tested: 37854
|
||||
|
||||
// Fshare/Fsonic Tested: 155 ( they don't have Michael )
|
||||
|
||||
export const SOURCES = [
|
||||
{
|
||||
key: '123anime',
|
||||
label: '123Anime',
|
||||
sourceFile: '123anime',
|
||||
proxyParam: 'a1',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: false,
|
||||
skipProxy: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /hlsx\d+cdn\.|burntburst\d+\.store|echovideo\.ru/i,
|
||||
headers: {
|
||||
Referer: 'https://play2.echovideo.ru/',
|
||||
Origin: 'https://play2.echovideo.ru',
|
||||
},
|
||||
},],
|
||||
},
|
||||
|
||||
{
|
||||
key: '111477',
|
||||
label: '111477',
|
||||
sourceFile: '111477',
|
||||
proxyParam: 'a11',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /111477\.xyz/i,
|
||||
headers: {
|
||||
Referer: 'https://st.111477.xyz/',
|
||||
Origin: 'https://st.111477.xyz',
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'anihq-sub',
|
||||
label: 'AniHQ (Sub)',
|
||||
sourceFile: 'anihq',
|
||||
proxyParam: 'ahsub',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true
|
||||
},
|
||||
|
||||
{
|
||||
key: 'anihq-dub',
|
||||
label: 'AniHQ (Dub)',
|
||||
sourceFile: 'anihq',
|
||||
proxyParam: 'ahdub',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true
|
||||
},
|
||||
|
||||
{
|
||||
key: 'anineko-sub',
|
||||
sourceFile: 'anineko',
|
||||
label: 'AniNeko (Sub)',
|
||||
proxyParam: 'anksub',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2
|
||||
},
|
||||
|
||||
{
|
||||
key: 'anineko-dub',
|
||||
sourceFile: 'anineko',
|
||||
label: 'AniNeko (Dub)',
|
||||
proxyParam: 'ankdub',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2
|
||||
},
|
||||
|
||||
{
|
||||
key: 'bcine',
|
||||
label: 'Bcine',
|
||||
sourceFile: 'bcine',
|
||||
proxyParam: 'bc',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /1embed\.cc|videasy\.to/i,
|
||||
headers: {
|
||||
Referer: 'https://bcine.ru/',
|
||||
Origin: 'https://bcine.ru',
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'dulo',
|
||||
label: 'Dulo',
|
||||
sourceFile: 'dulo',
|
||||
proxyParam: 'dl',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /dulo\.mov/i,
|
||||
headers: {
|
||||
Referer: 'https://dulo.mov/',
|
||||
Origin: 'https://dulo.mov',
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'flaxmovies',
|
||||
label: 'FlaxMovies',
|
||||
sourceFile: 'flaxmovies',
|
||||
proxyParam: 'fx',
|
||||
timeout: 20000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /flix2watch\.pro/i,
|
||||
headers: {
|
||||
Referer: 'https://flaxmovies.xyz/',
|
||||
Origin: 'https://flaxmovies.xyz',
|
||||
},
|
||||
},],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'fsonline',
|
||||
label: 'FSOnline',
|
||||
sourceFile: 'fsonline',
|
||||
proxyParam: 'fo',
|
||||
timeout: 20000,
|
||||
retries: 1,
|
||||
jitter: 0,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'kisskh',
|
||||
label: 'KissKH',
|
||||
sourceFile: 'kisskh',
|
||||
proxyParam: 'kk',
|
||||
timeout: 30000,
|
||||
jitter: 500,
|
||||
retries: 1,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'lmscript',
|
||||
label: 'LMScript',
|
||||
sourceFile: 'lmscript',
|
||||
proxyParam: 'lmsc',
|
||||
timeout: 20000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'lookmovie',
|
||||
label: 'LookMovie',
|
||||
sourceFile: 'lookmovie',
|
||||
proxyParam: 'lm',
|
||||
timeout: 20000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
verifyHeaders: {
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
key: 'movienight',
|
||||
label: 'MovieNight',
|
||||
sourceFile: 'movienight',
|
||||
proxyParam: 'mn',
|
||||
timeout: 20000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /movienig\.ht/i,
|
||||
headers: {
|
||||
Referer: 'https://movienig.ht/',
|
||||
Origin: 'https://movienig.ht',
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'movy',
|
||||
label: 'Movy',
|
||||
sourceFile: 'movy',
|
||||
proxyParam: 'mv',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /wecollege\.net|movy\.bz/i,
|
||||
headers: {
|
||||
Referer: 'https://www.movy.bz/',
|
||||
Origin: 'https://www.movy.bz',
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'pengu',
|
||||
label: 'Pengu',
|
||||
sourceFile: 'pengu',
|
||||
proxyParam: 'pg',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 1,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
disabled: false, // Enabled, but only returns streams when a manifest is provided
|
||||
},
|
||||
|
||||
{
|
||||
key: 'pstream',
|
||||
label: 'PStream',
|
||||
sourceFile: 'pstream',
|
||||
proxyParam: 'ps',
|
||||
timeout: 35000,
|
||||
jitter: 500,
|
||||
retries: 1,
|
||||
multiUrl: true,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'purstream',
|
||||
sourceFile: 'purstream',
|
||||
label: 'Purstream',
|
||||
proxyParam: 'ps',
|
||||
timeout: 20000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
skipProxy: true
|
||||
},
|
||||
|
||||
{
|
||||
key: 'rivestream',
|
||||
label: 'RiveStream',
|
||||
sourceFile: 'rivestream',
|
||||
proxyParam: 'rs',
|
||||
timeout: 30000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'streamaggregator',
|
||||
label: 'StreamAggregator',
|
||||
sourceFile: 'streamaggregator',
|
||||
proxyParam: 'sa',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /streamaggregator\.in/i,
|
||||
headers: {
|
||||
Referer: 'https://streamaggregator.in/',
|
||||
Origin: 'https://streamaggregator.in',
|
||||
},
|
||||
}],
|
||||
},
|
||||
{
|
||||
key: 'vidapi',
|
||||
label: 'VidAPI',
|
||||
sourceFile: 'vidapi',
|
||||
proxyParam: 'va',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 1,
|
||||
multiUrl: true
|
||||
},
|
||||
|
||||
{
|
||||
key: 'vidcore',
|
||||
label: 'VidCore',
|
||||
sourceFile: 'vidcore',
|
||||
proxyParam: 'vc',
|
||||
timeout: 35000,
|
||||
jitter: 500,
|
||||
retries: 1,
|
||||
multiUrl: true,
|
||||
},
|
||||
{
|
||||
key: 'vidfast',
|
||||
label: 'VidFast',
|
||||
sourceFile: 'vidfast',
|
||||
proxyParam: 'vf',
|
||||
timeout: 35000,
|
||||
jitter: 500,
|
||||
retries: 1,
|
||||
multiUrl: true,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'vidgod',
|
||||
label: 'VidGod',
|
||||
sourceFile: 'vidgod',
|
||||
proxyParam: 'vg',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'vidlink',
|
||||
label: 'Vidlink',
|
||||
sourceFile: 'vidlink',
|
||||
proxyParam: 'vl',
|
||||
timeout: 20000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
skipProxy: true,
|
||||
multiUrl: true,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'vidnest',
|
||||
label: 'VidNest',
|
||||
sourceFile: 'vidnest',
|
||||
proxyParam: 'vdn',
|
||||
timeout: 20000,
|
||||
retries: 1,
|
||||
jitter: 0,
|
||||
multiUrl: true,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'vidnest-sub',
|
||||
label: 'VidNest (Sub)',
|
||||
sourceFile: 'vidnest',
|
||||
proxyParam: 'vdn',
|
||||
timeout: 20000,
|
||||
retries: 1,
|
||||
jitter: 0,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'vidnest-dub',
|
||||
label: 'VidNest (Dub)',
|
||||
sourceFile: 'vidnest',
|
||||
proxyParam: 'vdn',
|
||||
timeout: 20000,
|
||||
retries: 1,
|
||||
jitter: 0,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
},
|
||||
|
||||
{
|
||||
key: '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',
|
||||
sourceFile: 'vidup',
|
||||
proxyParam: 'vu',
|
||||
timeout: 35000,
|
||||
jitter: 500,
|
||||
retries: 1,
|
||||
multiUrl: true
|
||||
},
|
||||
|
||||
{
|
||||
key: 'vidvault',
|
||||
label: 'VidVault',
|
||||
sourceFile: 'vidvault',
|
||||
proxyParam: 'vv',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'vidzee',
|
||||
label: 'VidZee',
|
||||
sourceFile: 'vidzee',
|
||||
proxyParam: 'vz',
|
||||
timeout: 20000,
|
||||
sourcesTimeout: 10000,
|
||||
jitter: 400,
|
||||
retries: 3,
|
||||
verifyHeaders: {
|
||||
Accept: '/',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
Referer: 'https://player.vidzee.wtf',
|
||||
Origin: 'https://player.vidzee.wtf',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
key: 'vixsrc',
|
||||
label: 'VixSrc',
|
||||
sourceFile: 'vixsrc',
|
||||
proxyParam: 'vx',
|
||||
timeout: 35000,
|
||||
jitter: 0,
|
||||
retries: 2,
|
||||
multiUrl: false,
|
||||
skipProxy: true,
|
||||
verifyHeaders: {
|
||||
Accept: 'application/json, text/javascript, /; q=0.01',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
Referer: 'https://vixsrc.to/',
|
||||
Origin: 'https://vixsrc.to',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
key: 'xdownloader',
|
||||
label: 'XDownloader',
|
||||
sourceFile: 'xdownloader',
|
||||
proxyParam: 'xd',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /films365\.org/i,
|
||||
headers: {
|
||||
Referer: 'https://www.films365.org/',
|
||||
Origin: 'https://www.films365.org',
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
export const HEALTH_PROBE_ID = '155';
|
||||
export const SOURCE_MAP = Object.fromEntries(SOURCES.map(s => [s.key, s]));
|
||||
export default SOURCES;
|
||||
@@ -1,53 +0,0 @@
|
||||
import SOURCES from "../config.js";
|
||||
|
||||
export async function fetchDownloads(sdkOrId, idOrS = null, sOrE = null, maybeE = null) {
|
||||
let sdk = null;
|
||||
let id = sdkOrId;
|
||||
let s = idOrS;
|
||||
let e = sOrE;
|
||||
|
||||
if (sdkOrId && typeof sdkOrId === 'object' && typeof sdkOrId.getStream === 'function') {
|
||||
sdk = sdkOrId;
|
||||
id = idOrS;
|
||||
s = sOrE;
|
||||
e = maybeE;
|
||||
}
|
||||
|
||||
if (!sdk || !id) return [];
|
||||
|
||||
const activeSources = SOURCES.filter(cfg => !cfg.disabled && cfg.skipProxy);
|
||||
const seenUrls = new Set();
|
||||
const downloads = [];
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
activeSources.map(async (cfg) => {
|
||||
try {
|
||||
const res = await sdk.getStream(cfg.key, id, s, e);
|
||||
if (!res) return;
|
||||
|
||||
const items = Array.isArray(res.allUrls) && res.allUrls.length > 0
|
||||
? res.allUrls
|
||||
: (res.url ? [{ url: res.url, label: cfg.label }] : []);
|
||||
|
||||
for (const item of items) {
|
||||
if (!item?.url || typeof item.url !== 'string') continue;
|
||||
const url = item.url.trim();
|
||||
|
||||
if (!url.startsWith('http') || url.includes('.m3u8')) continue;
|
||||
if (!/\.mp4(\?|$)/i.test(url) && !url.includes('.mp4')) continue;
|
||||
if (seenUrls.has(url)) continue;
|
||||
|
||||
seenUrls.add(url);
|
||||
|
||||
downloads.push({
|
||||
source: cfg.key,
|
||||
label: item.label || cfg.label,
|
||||
url,
|
||||
});
|
||||
}
|
||||
} catch { }
|
||||
})
|
||||
);
|
||||
|
||||
return downloads;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { HEALTH_PROBE_ID } from "./config.js";
|
||||
|
||||
const withTimeout = (promise, ms) => Promise.race([promise, new Promise(r => setTimeout(() => r(null), ms))]);
|
||||
async function withRetry(fn, attempts = 2, delay = 300) {
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
try {
|
||||
const result = await fn();
|
||||
if (result != null) return result;
|
||||
} catch (err) {
|
||||
if (i === attempts - 1) throw err;
|
||||
await new Promise(r => setTimeout(r, delay + Math.random() * delay * 0.5));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function probeSource(sdk, cfg, mod) {
|
||||
const audio = /dub$/.test(cfg.key) ? 'dub' : 'sub';
|
||||
const streamArgs = {
|
||||
id: HEALTH_PROBE_ID,
|
||||
s: null,
|
||||
e: null,
|
||||
clientIP: null,
|
||||
absoluteBase: null,
|
||||
audio,
|
||||
sdk,
|
||||
config: cfg,
|
||||
};
|
||||
|
||||
const t = Date.now();
|
||||
const result = await withTimeout(
|
||||
withRetry(() => mod.getStream(streamArgs), cfg.retries, 300),
|
||||
cfg.timeout
|
||||
);
|
||||
|
||||
const firstUrl = result?.url ?? result?.allUrls?.[0]?.url;
|
||||
return { ok: !!(typeof firstUrl === 'string' && firstUrl.startsWith('http')), ms: Date.now() - t };
|
||||
}
|
||||
122
.build/sdk/sdk.d.ts
vendored
122
.build/sdk/sdk.d.ts
vendored
@@ -1,122 +0,0 @@
|
||||
export interface SourceConfig {
|
||||
key: string;
|
||||
label: string;
|
||||
sourceFile: string;
|
||||
proxyParam: string;
|
||||
timeout: number;
|
||||
jitter: number;
|
||||
retries: number;
|
||||
multiUrl: boolean;
|
||||
skipProxy?: boolean;
|
||||
cdnHeaders?: {
|
||||
pattern: RegExp;
|
||||
headers: {
|
||||
Referer: string;
|
||||
Origin: string;
|
||||
};
|
||||
}[];
|
||||
skipVerify?: boolean;
|
||||
verifyHeaders?: boolean;
|
||||
disabled?: boolean;
|
||||
sourcesTimeout?: number;
|
||||
}
|
||||
export interface ProbeResult {
|
||||
ok: boolean;
|
||||
ms: number | null;
|
||||
}
|
||||
export interface StreamResult {
|
||||
url: string;
|
||||
headers: {
|
||||
"User-Agent": string;
|
||||
Referer: string;
|
||||
Origin: string;
|
||||
[key: string]: string;
|
||||
};
|
||||
server: string;
|
||||
quality: string;
|
||||
type: "mp4" | "hls";
|
||||
skipVerify?: boolean;
|
||||
skipProxy?: boolean;
|
||||
}
|
||||
|
||||
export interface DownloadResult {
|
||||
url: string;
|
||||
quality: string;
|
||||
size: number | null;
|
||||
type: "mkv";
|
||||
active: boolean;
|
||||
}
|
||||
export interface SubtitleResult {
|
||||
label: string;
|
||||
file: string;
|
||||
type: "vtt" | "srt";
|
||||
source: string;
|
||||
}
|
||||
|
||||
export type ProbeAllResult = Record<string, ProbeResult>;
|
||||
|
||||
export interface VylaSDKOptions {
|
||||
tmdbApiKey?: string;
|
||||
}
|
||||
|
||||
export default class VylaSDK {
|
||||
tmdbApiKey?: string;
|
||||
|
||||
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,
|
||||
s?: number | null,
|
||||
e?: number | null,
|
||||
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,
|
||||
e?: number | null
|
||||
): Promise<DownloadResult[]>;
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import SOURCES, { SOURCE_MAP } from "./config.js";
|
||||
import { fetchDownloads } from "./downloads/downloads.js";
|
||||
import { probeSource } from "./health.js";
|
||||
import { fetchSubtitles, getSubPathsMovie, getSubPathsTv } from "./subtitles.js";
|
||||
import { createStreamArgs } from "./utils/helpers.js";
|
||||
|
||||
const ALL_SOURCE_MODULES = Object.fromEntries(
|
||||
await Promise.all(SOURCES.map(async cfg => [cfg.key, await import(`./sources/${cfg.sourceFile}.js`)]))
|
||||
);
|
||||
|
||||
const SOURCE_MODULES = Object.fromEntries(
|
||||
Object.entries(ALL_SOURCE_MODULES).filter(([key]) => !SOURCE_MAP[key]?.disabled)
|
||||
);
|
||||
|
||||
export default class VylaSDK {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
import { getUA } from '../utils/helpers.js';
|
||||
|
||||
const SERVICE_ORIGIN = 'https://st.111477.xyz';
|
||||
|
||||
function generateManifestBaseUrl(tmdbKey) {
|
||||
let config = 'https://a.111477.xyz/::sort=file-desc::limit=3';
|
||||
if (tmdbKey) {
|
||||
config += `::tmdb=${tmdbKey}`;
|
||||
}
|
||||
const b64 = Buffer.from(config, 'utf8')
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '');
|
||||
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);
|
||||
const ua = getUA();
|
||||
const apiKey = sdk?.tmdbApiKey || process.env.TMDB_API_KEY;
|
||||
|
||||
const imdbId = await resolveImdbId(sdk, id, isTv);
|
||||
const targetIds = [];
|
||||
|
||||
if (imdbId && imdbId.startsWith('tt')) {
|
||||
targetIds.push(imdbId);
|
||||
}
|
||||
targetIds.push(`tmdb:${id}`);
|
||||
|
||||
const addonBase = generateManifestBaseUrl(apiKey);
|
||||
const seenUrls = new Set();
|
||||
const allUrls = [];
|
||||
|
||||
const defaultHeaders = {
|
||||
'User-Agent': ua,
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
};
|
||||
|
||||
for (const targetId of targetIds) {
|
||||
const endpoint = isTv
|
||||
? `${addonBase}/stream/series/${targetId}:${s || 1}:${e || 1}.json`
|
||||
: `${addonBase}/stream/movie/${targetId}.json`;
|
||||
|
||||
try {
|
||||
const res = await fetch(endpoint, {
|
||||
headers: defaultHeaders,
|
||||
signal: AbortSignal.timeout(12000),
|
||||
});
|
||||
|
||||
if (!res.ok) continue;
|
||||
const data = await res.json();
|
||||
|
||||
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;
|
||||
|
||||
seenUrls.add(url);
|
||||
|
||||
const rawTitle = item.title ? String(item.title).trim() : '';
|
||||
const rawName = item.name ? String(item.name).trim() : '111477';
|
||||
const label = rawTitle || rawName;
|
||||
|
||||
allUrls.push({
|
||||
url,
|
||||
label,
|
||||
headers: defaultHeaders,
|
||||
});
|
||||
}
|
||||
|
||||
if (allUrls.length > 0) break;
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
if (allUrls.length === 0) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
allUrls,
|
||||
headers: allUrls[0].headers,
|
||||
};
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import { getTmdbInfo, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
async function extractVoe(voeUrl) {
|
||||
try {
|
||||
let res = await fetch(voeUrl, {
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Referer': 'https://anihq.cc/'
|
||||
}
|
||||
});
|
||||
|
||||
let html = await res.text();
|
||||
|
||||
const redirectMatch = html.match(/window\.location\.href\s*=\s*['"](https?:\/\/[^'"]+)['"]/);
|
||||
if (redirectMatch) {
|
||||
res = await fetch(redirectMatch[1], {
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Referer': 'https://anihq.cc/'
|
||||
}
|
||||
});
|
||||
html = await res.text();
|
||||
}
|
||||
|
||||
const mirrorUrl = res.url;
|
||||
|
||||
const scriptMatch = html.match(/<script type="application\/json">\["(.*?)"\]<\/script>/);
|
||||
if (!scriptMatch) return null;
|
||||
|
||||
let str = scriptMatch[1];
|
||||
|
||||
str = str.replace(/[a-zA-Z]/g, (c) => {
|
||||
return String.fromCharCode((c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + 13) ? c : c - 26);
|
||||
});
|
||||
|
||||
const junk = ['@$', '^^', '~@', '%?', '*~', '!!', '#&'];
|
||||
junk.forEach(j => { str = str.split(j).join(''); });
|
||||
|
||||
str = Buffer.from(str, 'base64').toString('utf-8');
|
||||
|
||||
let shifted = '';
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
shifted += String.fromCharCode(str.charCodeAt(i) - 3);
|
||||
}
|
||||
|
||||
let reversed = shifted.split('').reverse().join('');
|
||||
|
||||
let finalJsonStr = Buffer.from(reversed, 'base64').toString('utf-8');
|
||||
|
||||
const finalData = JSON.parse(finalJsonStr);
|
||||
|
||||
if (finalData.file || finalData.source) {
|
||||
return {
|
||||
url: finalData.file || finalData.source,
|
||||
mirror: mirrorUrl
|
||||
};
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function cleanSlug(title) {
|
||||
return title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e, audio }) {
|
||||
if (!s) return null;
|
||||
|
||||
const info = await getTmdbInfo(id, 'tv', s);
|
||||
if (!info.isAnime) return null;
|
||||
|
||||
const seriesTitle = info.titles.find(t =>
|
||||
t.toLowerCase() !== 'east blue' &&
|
||||
t.length > 3
|
||||
) || info.titles[0];
|
||||
|
||||
const cleanTitle = cleanSlug(seriesTitle);
|
||||
const typeSuffix = audio === 'dub' ? 'english-dubbed' : 'english-subbed';
|
||||
const url = `https://anihq.cc/watch/${cleanTitle}-episode-${e}-${typeSuffix}/`;
|
||||
|
||||
try {
|
||||
let pageRes = await fetch(url, { headers: { 'User-Agent': USER_AGENT } });
|
||||
let html = await pageRes.text();
|
||||
|
||||
if (!pageRes.ok) {
|
||||
const searchUrl = `https://anihq.cc/search?keyword=${encodeURIComponent(seriesTitle)}`;
|
||||
const searchRes = await fetch(searchUrl, { headers: { 'User-Agent': USER_AGENT } });
|
||||
const searchHtml = await searchRes.text();
|
||||
const searchRegex = new RegExp(`href="(https://anihq.cc/watch/[^"]+-episode-${e}-${typeSuffix}/)"`, 'i');
|
||||
const match = searchHtml.match(searchRegex);
|
||||
if (match) {
|
||||
const retryRes = await fetch(match[1], { headers: { 'User-Agent': USER_AGENT } });
|
||||
html = await retryRes.text();
|
||||
} else return null;
|
||||
}
|
||||
|
||||
const iframeMatch = html.match(/iframe[^>]+src=["'](https:\/\/(?:[a-z0-9.]+|ellenpoliticalfollow\.com)\/e\/[a-zA-Z0-9]+)["']/i);
|
||||
if (!iframeMatch) return null;
|
||||
|
||||
const result = await extractVoe(iframeMatch[1]);
|
||||
|
||||
if (result && result.url) {
|
||||
const mirrorOrigin = new URL(result.mirror).origin;
|
||||
return {
|
||||
url: result.url,
|
||||
headers: {
|
||||
'Referer': mirrorOrigin + '/',
|
||||
'User-Agent': USER_AGENT,
|
||||
'Origin': mirrorOrigin
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
import { getTmdbInfo, USER_AGENT, fetchText } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = "https://anineko.to";
|
||||
|
||||
function cleanTitle(t) { return t ? t.toLowerCase().replace(/[^a-z0-9]/g, '') : ''; }
|
||||
|
||||
function decodeEntities(str) {
|
||||
return str.replace(/&(nbsp|amp|quot|lt|gt);/g, (m, e) => ({ "nbsp": " ", "amp": "&", "quot": "\"", "lt": "<", "gt": ">" })[e]).replace(/&#(\d+);/gi, (m, n) => String.fromCharCode(parseInt(n, 10)));
|
||||
}
|
||||
|
||||
async function search(query) {
|
||||
try {
|
||||
const html = await fetchText(`${BASE_URL}/browser?keyword=${encodeURIComponent(query)}`, {
|
||||
headers: { 'User-Agent': USER_AGENT },
|
||||
signal: AbortSignal.timeout(6000)
|
||||
});
|
||||
const results = [];
|
||||
let idx = 0;
|
||||
while ((idx = html.indexOf('nv-anime-thumb', idx)) !== -1) {
|
||||
const startHref = html.lastIndexOf('<a', idx);
|
||||
if (startHref === -1) { idx += 14; continue; }
|
||||
const hrefMatch = html.slice(startHref, html.indexOf('>', startHref)).match(/href=["']([^"']+)["']/i);
|
||||
if (!hrefMatch) { idx += 14; continue; }
|
||||
const slug = hrefMatch[1].split('/').pop();
|
||||
const h3Start = html.indexOf('nv-anime-title', idx);
|
||||
if (h3Start !== -1) {
|
||||
const contentStart = html.indexOf('>', h3Start) + 1;
|
||||
const contentEnd = html.indexOf('<', contentStart);
|
||||
if (contentEnd !== -1) results.push({ slug, text: html.slice(contentStart, contentEnd).trim() });
|
||||
}
|
||||
idx += 14;
|
||||
}
|
||||
if (results.length === 0) {
|
||||
const linkMatches = [...html.matchAll(/href=["']\/watch\/([^"']+)["']/gi)];
|
||||
for (const match of linkMatches) {
|
||||
if (!results.find(r => r.slug === match[1])) {
|
||||
const linkIdx = html.indexOf(match[0]);
|
||||
const titleStart = html.lastIndexOf('>', linkIdx);
|
||||
const titleEnd = html.indexOf('<', linkIdx);
|
||||
if (titleStart !== -1 && titleEnd !== -1 && titleEnd > linkIdx) {
|
||||
const title = html.slice(titleStart + 1, Math.min(titleEnd, linkIdx + 200)).trim();
|
||||
if (title.length > 2 && title.length < 100) {
|
||||
results.push({ slug: match[1], text: title });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
async function extractHls(embedUrl) {
|
||||
try {
|
||||
const html = await fetchText(embedUrl, {
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
Referer: `${BASE_URL}/`
|
||||
},
|
||||
signal: AbortSignal.timeout(6000)
|
||||
});
|
||||
const m = html.match(/const\s+src\s*=\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i) ||
|
||||
html.match(/file\s*:\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i) ||
|
||||
html.match(/["'](https?:\/\/[^"']+\/master\.m3u8[^"']*)["']/i) ||
|
||||
html.match(/["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i) ||
|
||||
html.match(/src:\s*["'](https?:\/\/[^"']+\.m3u8[^"']*)["']/i);
|
||||
if (m) return decodeEntities(m[1]);
|
||||
} catch { }
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e, audio, sdk }) {
|
||||
const tmdbApiKey = sdk?.tmdbApiKey || null;
|
||||
if (!tmdbApiKey) return null;
|
||||
try {
|
||||
const info = await getTmdbInfo(tmdbApiKey, id, s ? 'tv' : 'movie', s);
|
||||
if (!info || !info.isAnime) return null;
|
||||
let seriesSlug = null;
|
||||
for (const title of info.titles) {
|
||||
const potentialSlug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
||||
if (!potentialSlug) continue;
|
||||
try {
|
||||
const checkHtml = await fetchText(`${BASE_URL}/watch/${potentialSlug}/ep-${e || 1}`, {
|
||||
headers: { 'User-Agent': USER_AGENT, Referer: `${BASE_URL}/watch/${potentialSlug}` },
|
||||
signal: AbortSignal.timeout(4000)
|
||||
});
|
||||
if (checkHtml && checkHtml.includes('nv-watch-page')) {
|
||||
seriesSlug = potentialSlug;
|
||||
break;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (!seriesSlug) {
|
||||
for (const title of info.titles) {
|
||||
const results = await search(title);
|
||||
const targetClean = cleanTitle(title);
|
||||
|
||||
const exactSlugMatch = results.find(r => {
|
||||
const slugClean = cleanTitle(r.slug);
|
||||
return slugClean === targetClean || r.slug === targetClean;
|
||||
});
|
||||
|
||||
if (exactSlugMatch) {
|
||||
seriesSlug = exactSlugMatch.slug;
|
||||
break;
|
||||
}
|
||||
|
||||
const sortedResults = [...results].sort((a, b) => a.slug.length - b.slug.length);
|
||||
|
||||
for (const r of sortedResults) {
|
||||
const resultClean = cleanTitle(r.text);
|
||||
const isExactMatch = resultClean === targetClean;
|
||||
const isCloseMatch = r.text.toLowerCase().includes(title.toLowerCase());
|
||||
if (isExactMatch || isCloseMatch) {
|
||||
seriesSlug = r.slug;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (seriesSlug) break;
|
||||
}
|
||||
}
|
||||
if (!seriesSlug && info.titles.length) {
|
||||
const fSearch = await search(info.titles[0]);
|
||||
const sortedResults = [...fSearch].sort((a, b) => a.slug.length - b.slug.length);
|
||||
if (sortedResults.length) seriesSlug = sortedResults[0].slug;
|
||||
}
|
||||
if (!seriesSlug) return null;
|
||||
const watchHtml = await fetchText(`${BASE_URL}/watch/${seriesSlug}/ep-${e || 1}`, {
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
Referer: `${BASE_URL}/watch/${seriesSlug}`
|
||||
},
|
||||
signal: AbortSignal.timeout(6000)
|
||||
});
|
||||
const byAudio = { sub: [], dub: [] };
|
||||
|
||||
const iframeMatch = watchHtml.match(/<iframe[^>]*src=["']([^"']+)["'][^>]*>/i);
|
||||
if (iframeMatch) {
|
||||
const iframeSrc = decodeEntities(iframeMatch[1]);
|
||||
const hls = await extractHls(iframeSrc);
|
||||
if (hls) {
|
||||
const audioType = audio === 'dub' ? 'dub' : 'sub';
|
||||
byAudio[audioType].push(iframeSrc);
|
||||
}
|
||||
}
|
||||
|
||||
if (byAudio.sub.length === 0 && byAudio.dub.length === 0) {
|
||||
const tabMatches = [...watchHtml.matchAll(/<button[^>]*class=["'][^"']*nv-server-tab[^"']*["'][^>]*data-id=["']([^"']+)["'][^>]*>/gi)];
|
||||
const tabMap = {};
|
||||
for (const match of tabMatches) {
|
||||
const tabId = match[1].toLowerCase();
|
||||
const tabClassMatch = match[0].match(/class=["'][^"']*(tab_\d+)[^"']/i);
|
||||
if (tabClassMatch) {
|
||||
const tabClass = tabClassMatch[1];
|
||||
tabMap[tabClass] = tabId.includes('dub') ? 'dub' : 'sub';
|
||||
}
|
||||
}
|
||||
|
||||
const btnRegex = /<button\s+[^>]*class=["'][^"']*server-video[^"']*["'][^>]*>/gi;
|
||||
const btnMatches = [...watchHtml.matchAll(btnRegex)];
|
||||
for (const match of btnMatches) {
|
||||
const btnHtml = match[0];
|
||||
const videoMatch = btnHtml.match(/data-video=["']([^"']+)["']/i);
|
||||
const tabMatch = btnHtml.match(/data-tab=["']([^"']+)["']/i);
|
||||
if (videoMatch && tabMatch) {
|
||||
const videoUrl = decodeEntities(videoMatch[1]);
|
||||
const tabClass = tabMatch[1];
|
||||
const audioType = tabMap[tabClass] || 'sub';
|
||||
byAudio[audioType].push(videoUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
const audiosToTry = audio === "all" ? ["sub", "dub"] : (audio === "dub" ? ["dub", "sub"] : ["sub", "dub"]);
|
||||
const allUrls = [];
|
||||
for (const aud of audiosToTry) {
|
||||
for (const embed of byAudio[aud]) {
|
||||
const hls = await extractHls(embed);
|
||||
if (hls) allUrls.push({ url: hls, type: "hls", audio: aud, server: "AniNeko", skipProxy: false });
|
||||
}
|
||||
if (allUrls.length) break;
|
||||
}
|
||||
return allUrls.length ? { allUrls } : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
export async function getSources(args) {
|
||||
const stream = await getStream(args);
|
||||
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
import { USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const _sInit = [23, 27, 30, 31, 86, 28, 19, 86, 9, 18, 19, 21, 26, 27, 86, 120, 114, 120, 116, 86, 9, 18, 19, 21, 26, 27, 122, 89, 25, 23];
|
||||
const _vParam = `_v=${(_sInit[15] ^ 0x5a)}${(_sInit[16] ^ 0x5a)}${(_sInit[17] ^ 0x5a)}${(_sInit[18] ^ 0x5a)}`;
|
||||
|
||||
const nD = '4860ac8bfddb';
|
||||
const aD = '224eff10e662e9635c9f671cf46351dcd69af42b1edd56f5e5fa21751f44b9c8';
|
||||
const Ls = [17, 91, 203, 44, 8, 177, 62, 239, 119, 3, 154, 81, 28, 210, 101, 7];
|
||||
const wa = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
||||
|
||||
const SERVERS = [
|
||||
{ id: 'NIGHT', endpoint: '/server/night' },
|
||||
{ id: 'EMP', endpoint: '/server/emp' },
|
||||
{ id: 'MAIN', endpoint: '/server/vidsrc' }
|
||||
];
|
||||
|
||||
const CORS_PROXY = 'https://eve.pstream.cfd';
|
||||
let cachedToken = null;
|
||||
let tokenExpiry = 0;
|
||||
|
||||
function ab(e) {
|
||||
let t = e >>> 0;
|
||||
t ^= t >>> 16;
|
||||
t = Math.imul(t, 2146121005) >>> 0;
|
||||
t ^= t >>> 15;
|
||||
t = Math.imul(t, 2221713035) >>> 0;
|
||||
return (t ^ (t >>> 16)) >>> 0;
|
||||
}
|
||||
|
||||
function sD(e) {
|
||||
const t = new TextEncoder().encode(aD);
|
||||
const r = Math.max(32, Math.min(128, e + 17));
|
||||
const n = new Uint8Array(r);
|
||||
let a = 2166136261;
|
||||
for (let s = 0; s < r; s += 1) {
|
||||
a ^= t[s % t.length] ?? s;
|
||||
a = ab((a + Ls[s % Ls.length] + ((2654435761 * s) >>> 0)) >>> 0);
|
||||
n[s] = a & 255;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function iD(e) {
|
||||
let t = '';
|
||||
for (let r = 0; r < e.length; r += 3) {
|
||||
const n = e[r];
|
||||
const a = e[r + 1];
|
||||
const s = e[r + 2];
|
||||
t += wa[n >>> 2];
|
||||
t += wa[((3 & n) << 4) | ((a ?? 0) >>> 4)];
|
||||
if (a === undefined) break;
|
||||
t += wa[((15 & a) << 2) | ((s ?? 0) >>> 6)];
|
||||
if (s === undefined) break;
|
||||
t += wa[63 & s];
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
function generateDirectHlsUrl(id, s, e) {
|
||||
const isTv = s != null && e != null;
|
||||
const tmdbId = Math.floor(Number(id));
|
||||
const season = isTv ? Math.floor(Number(s || 1)) : 0;
|
||||
const episode = isTv ? Math.floor(Number(e || 1)) : 0;
|
||||
|
||||
const str = `${nD}:${isTv ? 's' : 'm'}:${tmdbId}:${season}:${episode}`;
|
||||
const a = new TextEncoder().encode(str);
|
||||
const sArr = sD(a.length);
|
||||
const i = new Uint8Array(a.length + 2);
|
||||
i[0] = a.length & 255;
|
||||
i[1] = (a.length >>> 8) & 255;
|
||||
let o = (2654435769 ^ a.length) >>> 0;
|
||||
for (let l = 0; l < a.length; l += 1) {
|
||||
o = ab((o + sArr[l % sArr.length] + Ls[l % Ls.length] + l) >>> 0);
|
||||
i[l + 2] = (a[l] ^ (255 & o)) ^ sArr[(7 * l + 3) % sArr.length];
|
||||
}
|
||||
|
||||
return `https://glendale-plumbing.com/c/v1/${iD(i)}/master.m3u8`;
|
||||
}
|
||||
|
||||
async function fetchInternalToken() {
|
||||
if (cachedToken && Date.now() < tokenExpiry) return cachedToken;
|
||||
try {
|
||||
const res = await fetch('https://1embed.cc/api/token', {
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Referer': 'https://1embed.cc/',
|
||||
'Accept': 'application/json, text/plain, */*'
|
||||
},
|
||||
signal: AbortSignal.timeout(4000)
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data?.token) {
|
||||
cachedToken = data.token;
|
||||
tokenExpiry = Date.now() + 1800 * 1000;
|
||||
return cachedToken;
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
|
||||
try {
|
||||
const proxiedUrl = `${CORS_PROXY}/?destination=${encodeURIComponent('https://1embed.cc/api/token')}`;
|
||||
const res = await fetch(proxiedUrl, {
|
||||
headers: {
|
||||
'X-Referer': 'https://1embed.cc/',
|
||||
'X-User-Agent': USER_AGENT
|
||||
},
|
||||
signal: AbortSignal.timeout(4000)
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data?.token) {
|
||||
cachedToken = data.token;
|
||||
tokenExpiry = Date.now() + 1800 * 1000;
|
||||
return cachedToken;
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, clientIP } = args;
|
||||
const isTv = s != null && e != null;
|
||||
const isMovie = !isTv;
|
||||
const allUrls = [];
|
||||
|
||||
const directHlsUrl = generateDirectHlsUrl(id, s, e);
|
||||
const masterUrlWithMark = `${directHlsUrl}?${_vParam}`;
|
||||
|
||||
allUrls.push({
|
||||
url: masterUrlWithMark,
|
||||
server: 'Bcine (Direct Master)',
|
||||
quality: 'Auto',
|
||||
type: 'hls',
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Referer': 'https://bcine.ru/',
|
||||
'Origin': 'https://bcine.ru',
|
||||
...(clientIP && { 'X-Forwarded-For': clientIP })
|
||||
}
|
||||
});
|
||||
|
||||
const token = await fetchInternalToken();
|
||||
|
||||
await Promise.allSettled(
|
||||
SERVERS.map(async (server) => {
|
||||
const query = isTv
|
||||
? `id=${id}?type=tv&s=${s || 1}&e=${e || 1}`
|
||||
: `id=${id}?type=movie`;
|
||||
|
||||
const stParam = token ? `&_st=${token}` : '';
|
||||
const targetUrl = `https://1embed.cc${server.endpoint}/${query}${stParam}`;
|
||||
|
||||
const headers = {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Referer': 'https://bcine.ru/',
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
...(token && { 'X-Stream-Token': token }),
|
||||
...(clientIP && { 'X-Forwarded-For': clientIP })
|
||||
};
|
||||
|
||||
let res = await fetch(targetUrl, { headers, signal: AbortSignal.timeout(5000) }).catch(() => null);
|
||||
if (!res || !res.ok) {
|
||||
const proxiedUrl = `${CORS_PROXY}/?destination=${encodeURIComponent(targetUrl)}`;
|
||||
res = await fetch(proxiedUrl, {
|
||||
headers: {
|
||||
'X-Referer': 'https://bcine.ru/',
|
||||
'X-User-Agent': USER_AGENT
|
||||
},
|
||||
signal: AbortSignal.timeout(5000)
|
||||
}).catch(() => null);
|
||||
}
|
||||
|
||||
if (res && res.ok) {
|
||||
try {
|
||||
const data = await res.json();
|
||||
if (data && data.success !== false) {
|
||||
const streamUrl = data.streams?.proxy_m3u8 || data.streams?.raw_m3u8 || data.streams?.m3u8 || data.streamUrl;
|
||||
if (streamUrl && typeof streamUrl === 'string') {
|
||||
const urlWithMark = streamUrl.includes('?') ? `${streamUrl}&${_vParam}` : `${streamUrl}?${_vParam}`;
|
||||
allUrls.push({
|
||||
url: urlWithMark,
|
||||
server: `Bcine (${data.selectedSource || data.provider || server.id})`,
|
||||
quality: 'Auto',
|
||||
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Referer': 'https://bcine.ru/',
|
||||
'Origin': 'https://bcine.ru'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return allUrls.length ? { allUrls } : null;
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
import { getUA } 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;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSource(url, headers) {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers,
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
return await res.json();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e, sdk }) {
|
||||
const isTv = Boolean(s && e);
|
||||
const mediaType = isTv ? 'tv' : 'movie';
|
||||
const ua = getUA();
|
||||
|
||||
const details = await resolveDetails(sdk, id, isTv);
|
||||
const title = details?.title || '';
|
||||
const year = details?.year || '';
|
||||
const runtime = details?.runtime || '';
|
||||
|
||||
const referer = isTv
|
||||
? `${BASE_URL}/watch/tv/${id}/${s}/${e}`
|
||||
: `${BASE_URL}/watch/movie/${id}`;
|
||||
|
||||
const defaultHeaders = {
|
||||
'User-Agent': ua,
|
||||
Referer: referer,
|
||||
Origin: BASE_URL,
|
||||
Accept: '*/*',
|
||||
'Sec-Fetch-Dest': 'empty',
|
||||
'Sec-Fetch-Mode': 'cors',
|
||||
'Sec-Fetch-Site': 'same-origin',
|
||||
};
|
||||
|
||||
const baseParams = new URLSearchParams({
|
||||
tmdbId: String(id),
|
||||
type: mediaType,
|
||||
});
|
||||
|
||||
const additionalParams = new URLSearchParams({
|
||||
tmdbId: String(id),
|
||||
type: mediaType,
|
||||
progressive: 'true',
|
||||
});
|
||||
|
||||
if (title) additionalParams.set('title', title);
|
||||
if (year) additionalParams.set('year', String(year));
|
||||
if (runtime) additionalParams.set('runtime', String(runtime));
|
||||
|
||||
if (isTv) {
|
||||
baseParams.set('season', String(s));
|
||||
baseParams.set('episode', String(e));
|
||||
additionalParams.set('season', String(s));
|
||||
additionalParams.set('episode', String(e));
|
||||
}
|
||||
|
||||
const endpoints = [
|
||||
`${BASE_URL}/api/sources?${baseParams.toString()}`,
|
||||
`${BASE_URL}/api/sources/willow?${baseParams.toString()}`,
|
||||
`${BASE_URL}/api/sources/additional?${additionalParams.toString()}`,
|
||||
];
|
||||
|
||||
const seenUrls = new Set();
|
||||
const allUrls = [];
|
||||
|
||||
let pendingUrls = [...endpoints];
|
||||
const maxPollTime = Date.now() + 16000;
|
||||
|
||||
while (pendingUrls.length > 0 && Date.now() < maxPollTime) {
|
||||
const results = await Promise.all(
|
||||
pendingUrls.map((url) => fetchSource(url, defaultHeaders))
|
||||
);
|
||||
|
||||
const nextPending = [];
|
||||
|
||||
results.forEach((data, index) => {
|
||||
if (!data) return;
|
||||
|
||||
if (Array.isArray(data.sources) && data.sources.length > 0) {
|
||||
for (const src of data.sources) {
|
||||
let streamUrl = String(src?.url || '').trim();
|
||||
if (!streamUrl) continue;
|
||||
|
||||
if (streamUrl.startsWith('/')) {
|
||||
streamUrl = `${BASE_URL}${streamUrl}`;
|
||||
}
|
||||
|
||||
if (!streamUrl.startsWith('http') || seenUrls.has(streamUrl)) continue;
|
||||
seenUrls.add(streamUrl);
|
||||
|
||||
allUrls.push({
|
||||
url: streamUrl,
|
||||
label: src.label || 'Dulo',
|
||||
headers: {
|
||||
'User-Agent': ua,
|
||||
Referer: referer,
|
||||
},
|
||||
captions: Array.isArray(src.captions) ? src.captions : [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (data.pending) {
|
||||
nextPending.push(pendingUrls[index]);
|
||||
}
|
||||
});
|
||||
|
||||
pendingUrls = nextPending;
|
||||
if (pendingUrls.length > 0) {
|
||||
await sleep(1500);
|
||||
}
|
||||
}
|
||||
|
||||
if (allUrls.length === 0) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
allUrls,
|
||||
headers: allUrls[0].headers,
|
||||
};
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import { USER_AGENT, fetchJson } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = 'https://flaxmovies.xyz';
|
||||
|
||||
async function getWorkerUrls(embedUrl) {
|
||||
const foundUrls = [];
|
||||
try {
|
||||
const res = await fetch(embedUrl, {
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Referer': `${BASE_URL}/`,
|
||||
'Origin': BASE_URL
|
||||
},
|
||||
signal: AbortSignal.timeout(8000)
|
||||
});
|
||||
if (res.ok) {
|
||||
const html = await res.text();
|
||||
const directMatch = html.match(/WORKER_URL\s*[:=]\s*["'](https?:\/\/[^"']+)["']/i);
|
||||
if (directMatch && directMatch[1]) {
|
||||
foundUrls.push(directMatch[1].replace(/\/+$/, ''));
|
||||
}
|
||||
|
||||
const scriptMatches = [...html.matchAll(/<script[^>]+src=["']([^"']+)["']/gi)];
|
||||
for (const sm of scriptMatches) {
|
||||
let scriptUrl = sm[1];
|
||||
if (scriptUrl.startsWith('//')) scriptUrl = `https:${scriptUrl}`;
|
||||
else if (scriptUrl.startsWith('/')) scriptUrl = `${BASE_URL}${scriptUrl}`;
|
||||
else if (!scriptUrl.startsWith('http')) scriptUrl = `${BASE_URL}/${scriptUrl}`;
|
||||
|
||||
if (scriptUrl.includes('jsdelivr') || scriptUrl.includes('google') || scriptUrl.includes('cloudflare')) continue;
|
||||
|
||||
try {
|
||||
const sRes = await fetch(scriptUrl, {
|
||||
headers: { 'User-Agent': USER_AGENT, 'Referer': embedUrl },
|
||||
signal: AbortSignal.timeout(6000)
|
||||
});
|
||||
if (sRes.ok) {
|
||||
const sCode = await sRes.text();
|
||||
const m = sCode.match(/WORKER_URL\s*[:=]\s*["'](https?:\/\/[^"']+)["']/i);
|
||||
if (m && m[1]) {
|
||||
const wUrl = m[1].replace(/\/+$/, '');
|
||||
if (!foundUrls.includes(wUrl)) foundUrls.push(wUrl);
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
|
||||
const defaults = ['https://freakyniki.elaxo.lol', 'https://vidlove.nabilekson.workers.dev'];
|
||||
for (const d of defaults) {
|
||||
if (!foundUrls.includes(d)) foundUrls.push(d);
|
||||
}
|
||||
return foundUrls;
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, server } = args;
|
||||
const isTv = s != null && e != null;
|
||||
const embedUrl = isTv
|
||||
? `${BASE_URL}/embed/tv/${id}/${s}/${e}`
|
||||
: `${BASE_URL}/embed/movie/${id}`;
|
||||
|
||||
const workerUrls = await getWorkerUrls(embedUrl);
|
||||
const query = isTv
|
||||
? `tmdb_id=${id}&tmdbId=${id}&season=${s}&episode=${e}`
|
||||
: `tmdb_id=${id}&tmdbId=${id}`;
|
||||
|
||||
for (const worker of workerUrls) {
|
||||
try {
|
||||
const url = `${worker}/?${query}`;
|
||||
const data = await fetchJson(url, {
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Referer': `${BASE_URL}/`,
|
||||
'Origin': BASE_URL,
|
||||
'Accept': 'application/json, text/plain, */*'
|
||||
},
|
||||
signal: AbortSignal.timeout(12000)
|
||||
});
|
||||
|
||||
if (data && Array.isArray(data.streams) && data.streams.length > 0) {
|
||||
let allUrls = data.streams
|
||||
.filter(st => st && typeof st.url === 'string')
|
||||
.map(st => {
|
||||
const isHls = (st.format || '').toLowerCase().includes('hls') || st.url.includes('.m3u8');
|
||||
return {
|
||||
url: st.url,
|
||||
server: `FlaxMovies - ${st.provider || 'Default'}`,
|
||||
quality: st.resolution || 'Auto',
|
||||
type: isHls ? 'hls' : 'mp4',
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Referer': `${BASE_URL}/`,
|
||||
'Origin': BASE_URL
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
if (server && server !== 'all') {
|
||||
const clean = server.toLowerCase().replace('flaxmovies - ', '');
|
||||
const filtered = allUrls.filter(u => u.server.toLowerCase().includes(clean));
|
||||
if (filtered.length) allUrls = filtered;
|
||||
}
|
||||
|
||||
if (allUrls.length) {
|
||||
return { allUrls };
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getSources() {
|
||||
return ['FlaxMovies - Airflix', 'FlaxMovies - Xpass-VIP 1'];
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
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))] : [];
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
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))] : [];
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { getTmdbInfo } from '../utils/helpers.js';
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, tmdbApiKey } = args;
|
||||
const type = (s != null && e != null) ? 'tv' : 'movie';
|
||||
|
||||
try {
|
||||
const tmdbInfo = await getTmdbInfo(tmdbApiKey, id, type, s);
|
||||
const name = tmdbInfo.titles[0] || '';
|
||||
if (!name) return null;
|
||||
|
||||
if (type === 'tv') {
|
||||
return null;
|
||||
} else {
|
||||
const url = `https://lmscript.xyz/v1/movies?filters[q]=${encodeURIComponent(name)}&expand=streams`;
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
const movie = data.items?.find(item => `${item.tmdb_prefix}` === `${id}` || `${item.tmdb_id}` === `${id}`);
|
||||
if (!movie) return null;
|
||||
|
||||
const streams = movie.streams || {};
|
||||
const SOURCE_ORDER = ["1080p", "720p", "480p", "360p"];
|
||||
const sortedStreams = Object.keys(streams).sort((a, b) => SOURCE_ORDER.indexOf(a) - SOURCE_ORDER.indexOf(b));
|
||||
|
||||
if (!sortedStreams.length) return null;
|
||||
|
||||
const allUrls = sortedStreams.map(quality => {
|
||||
const streamUrl = streams[quality];
|
||||
return {
|
||||
url: streamUrl,
|
||||
quality,
|
||||
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: {}
|
||||
};
|
||||
});
|
||||
|
||||
return { allUrls };
|
||||
}
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
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,117 +0,0 @@
|
||||
import { getUA } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = 'https://movienig.ht';
|
||||
|
||||
const PRIORITY_SERVERS = [
|
||||
{ id: 'dallas', label: 'Dallas 4K' },
|
||||
{ id: 'austin', label: 'Austin' },
|
||||
{ id: 'helena', label: 'Helena' },
|
||||
{ id: 'seattle', label: 'Seattle 4K' },
|
||||
{ id: 'vixsrc-1', label: 'Newport Beach' },
|
||||
{ id: 'tucson', label: 'Tucson' },
|
||||
{ id: 'salem', label: 'Salem' },
|
||||
];
|
||||
|
||||
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}&append_to_response=external_ids`, {
|
||||
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 ? parseInt(releaseDate.split('-')[0], 10) : null;
|
||||
const imdbId = data.imdb_id || data.external_ids?.imdb_id || null;
|
||||
return { title, year, imdbId };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e, sdk }) {
|
||||
const isTv = Boolean(s && e);
|
||||
const ua = getUA();
|
||||
|
||||
const details = await resolveDetails(sdk, id, isTv);
|
||||
const title = details?.title || String(id);
|
||||
const year = details?.year;
|
||||
const imdbId = details?.imdbId;
|
||||
|
||||
const encTitle = encodeURIComponent(title);
|
||||
const yearQuery = year ? `&year=${year}` : '';
|
||||
const imdbQuery = imdbId ? `&imdbId=${imdbId}` : '';
|
||||
|
||||
const seenUrls = new Set();
|
||||
const allUrls = [];
|
||||
|
||||
const defaultHeaders = {
|
||||
'User-Agent': ua,
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
Accept: 'text/event-stream',
|
||||
};
|
||||
|
||||
const tasks = PRIORITY_SERVERS.map(async (server) => {
|
||||
try {
|
||||
const url = isTv
|
||||
? `${BASE_URL}/api/stream/v1/tv/${id}/${s}/${e}?title=${encTitle}${yearQuery}${imdbQuery}&server=${server.id}&only=1`
|
||||
: `${BASE_URL}/api/stream/v1/movie/${id}?title=${encTitle}${yearQuery}${imdbQuery}&server=${server.id}&only=1`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
headers: defaultHeaders,
|
||||
signal: AbortSignal.timeout(6000),
|
||||
});
|
||||
|
||||
if (!res.ok) return;
|
||||
const bodyStr = await res.text();
|
||||
|
||||
if (bodyStr.includes('event: done')) {
|
||||
const doneIdx = bodyStr.indexOf('event: done');
|
||||
const dataIdx = bodyStr.indexOf('data: ', doneIdx);
|
||||
|
||||
if (dataIdx !== -1) {
|
||||
const jsonStart = dataIdx + 6;
|
||||
const jsonEnd = bodyStr.indexOf('\n', jsonStart);
|
||||
const jsonText = (jsonEnd !== -1 ? bodyStr.substring(jsonStart, jsonEnd) : bodyStr.substring(jsonStart)).trim();
|
||||
|
||||
const data = JSON.parse(jsonText);
|
||||
if (Array.isArray(data?.sources)) {
|
||||
for (const src of data.sources) {
|
||||
if (!src || typeof src !== 'object') continue;
|
||||
const rawUrl = String(src.url || '').trim();
|
||||
if (!rawUrl || seenUrls.has(rawUrl)) continue;
|
||||
|
||||
seenUrls.add(rawUrl);
|
||||
|
||||
const quality = src.quality ? String(src.quality) : 'Auto';
|
||||
const titleQuality = quality !== 'Auto' ? ` (${quality})` : '';
|
||||
|
||||
allUrls.push({
|
||||
url: rawUrl,
|
||||
label: `MovieNight ${server.label}${titleQuality}`,
|
||||
headers: {
|
||||
'User-Agent': ua,
|
||||
Referer: `${BASE_URL}/`,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
});
|
||||
|
||||
await Promise.all(tasks);
|
||||
|
||||
if (allUrls.length === 0) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
allUrls,
|
||||
headers: allUrls[0].headers,
|
||||
};
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
import { getUA } from '../utils/helpers.js';
|
||||
|
||||
const API_BASE = 'https://api.wecollege.net';
|
||||
const REFERER = 'https://www.movy.bz/';
|
||||
const ORIGIN = 'https://www.movy.bz';
|
||||
const MAGIC = [109, 118, 109, 49];
|
||||
|
||||
const SERVERS = [
|
||||
{ endpoint: 'miami', name: 'Miami', note: 'Original audio (Up to 4K)' },
|
||||
{ endpoint: 'seattle', name: 'Seattle', note: 'Original audio' },
|
||||
{ endpoint: 'denver', name: 'Denver', note: 'Original audio' },
|
||||
{ endpoint: 'chicago', name: 'Chicago', note: 'Original audio' },
|
||||
{ endpoint: 'dallas', name: 'Dallas', note: 'Original audio' },
|
||||
{ endpoint: 'atlanta', name: 'Atlanta', note: 'Original audio' },
|
||||
{ endpoint: 'houston', name: 'Houston', note: 'Original audio' },
|
||||
{ endpoint: 'austin', name: 'Austin', note: 'Original audio' },
|
||||
{ endpoint: 'boston', name: 'Boston', note: 'Original audio' },
|
||||
{ endpoint: 'munich', name: 'Munich', note: 'German audio', extra: 'language=german' },
|
||||
{ endpoint: 'berlin', name: 'Berlin', note: 'German audio' },
|
||||
{ endpoint: 'paris', name: 'Paris', note: 'French audio' },
|
||||
{ endpoint: 'delhi', name: 'Delhi', note: 'Hindi audio' },
|
||||
{ endpoint: 'cancun', name: 'Cancun', note: 'Spanish audio' },
|
||||
];
|
||||
|
||||
const seedCache = new Map();
|
||||
|
||||
function _l(e) {
|
||||
let v = e >>> 0;
|
||||
v = (v ^ (v >>> 16)) >>> 0;
|
||||
v = Math.imul(v, 0x85ebca6b) >>> 0;
|
||||
v = (v ^ (v >>> 13)) >>> 0;
|
||||
v = Math.imul(v, 0xc2b2ae35) >>> 0;
|
||||
return (v ^ (v >>> 16)) >>> 0;
|
||||
}
|
||||
|
||||
function _u(e, t) {
|
||||
const shift = t & 31;
|
||||
if (shift === 0) return e >>> 0;
|
||||
return (((e << shift) >>> 0) | (e >>> (32 - shift))) >>> 0;
|
||||
}
|
||||
|
||||
function _fnv1a(str) {
|
||||
let t = 0x811c9dc5 >>> 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const code = str.charCodeAt(i);
|
||||
t = Math.imul((t ^ code) >>> 0, 0x1000193) >>> 0;
|
||||
}
|
||||
return _l(t);
|
||||
}
|
||||
|
||||
function _initKeyState(seed, tmdbId) {
|
||||
const s = new Array(61).fill(0);
|
||||
const isSet = new Array(61).fill(false);
|
||||
const idVal = ((tmdbId >>> 0) ^ 0x9e3779b9) >>> 0;
|
||||
let r = _l((_fnv1a(seed) ^ _l(idVal)) >>> 0);
|
||||
|
||||
for (let e = 0; e < 8; e++) {
|
||||
const t = r % 61;
|
||||
r = _u((r + 0x9e3779b9) >>> 0, 7 + (7 & e));
|
||||
s[t] = (r ^ _l(r)) >>> 0;
|
||||
isSet[t] = true;
|
||||
r = _l((r + t) >>> 0);
|
||||
}
|
||||
|
||||
const acc = _l((0xa5a5a5a5 ^ r) >>> 0);
|
||||
return { s, isSet, acc };
|
||||
}
|
||||
|
||||
function _nextKeystreamWord(state, t) {
|
||||
const r = state.s;
|
||||
let nState = state.acc;
|
||||
const i = nState % 61;
|
||||
const d = state.isSet[i] ? r[i] : 0;
|
||||
const c = Math.imul((t + 1) >>> 0, 0x9e3779b9) >>> 0;
|
||||
const a = nState;
|
||||
const sVal = (d ^ c) >>> 0;
|
||||
const h = state.isSet[i] ? ((a | sVal) >>> 0) : ((a ^ sVal) >>> 0);
|
||||
const term1 = _u((h + nState) >>> 0, 31 & i);
|
||||
const term2 = _u(nState, 31 & (i * 7));
|
||||
nState = _l((((term1 ^ term2) >>> 0) + 0x9e3779b9) >>> 0);
|
||||
r[i] = nState;
|
||||
state.isSet[i] = true;
|
||||
state.acc = nState;
|
||||
return nState >>> 0;
|
||||
}
|
||||
|
||||
function _generateKeyStream(seed, tmdbId, len) {
|
||||
const state = _initKeyState(seed, tmdbId);
|
||||
const out = new Uint8Array(len);
|
||||
let wordIdx = 0;
|
||||
let byteIdx = 0;
|
||||
|
||||
while (byteIdx < len) {
|
||||
const word = _nextKeystreamWord(state, wordIdx++);
|
||||
out[byteIdx++] = word & 0xFF;
|
||||
if (byteIdx < len) out[byteIdx++] = (word >>> 8) & 0xFF;
|
||||
if (byteIdx < len) out[byteIdx++] = (word >>> 16) & 0xFF;
|
||||
if (byteIdx < len) out[byteIdx++] = (word >>> 24) & 0xFF;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function decrypt(cipherB64, seed, tmdbId) {
|
||||
try {
|
||||
let normalized = cipherB64.replace(/-/g, '+').replace(/_/g, '/');
|
||||
while (normalized.length % 4 !== 0) normalized += '=';
|
||||
const cipherBytes = Buffer.from(normalized, 'base64');
|
||||
if (cipherBytes.length <= MAGIC.length) return null;
|
||||
|
||||
const ks = _generateKeyStream(seed, tmdbId, cipherBytes.length);
|
||||
const decrypted = new Uint8Array(cipherBytes.length);
|
||||
for (let i = 0; i < cipherBytes.length; i++) {
|
||||
decrypted[i] = cipherBytes[i] ^ ks[i];
|
||||
}
|
||||
|
||||
for (let k = 0; k < MAGIC.length; k++) {
|
||||
if (decrypted[k] !== MAGIC[k]) return null;
|
||||
}
|
||||
|
||||
return Buffer.from(decrypted.subarray(MAGIC.length)).toString('utf8');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getSeed(tmdbId, ua) {
|
||||
const now = Date.now();
|
||||
const cached = seedCache.get(tmdbId);
|
||||
if (cached && cached.expiresAt > now + 5000) {
|
||||
return cached.seed;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/seed?mediaId=${tmdbId}`, {
|
||||
headers: {
|
||||
'User-Agent': ua,
|
||||
Referer: REFERER,
|
||||
Origin: ORIGIN,
|
||||
},
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const seed = data?.seed ? String(data.seed) : '';
|
||||
const ttlMs = typeof data?.ttlMs === 'number' ? data.ttlMs : 30000;
|
||||
if (seed) {
|
||||
seedCache.set(tmdbId, { seed, expiresAt: now + ttlMs });
|
||||
return seed;
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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}&append_to_response=external_ids`, {
|
||||
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 ? parseInt(releaseDate.split('-')[0], 10) : null;
|
||||
const imdbId = data.imdb_id || data.external_ids?.imdb_id || null;
|
||||
return { title, year, imdbId };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function formatQuality(raw) {
|
||||
const lower = String(raw).toLowerCase();
|
||||
if (lower.includes('2160') || lower.includes('4k')) return '4K';
|
||||
if (lower.includes('1080')) return '1080p';
|
||||
if (lower.includes('720')) return '720p';
|
||||
if (lower.includes('480')) return '480p';
|
||||
if (lower.includes('360')) return '360p';
|
||||
if (raw) return String(raw);
|
||||
return 'Auto';
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e, sdk }) {
|
||||
const isTv = Boolean(s && e);
|
||||
const mediaType = isTv ? 'tv' : 'movie';
|
||||
const tmdbId = parseInt(id, 10);
|
||||
const ua = getUA();
|
||||
|
||||
const details = await resolveDetails(sdk, id, isTv);
|
||||
const title = details?.title || String(id);
|
||||
const year = details?.year;
|
||||
const imdbId = details?.imdbId;
|
||||
|
||||
const seed = await getSeed(tmdbId, ua);
|
||||
if (!seed) return null;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
title,
|
||||
mediaType,
|
||||
tmdbId: String(tmdbId),
|
||||
enc: '2',
|
||||
seed,
|
||||
});
|
||||
|
||||
if (year) params.set('year', String(year));
|
||||
if (isTv) {
|
||||
if (s) params.set('seasonId', String(s));
|
||||
if (e) params.set('episodeId', String(e));
|
||||
}
|
||||
if (imdbId) params.set('imdbId', imdbId);
|
||||
|
||||
const baseQuery = params.toString();
|
||||
const seenUrls = new Set();
|
||||
const allUrls = [];
|
||||
|
||||
const defaultHeaders = {
|
||||
'User-Agent': ua,
|
||||
Referer: REFERER,
|
||||
Origin: ORIGIN,
|
||||
};
|
||||
|
||||
const tasks = SERVERS.map(async (server) => {
|
||||
let fullUrl = `${API_BASE}/${server.endpoint}/sources?${baseQuery}`;
|
||||
if (server.extra) fullUrl += `&${server.extra}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(fullUrl, {
|
||||
headers: defaultHeaders,
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
|
||||
if (!res.ok) return;
|
||||
const encText = (await res.text()).trim();
|
||||
if (!encText || encText.startsWith('<')) return;
|
||||
|
||||
const decJsonStr = decrypt(encText, seed, tmdbId);
|
||||
if (!decJsonStr) return;
|
||||
|
||||
const parsed = JSON.parse(decJsonStr);
|
||||
if (!Array.isArray(parsed?.sources)) return;
|
||||
|
||||
for (const src of parsed.sources) {
|
||||
if (!src || typeof src !== 'object') continue;
|
||||
const streamUrl = String(src.url || '').trim();
|
||||
if (!streamUrl || seenUrls.has(streamUrl)) continue;
|
||||
|
||||
seenUrls.add(streamUrl);
|
||||
|
||||
const cleanQuality = formatQuality(src.quality || 'Auto');
|
||||
const isSpecificAudio = server.note.toLowerCase().includes('audio') && !server.note.toLowerCase().includes('original');
|
||||
const langLabel = isSpecificAudio ? server.note.split(' ')[0] : '';
|
||||
const label = isSpecificAudio
|
||||
? `${server.name} (${langLabel}) · ${cleanQuality}`
|
||||
: `${server.name} · ${cleanQuality}`;
|
||||
|
||||
allUrls.push({
|
||||
url: streamUrl,
|
||||
label,
|
||||
headers: {
|
||||
'User-Agent': ua,
|
||||
Referer: REFERER,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch { }
|
||||
});
|
||||
|
||||
await Promise.all(tasks);
|
||||
|
||||
if (allUrls.length === 0) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
allUrls,
|
||||
headers: allUrls[0].headers,
|
||||
};
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
async function safeImport(moduleName) {
|
||||
try {
|
||||
return await import(`./${moduleName}.js`);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, clientIP, sdk } = args;
|
||||
const isTv = s != null && e != null;
|
||||
const type = isTv ? 'tv' : 'movie';
|
||||
const allUrls = [];
|
||||
|
||||
const providers = [
|
||||
{ name: 'CineSu', key: 'cinesu', fnName: 'fetchCineSuStreams' },
|
||||
{ name: 'ZxcStream', key: 'zxcstream', fnName: 'fetchZxcStreams' },
|
||||
{ name: 'VidFast', key: 'vidfast', fnName: 'fetchVidFastStreams' },
|
||||
{ name: '1Embed', key: '1embed', fnName: 'fetchOneEmbedStreams' },
|
||||
];
|
||||
|
||||
await Promise.all(
|
||||
providers.map(async ({ name, key, fnName }) => {
|
||||
try {
|
||||
let streamResult = null;
|
||||
|
||||
if (sdk) {
|
||||
try {
|
||||
streamResult = await sdk.getStream(key, id, s, e, clientIP);
|
||||
} catch { }
|
||||
}
|
||||
|
||||
if (!streamResult) {
|
||||
const mod = await safeImport(key);
|
||||
if (mod) {
|
||||
if (typeof mod.getStream === 'function') {
|
||||
streamResult = await mod.getStream(args);
|
||||
} else if (typeof mod[fnName] === 'function') {
|
||||
const raw = await mod[fnName]({ type, id, season: s, episode: e });
|
||||
if (Array.isArray(raw)) {
|
||||
streamResult = { allUrls: raw };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!streamResult) return;
|
||||
|
||||
const streams = streamResult.allUrls || (streamResult.url ? [streamResult] : []);
|
||||
for (const item of streams) {
|
||||
allUrls.push({
|
||||
url: item.url,
|
||||
server: `FlyStream (${item.source || item.server || name})`,
|
||||
quality: item.quality || 'Auto',
|
||||
type: item.type || item.format || (item.url?.includes('.m3u8') ? 'hls' : 'mp4'),
|
||||
headers: item.headers || {
|
||||
'User-Agent': USER_AGENT,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch { }
|
||||
})
|
||||
);
|
||||
|
||||
return allUrls.length ? { allUrls } : null;
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
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
|
||||
};
|
||||
@@ -1,179 +0,0 @@
|
||||
// im being salty
|
||||
|
||||
import { USER_AGENT, fetchJson } from '../utils/helpers.js';
|
||||
|
||||
const C_ARRAY = [
|
||||
'4Z7lUo', 'gwIVSMD', 'PLmz2elE2v', 'Z4OFV0', 'SZ6RZq6Zc', 'zhJEFYxrz8', 'FOm7b0', 'axHS3q4KDq', 'o9zuXQ', '4Aebt',
|
||||
'wgjjWwKKx', 'rY4VIxqSN', 'kfjbnSo', '2DyrFA1M', 'YUixDM9B', 'JQvgEj0', 'mcuFx6JIek', 'eoTKe26gL', 'qaI9EVO1rB', '0xl33btZL',
|
||||
'1fszuAU', 'a7jnHzst6P', 'wQuJkX', 'cBNhTJlEOf', 'KNcFWhDvgT', 'XipDGjST', 'PCZJlbHoyt', '2AYnMZkqd', 'HIpJh', 'KH0C3iztrG',
|
||||
'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'
|
||||
];
|
||||
|
||||
const DEFAULT_SERVICES = [
|
||||
'apex', 'pulse', 'solstice', 'quasar', 'horizon', 'primevids',
|
||||
'flowcast', 'asiacloud', 'citadel', 'hindicast', 'guru'
|
||||
];
|
||||
|
||||
function generateRiveSecretKey(e) {
|
||||
if (e === undefined || e === null) return 'rive';
|
||||
try {
|
||||
let t, n;
|
||||
const r = String(e);
|
||||
if (isNaN(Number(e))) {
|
||||
const sum = r.split('').reduce((acc, ch) => acc + ch.charCodeAt(0), 0);
|
||||
t = C_ARRAY[sum % C_ARRAY.length] || Buffer.from(r).toString('base64');
|
||||
n = Math.floor((sum % r.length) / 2);
|
||||
} else {
|
||||
const i = Number(e);
|
||||
t = C_ARRAY[i % C_ARRAY.length] || Buffer.from(r).toString('base64');
|
||||
n = Math.floor((i % r.length) / 2);
|
||||
}
|
||||
const i = r.slice(0, n) + t + r.slice(n);
|
||||
const o = (function (str) {
|
||||
const t = String(str);
|
||||
let n = 3735928559 ^ t.length;
|
||||
for (let e = 0; e < t.length; e++) {
|
||||
const r = t.charCodeAt(e);
|
||||
const rXor = (131 * e + 89 ^ (r << (e % 5))) & 255;
|
||||
const r2 = r ^ rXor;
|
||||
n = (((n << 7) | (n >>> 25)) >>> 0) ^ r2;
|
||||
const idx = (65535 & n) * 60205;
|
||||
const o2 = ((n >>> 16) * 60205) << 16;
|
||||
n = (idx + o2) >>> 0;
|
||||
n ^= n >>> 11;
|
||||
}
|
||||
n ^= n >>> 15;
|
||||
n = ((65535 & n) * 49842 + (((n >>> 16) * 49842) << 16)) >>> 0;
|
||||
n ^= n >>> 13;
|
||||
n = ((65535 & n) * 40503 + (((n >>> 16) * 40503) << 16)) >>> 0;
|
||||
n ^= n >>> 16;
|
||||
n = ((65535 & n) * 10196 + (((n >>> 16) * 10196) << 16)) >>> 0;
|
||||
return ((n ^= n >>> 15)).toString(16).padStart(8, '0');
|
||||
})(
|
||||
(function (str) {
|
||||
const s = String(str);
|
||||
let t = 0;
|
||||
for (let n = 0; n < s.length; n++) {
|
||||
const r = s.charCodeAt(n);
|
||||
const i = (((t = (r + (t << 6) + (t << 16) - t) >>> 0) << (n % 5)) | (t >>> (32 - (n % 5)))) >>> 0;
|
||||
t ^= (i ^ ((r << (n % 7)) | (r >>> (8 - (n % 7))))) >>> 0;
|
||||
t = (t + ((t >>> 11) ^ (t << 3))) >>> 0;
|
||||
}
|
||||
t ^= t >>> 15;
|
||||
t = ((65535 & t) * 49842 + ((((t >>> 16) * 49842) & 65535) << 16)) >>> 0;
|
||||
t ^= t >>> 13;
|
||||
t = ((65535 & t) * 40503 + ((((t >>> 16) * 40503) & 65535) << 16)) >>> 0;
|
||||
return ((t ^= t >>> 16)).toString(16).padStart(8, '0');
|
||||
})(i)
|
||||
);
|
||||
return Buffer.from(o, 'utf-8').toString('base64');
|
||||
} catch {
|
||||
return 'topSecret';
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRiveServices() {
|
||||
try {
|
||||
const secretKey = generateRiveSecretKey();
|
||||
const url = `https://www.rivestream.app/api/backendfetch?requestID=VideoProviderServices&secretKey=${encodeURIComponent(secretKey)}`;
|
||||
const res = await fetchJson(url, {
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Referer': 'https://www.rivestream.app/',
|
||||
'Accept': 'application/json, text/plain, */*'
|
||||
},
|
||||
signal: AbortSignal.timeout(6000)
|
||||
});
|
||||
return Array.isArray(res?.data) && res.data.length ? res.data : DEFAULT_SERVICES;
|
||||
} catch {
|
||||
return DEFAULT_SERVICES;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchServiceStreams(service, isMovie, id, s, e, secretKey, referer) {
|
||||
try {
|
||||
const cb = (service === 'primevids' || service === 'citadel')
|
||||
? `&cb=${Math.floor(Date.now() / 3e6)}`
|
||||
: '';
|
||||
|
||||
const reqID = isMovie ? 'movieVideoProvider' : 'tvVideoProvider';
|
||||
const params = isMovie
|
||||
? `requestID=${reqID}&id=${encodeURIComponent(id)}&service=${service}${cb}&secretKey=${encodeURIComponent(secretKey)}`
|
||||
: `requestID=${reqID}&id=${encodeURIComponent(id)}&season=${encodeURIComponent(s)}&episode=${encodeURIComponent(e)}&service=${service}${cb}&secretKey=${encodeURIComponent(secretKey)}`;
|
||||
|
||||
const url = `https://www.rivestream.app/api/backendfetch?${params}`;
|
||||
const res = await fetchJson(url, {
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Referer': referer,
|
||||
'Accept': 'application/json, text/plain, */*'
|
||||
},
|
||||
signal: AbortSignal.timeout(10000)
|
||||
});
|
||||
|
||||
if (!res?.data?.sources || !Array.isArray(res.data.sources)) return [];
|
||||
|
||||
const streams = [];
|
||||
for (const item of res.data.sources) {
|
||||
if (!item || !item.url) continue;
|
||||
|
||||
const format = String(item.format || '').toLowerCase();
|
||||
const urlStr = String(item.url || '').toLowerCase();
|
||||
const isM3U8 = format === 'hls' || format === 'm3u8' || urlStr.includes('.m3u8') || urlStr.includes('.txt');
|
||||
|
||||
streams.push({
|
||||
url: item.url,
|
||||
server: `Rive (${item.source || service})`,
|
||||
quality: String(item.quality || 'Auto'),
|
||||
type: isM3U8 ? 'hls' : 'mp4',
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Referer': referer
|
||||
}
|
||||
});
|
||||
}
|
||||
return streams;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, server } = args;
|
||||
const isTv = s != null && e != null;
|
||||
const isMovie = !isTv;
|
||||
|
||||
const secretKey = generateRiveSecretKey(id);
|
||||
const referer = isMovie
|
||||
? `https://www.rivestream.app/watch?type=movie&id=${id}`
|
||||
: `https://www.rivestream.app/watch?type=tv&id=${id}&season=${s}&episode=${e}`;
|
||||
|
||||
let services = await fetchRiveServices();
|
||||
|
||||
if (server && server !== 'all') {
|
||||
const clean = server.toLowerCase().replace('rive (', '').replace(')', '').replace('rive', '').trim();
|
||||
services = services.filter(srv => srv.toLowerCase().includes(clean));
|
||||
if (!services.length) services = await fetchRiveServices();
|
||||
}
|
||||
|
||||
const settled = await Promise.allSettled(
|
||||
services.map(srv => fetchServiceStreams(srv, isMovie, id, s, e, secretKey, referer))
|
||||
);
|
||||
|
||||
const allUrls = [];
|
||||
for (const r of settled) {
|
||||
if (r.status === 'fulfilled' && Array.isArray(r.value)) {
|
||||
allUrls.push(...r.value);
|
||||
}
|
||||
}
|
||||
|
||||
return allUrls.length ? { allUrls } : null;
|
||||
}
|
||||
|
||||
export async function getSources() {
|
||||
const services = await fetchRiveServices();
|
||||
return services.map(s => `Rive (${s})`);
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
import { getUA } 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);
|
||||
const mediaType = isTv ? 'tv' : 'movie';
|
||||
const ua = getUA();
|
||||
|
||||
const details = await resolveDetails(sdk, id, isTv);
|
||||
const title = details?.title || String(id);
|
||||
const year = details?.year || '';
|
||||
|
||||
const formattedTitle = encodeURIComponent(title).replaceAll('%20', '+');
|
||||
const params = new URLSearchParams({
|
||||
type: mediaType,
|
||||
stream: 'true',
|
||||
});
|
||||
|
||||
if (year) params.set('year', String(year));
|
||||
if (isTv) {
|
||||
params.set('season', String(s));
|
||||
params.set('episode', String(e));
|
||||
}
|
||||
|
||||
const scrapeUrl = `${BASE_URL}/api/scrape?title=${formattedTitle}&${params.toString()}`;
|
||||
|
||||
const res = await fetch(scrapeUrl, {
|
||||
headers: {
|
||||
'User-Agent': ua,
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
Accept: 'text/event-stream, application/x-ndjson, text/plain, */*',
|
||||
},
|
||||
signal: AbortSignal.timeout(90000),
|
||||
});
|
||||
|
||||
if (!res.ok) return null;
|
||||
|
||||
const lines = [];
|
||||
if (res.body?.getReader) {
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let buffer = '';
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const parts = buffer.split('\n');
|
||||
buffer = parts.pop() || '';
|
||||
for (const part of parts) {
|
||||
const trimmed = part.trim();
|
||||
if (trimmed) lines.push(trimmed);
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
if (buffer.trim()) lines.push(buffer.trim());
|
||||
} else {
|
||||
const text = await res.text();
|
||||
for (const line of text.split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed) lines.push(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
const seenUrls = new Set();
|
||||
const allUrls = [];
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const raw = line.startsWith('data:') ? line.slice(5).trim() : line;
|
||||
if (!raw || raw === '[DONE]') continue;
|
||||
const item = JSON.parse(raw);
|
||||
const streamUrl = String(item?.url || '').trim();
|
||||
if (!streamUrl || !streamUrl.startsWith('http') || seenUrls.has(streamUrl)) continue;
|
||||
|
||||
seenUrls.add(streamUrl);
|
||||
|
||||
const serverName = item.server || item.providerKey || 'Stream';
|
||||
const quality = item.quality ? ` · ${item.quality}` : '';
|
||||
const size = item.size ? ` (${item.size})` : '';
|
||||
const label = `${serverName}${quality}${size}`;
|
||||
|
||||
const headers = {
|
||||
'User-Agent': ua,
|
||||
...(item.headers && typeof item.headers === 'object' ? item.headers : {}),
|
||||
};
|
||||
|
||||
if (item.referer) {
|
||||
headers.Referer = item.referer;
|
||||
}
|
||||
|
||||
allUrls.push({
|
||||
url: streamUrl,
|
||||
label,
|
||||
headers,
|
||||
});
|
||||
} catch { }
|
||||
}
|
||||
|
||||
if (allUrls.length === 0) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
allUrls,
|
||||
headers: allUrls[0].headers,
|
||||
};
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
import { getUA } 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;
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
import { getUA } 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;
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
import { USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const REDIS_URL = 'https://vidnest-redis-fell-prism-rest.cloud.layerbase.dev/';
|
||||
const REDIS_AUTH = 'Bearer ve8z9XSKatu74M7FjLU8eQ29';
|
||||
const ORIGIN = 'https://vidgod.space';
|
||||
|
||||
const SERVERS = [
|
||||
{
|
||||
name: 'Pulsar',
|
||||
key: 'pulsar',
|
||||
base: 'https://vidnest-extractor.vividdubbing.workers.dev/api',
|
||||
serverParam: 'prime'
|
||||
},
|
||||
{
|
||||
name: 'Orion',
|
||||
key: 'orion',
|
||||
base: 'https://404-vidnest.lofiserver.workers.dev/api',
|
||||
serverParam: 'gama'
|
||||
},
|
||||
{
|
||||
name: 'Stellar',
|
||||
key: 'stellar',
|
||||
base: 'https://404-vidnest.lofiserver.workers.dev/api',
|
||||
serverParam: 'sigma'
|
||||
}
|
||||
];
|
||||
|
||||
function buildCacheKey(serverKey, type, id, s, e) {
|
||||
const parts = [serverKey, type, id];
|
||||
if (type === 'tv') {
|
||||
parts.push(s || '1', e || '1');
|
||||
}
|
||||
return parts.join(':');
|
||||
}
|
||||
|
||||
async function getFromCache(key) {
|
||||
try {
|
||||
const res = await fetch(REDIS_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': REDIS_AUTH,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': '*/*',
|
||||
'Origin': ORIGIN,
|
||||
'Referer': `${ORIGIN}/`,
|
||||
'User-Agent': USER_AGENT
|
||||
},
|
||||
body: JSON.stringify(['GET', key]),
|
||||
signal: AbortSignal.timeout(6000)
|
||||
});
|
||||
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
if (!data || !data.result) return null;
|
||||
|
||||
const parsed = typeof data.result === 'string' ? JSON.parse(data.result) : data.result;
|
||||
return parsed?.streams?.length ? parsed : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchFromWorker(srv, type, id, s, e) {
|
||||
try {
|
||||
const endpoint = type === 'movie'
|
||||
? `${srv.base}/movie/${id}?server=${srv.serverParam}`
|
||||
: `${srv.base}/tv/${id}/${s || '1'}/${e || '1'}?server=${srv.serverParam}`;
|
||||
|
||||
const res = await fetch(endpoint, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Origin': ORIGIN,
|
||||
'Referer': `${ORIGIN}/`,
|
||||
'User-Agent': USER_AGENT
|
||||
},
|
||||
signal: AbortSignal.timeout(8000)
|
||||
});
|
||||
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
return data?.streams?.length ? data : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveServer(srv, type, id, s, e) {
|
||||
const cacheKey = buildCacheKey(srv.key, type, id, s, e);
|
||||
const cached = await getFromCache(cacheKey);
|
||||
if (cached) return { server: srv.name, data: cached };
|
||||
|
||||
const live = await fetchFromWorker(srv, type, id, s, e);
|
||||
if (live) return { server: srv.name, data: live };
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, server } = args;
|
||||
const isTv = s != null && e != null;
|
||||
const type = isTv ? 'tv' : 'movie';
|
||||
|
||||
let targets = SERVERS;
|
||||
if (server && server !== 'all') {
|
||||
const clean = server.toLowerCase().trim();
|
||||
targets = SERVERS.filter(srv => srv.name.toLowerCase() === clean || srv.key.toLowerCase() === clean);
|
||||
if (!targets.length) targets = SERVERS;
|
||||
}
|
||||
|
||||
const settled = await Promise.allSettled(
|
||||
targets.map(srv => resolveServer(srv, type, id, s, e))
|
||||
);
|
||||
|
||||
const allUrls = [];
|
||||
|
||||
for (const r of settled) {
|
||||
if (r.status !== 'fulfilled' || !r.value) continue;
|
||||
const { server: serverName, data } = r.value;
|
||||
|
||||
for (const item of data.streams || []) {
|
||||
if (!item || !item.url) continue;
|
||||
|
||||
const typeStr = String(item.type || '').toLowerCase();
|
||||
const urlStr = String(item.url || '').toLowerCase();
|
||||
const isM3U8 =
|
||||
typeStr === 'hls' ||
|
||||
typeStr === 'application/vnd.apple.mpegurl' ||
|
||||
typeStr === 'cloudflare' ||
|
||||
typeStr === 'tiktok' ||
|
||||
urlStr.includes('.m3u8') ||
|
||||
urlStr.includes('.txt');
|
||||
|
||||
allUrls.push({
|
||||
url: item.url,
|
||||
server: serverName,
|
||||
quality: item.quality || 'Auto',
|
||||
type: isM3U8 ? 'hls' : 'mp4',
|
||||
headers: item.headers || {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Referer': `${ORIGIN}/`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return allUrls.length ? { allUrls } : null;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { fetchJson, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const BASE = "https://vidlink.pro";
|
||||
const HEADERS = { 'User-Agent': USER_AGENT, Origin: BASE, Referer: `${BASE}/` };
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
try {
|
||||
const encData = await fetchJson(`https://enc-dec.app/api/enc-vidlink?text=${id}`, { signal: AbortSignal.timeout(6000) });
|
||||
if (encData?.status !== 200 || !encData?.result) return null;
|
||||
const data = await fetchJson(s ? `${BASE}/api/b/tv/${encData.result}/${s}/${e || 1}` : `${BASE}/api/b/movie/${encData.result}`, { headers: HEADERS, signal: AbortSignal.timeout(10000) });
|
||||
const stream = data?.stream;
|
||||
if (!stream) return null;
|
||||
if (stream.type === 'file' && stream.qualities) {
|
||||
let picked = null;
|
||||
for (const q of ['1080', '720', '480', '360']) if (stream.qualities[q]?.url) { picked = stream.qualities[q].url; break; }
|
||||
if (!picked) picked = stream.qualities[Object.keys(stream.qualities)[0]]?.url;
|
||||
if (picked) return { allUrls: [{ url: picked, type: 'mp4', audio: 'sub', server: 'Vidlink', skipHlsCheck: true, skipCache: true }] };
|
||||
} else if (stream.playlist) {
|
||||
return { allUrls: [{ url: stream.playlist, type: 'hls', audio: 'sub', server: 'Vidlink', skipHlsCheck: true, skipCache: true }] };
|
||||
}
|
||||
return null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
export async function getSources(args) {
|
||||
const stream = await getStream(args);
|
||||
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = 'https://vidnest.fun';
|
||||
const API_BASE_URL = 'https://new.vidnest.fun';
|
||||
const REQUEST_HEADERS = { 'User-Agent': USER_AGENT, 'Accept': 'application/json, text/javascript, */*; q=0.01', 'Accept-Language': 'en-US,en;q=0.9', 'Referer': `${BASE_URL}/`, 'Origin': BASE_URL };
|
||||
const CDN_PROXY_HEADERS = { 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0', 'accept': '*/*', 'accept-language': 'en-US,en;q=0.5', 'origin': 'https://megaplay.buzz', 'referer': 'https://megaplay.buzz/' };
|
||||
|
||||
export const CDN_HEADERS = [
|
||||
{ pattern: /letsgocdn\d+\.shop/i, headers: { 'Referer': BASE_URL, 'Origin': BASE_URL, 'User-Agent': USER_AGENT } },
|
||||
{ pattern: /cdn\.mewstream\.buzz/i, headers: { 'Referer': BASE_URL, 'Origin': BASE_URL, 'User-Agent': USER_AGENT } }
|
||||
];
|
||||
|
||||
const ALPHABET = 'RB0fpH8ZEyVLkv7c2i6MAJ5u3IKFDxlS1NTsnGaqmXYdUrtzjwObCgQP94hoeW+/=';
|
||||
const REV_MAP = new Uint8Array(256);
|
||||
for (let i = 0; i < ALPHABET.length; i++) REV_MAP[ALPHABET.charCodeAt(i)] = i;
|
||||
|
||||
function decrypt(payload) {
|
||||
const len = payload.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
let j = 0;
|
||||
for (let i = 0; i < len; i += 4) {
|
||||
const c0 = REV_MAP[payload.charCodeAt(i)] ?? 64;
|
||||
const c1 = REV_MAP[payload.charCodeAt(i + 1)] ?? 64;
|
||||
const c2 = REV_MAP[payload.charCodeAt(i + 2)] ?? 64;
|
||||
const c3 = REV_MAP[payload.charCodeAt(i + 3)] ?? 64;
|
||||
bytes[j++] = (c0 << 2) | (c1 >> 4);
|
||||
if (c2 !== 64) bytes[j++] = ((c1 & 15) << 4) | (c2 >> 2);
|
||||
if (c3 !== 64) bytes[j++] = ((c2 & 3) << 6) | c3;
|
||||
}
|
||||
return JSON.parse(new TextDecoder().decode(bytes.subarray(0, j)));
|
||||
}
|
||||
|
||||
async function tmdbToAnilist(tmdbId, season) {
|
||||
try {
|
||||
const data = await fetchJson(`https://api.ani.zip/mappings?tmdb_id=${tmdbId}&type=tv&season=${season || 1}`, { signal: AbortSignal.timeout(6000) });
|
||||
return data?.mappings?.[0]?.anilist_id || null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
const SERVER_KEYS = ['hollymoviehd', 'allmovies', 'catflix', 'purstream', 'lamda', 'vidlink', 'klikxxi'];
|
||||
|
||||
export async function getStream({ id, s, e, sdk, audio }) {
|
||||
const tmdbApiKey = sdk?.tmdbApiKey || null;
|
||||
if (!tmdbApiKey) return null;
|
||||
if (s) {
|
||||
const k = tmdbApiKey;
|
||||
const showData = k ? await fetchJson(`https://api.themoviedb.org/3/tv/${id}?api_key=${k}`, { signal: AbortSignal.timeout(5000) }).catch(() => null) : null;
|
||||
if (showData?.genres?.some(g => g.id === 16)) {
|
||||
const anilistId = await tmdbToAnilist(id, s);
|
||||
if (anilistId) {
|
||||
try {
|
||||
const json = await fetchJson(`${API_BASE_URL}/hianime/anime/${anilistId}/${e || 1}/${audio === 'dub' ? 'dub' : 'sub'}`, { headers: REQUEST_HEADERS, signal: AbortSignal.timeout(15000) });
|
||||
const data = json.encrypted ? decrypt(json.data) : json.data;
|
||||
const file = data?.sources?.[0]?.file;
|
||||
if (file) return { url: `https://megacloud.animanga.fun/proxy?url=${encodeURIComponent(file)}&headers=${encodeURIComponent(JSON.stringify(CDN_PROXY_HEADERS))}`, headers: REQUEST_HEADERS };
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (audio === 'dub') return null;
|
||||
const segment = s ? `tv/${id}/${s}/${e || 1}` : `movie/${id}`;
|
||||
const settled = await Promise.allSettled(SERVER_KEYS.map(async server => {
|
||||
const json = await fetchJson(`${API_BASE_URL}/${server}/${segment}`, { headers: REQUEST_HEADERS, signal: AbortSignal.timeout(10000) });
|
||||
const data = json.encrypted ? decrypt(json.data) : json.data;
|
||||
const file = data?.sources?.[0]?.file ?? data?.streams?.[0]?.url ?? data?.url?.[0]?.link ?? data?.data?.stream?.playlist;
|
||||
if (file) return { url: file, server, headers: REQUEST_HEADERS };
|
||||
throw new Error();
|
||||
}));
|
||||
const allUrls = [];
|
||||
for (const r of settled) if (r.status === 'fulfilled') allUrls.push(r.value);
|
||||
return allUrls.length ? { allUrls } : null;
|
||||
}
|
||||
|
||||
export async function getSources(args) {
|
||||
const res = await getStream(args);
|
||||
return res?.allUrls ? res.allUrls.map(u => u.server) : [];
|
||||
}
|
||||
|
||||
export const VERIFY_HEADERS = { ...REQUEST_HEADERS };
|
||||
@@ -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 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
import { USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = 'https://vidvault.to';
|
||||
const API_BASE = `${BASE_URL}/api`;
|
||||
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,
|
||||
'User-Agent': USER_AGENT,
|
||||
'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',
|
||||
headers: HEADERS,
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Token fetch failed: ${res.status}`);
|
||||
const data = await res.json();
|
||||
return data?.t;
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
try {
|
||||
const token = await getToken();
|
||||
if (!token) return null;
|
||||
|
||||
const payload = {
|
||||
type,
|
||||
tmdbId: String(id),
|
||||
};
|
||||
|
||||
if (isTv) {
|
||||
payload.season = Number(s);
|
||||
payload.episode = Number(e);
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_BASE}/download-proxy`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...HEADERS,
|
||||
'Content-Type': 'application/json',
|
||||
'x-request-token': token,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
|
||||
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}`;
|
||||
|
||||
allUrls.push({
|
||||
url: proxiedUrl,
|
||||
server: `VidVault - MP4 ${res}p${sizeStr}`.trim(),
|
||||
quality: typeof res === 'number' ? `${res}p` : res,
|
||||
type: 'mp4',
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
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})`);
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
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 lang = extract(/lang(?:uage)?["']\s*:\s*["']([a-z]{2,5})/i, html) || 'en';
|
||||
const rawMasterUrl = `${playlist}${playlist.includes('?') ? '&' : '?'}token=${token}&expires=${expires}&h=1&lang=${lang}`;
|
||||
|
||||
const masterProxy = absoluteBase && absoluteBase !== '/'
|
||||
? `${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;
|
||||
|
||||
let finalUrl = null;
|
||||
let bestHeight = 0;
|
||||
const lines = playlistText.split('\n');
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (!line.startsWith('#EXT-X-STREAM-INF')) continue;
|
||||
const height = parseInt(line.match(/RESOLUTION=\d+x(\d+)/)?.[1] || '0', 10);
|
||||
let next = lines[i + 1]?.trim();
|
||||
if (!next || next.startsWith('#')) continue;
|
||||
|
||||
if (height > bestHeight) {
|
||||
bestHeight = height;
|
||||
finalUrl = next;
|
||||
}
|
||||
}
|
||||
|
||||
if (!finalUrl) finalUrl = rawMasterUrl;
|
||||
|
||||
if (!finalUrl.startsWith('http')) {
|
||||
const baseMatch = rawMasterUrl.match(/^(https?:\/\/[^\/]+)/);
|
||||
const base = baseMatch ? baseMatch[1] : BASE_URL;
|
||||
if (finalUrl.startsWith('/')) {
|
||||
finalUrl = `${base}${finalUrl}`;
|
||||
} else {
|
||||
const pathParts = rawMasterUrl.split('/');
|
||||
pathParts.pop();
|
||||
finalUrl = `${pathParts.join('/')}/${finalUrl}`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
allUrls: [
|
||||
{
|
||||
url: finalUrl,
|
||||
server: 'VixSrc',
|
||||
skipProxy: true,
|
||||
headers: {
|
||||
...HEADERS,
|
||||
'Referer': `${BASE_URL}/`
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
const BASE_URL = 'https://www.films365.org';
|
||||
const HEADERS = {
|
||||
Authorization: 'Bearer 79a02956be35835728a044b11e2ae793149d45fb2c89cb6d029ec01aac19bfdb',
|
||||
'Content-Type': 'application/json',
|
||||
'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);
|
||||
const targetType = isTv ? 'tv' : 'movie';
|
||||
|
||||
const title = await resolveTitle(sdk, id, isTv);
|
||||
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),
|
||||
});
|
||||
|
||||
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 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;
|
||||
|
||||
const itemTitle = String(item?.title || '');
|
||||
const cleanItemTitle = itemTitle.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
|
||||
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'],
|
||||
},
|
||||
});
|
||||
}
|
||||
} 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}`,
|
||||
headers: {
|
||||
'User-Agent': HEADERS['User-Agent'],
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allUrls.length === 0) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
allUrls,
|
||||
headers: allUrls[0].headers,
|
||||
};
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
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 };
|
||||
@@ -1,230 +0,0 @@
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
export const UA_LIST = [
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
|
||||
'Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0',
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0',
|
||||
];
|
||||
|
||||
export const getUA = () => UA_LIST[Math.floor(Math.random() * UA_LIST.length)];
|
||||
export const USER_AGENT = UA_LIST[0];
|
||||
|
||||
let sharedDcKey = null;
|
||||
|
||||
export function decryptPartner(responseText, keyEnv) {
|
||||
try {
|
||||
const sep = responseText.indexOf(':');
|
||||
if (sep === -1) return null;
|
||||
const iv = Buffer.from(responseText.slice(0, sep), 'base64');
|
||||
const encrypted = responseText.slice(sep + 1);
|
||||
if (!sharedDcKey) sharedDcKey = crypto.createHash('sha256').update(keyEnv || '').digest();
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', sharedDcKey, iv);
|
||||
return JSON.parse(decipher.update(encrypted, 'base64', 'utf8') + decipher.final('utf8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchJson(url, options) {
|
||||
const res = await fetch(url, options);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function fetchText(url, options) {
|
||||
const res = await fetch(url, options);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.text();
|
||||
}
|
||||
|
||||
const tmdbInfoCache = new Map();
|
||||
const anilistCache = new Map();
|
||||
const tmdbValidationCache = new Map();
|
||||
|
||||
function pruneHelperCache(cache) {
|
||||
const now = Date.now();
|
||||
for (const [k, v] of cache) if (now - v.ts >= v.ttl) cache.delete(k);
|
||||
if (cache.size > 3000) {
|
||||
const overflow = cache.size - 3000;
|
||||
const it = cache.keys();
|
||||
for (let i = 0; i < overflow; i++) cache.delete(it.next().value);
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
pruneHelperCache(tmdbValidationCache);
|
||||
pruneHelperCache(tmdbInfoCache);
|
||||
pruneHelperCache(anilistCache);
|
||||
}, 60000);
|
||||
|
||||
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; }
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export const webcrypto = globalThis.crypto;
|
||||
export function createHash() { throw new Error('unsupported'); }
|
||||
export function createDecipheriv() { throw new Error('unsupported'); }
|
||||
export default { webcrypto, createHash, createDecipheriv };
|
||||
@@ -1,7 +0,0 @@
|
||||
import { Buffer } from 'buffer';
|
||||
globalThis.Buffer = globalThis.Buffer || Buffer;
|
||||
globalThis.process = globalThis.process || { env: {} };
|
||||
if (!AbortSignal.timeout) {
|
||||
AbortSignal.timeout = ms => { const c = new AbortController(); setTimeout(() => c.abort(), ms); return c.signal; };
|
||||
}
|
||||
export { Buffer };
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -5,4 +5,5 @@ android/app/build/
|
||||
android/.gradle/
|
||||
.gradle/
|
||||
*.apk
|
||||
.env
|
||||
.env
|
||||
.build
|
||||
@@ -16,4 +16,4 @@ An Android TV streaming app built with Jetpack Compose and ExoPlayer.
|
||||
npm run dev
|
||||
```
|
||||
|
||||
This builds the APK as `vyla.apk`.
|
||||
This builds the APK as `vyla.apk`.
|
||||
|
||||
@@ -136,6 +136,7 @@ fun PlayerScreen(
|
||||
val setButtonFocusRequester = remember { FocusRequester() }
|
||||
val moreButtonFocusRequester = remember { FocusRequester() }
|
||||
var hideControlsJob by remember { mutableStateOf<Job?>(null) }
|
||||
val playerContainerFocusRequester = remember { FocusRequester() }
|
||||
fun resetHideTimer() {
|
||||
hideControlsJob?.cancel()
|
||||
if (activeMenu == ActivePlayerMenu.NONE && !isEpisodesDrawerOpen && isPlaying) {
|
||||
@@ -144,6 +145,9 @@ fun PlayerScreen(
|
||||
if (isPlaying && activeMenu == ActivePlayerMenu.NONE && !isEpisodesDrawerOpen) {
|
||||
areControlsVisible = false
|
||||
}
|
||||
try {
|
||||
playerContainerFocusRequester.requestFocus()
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -420,6 +424,7 @@ fun PlayerScreen(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.Black)
|
||||
.focusRequester(playerContainerFocusRequester)
|
||||
.focusable()
|
||||
.onKeyEvent { event ->
|
||||
if (event.type == KeyEventType.KeyDown) {
|
||||
|
||||
Reference in New Issue
Block a user