mirror of
https://gitlab.com/vyla-entertainment/sdk.git
synced 2026-11-09 19:57:23 -05:00
config.js modified, downloads.js modified, trendimovies.js removed, sdk.js modified, anipm.js removed, mapple.js removed, megasource.js removed, meowtv.js modified, nova.js removed, purstream.js modified, videasy.js removed, vidvault.js modified, xpass.js removed, zxcstream.js removed, subtitles.js modified, test/index.js modified, 111477.js added, 4khdhub.js added, downloadeverything.js added, movienight.js added, movy.js added, streamaggregator.js added, vadapav.js added, vuflix.js added, xdownloader.js added
This commit is contained in:
258
src/config.js
258
src/config.js
@@ -1,4 +1,4 @@
|
||||
// Currently active sources reported on 9:22:41 PM PST 7/30/2026
|
||||
// Currently active sources reported on 6:18:22 PM PST 9/13/2026
|
||||
|
||||
// None Anime Tested: 936075
|
||||
// Anime Tested: 37854
|
||||
@@ -6,6 +6,18 @@
|
||||
// Fshare/Fsonic Tested: 155 ( they don't have Michael )
|
||||
|
||||
export const SOURCES = [
|
||||
{
|
||||
key: '4khdhub',
|
||||
label: '4KHDHub',
|
||||
sourceFile: '4khdhub',
|
||||
proxyParam: 'khd',
|
||||
timeout: 35000,
|
||||
jitter: 500,
|
||||
retries: 1,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
},
|
||||
|
||||
{
|
||||
key: '123anime',
|
||||
label: '123Anime',
|
||||
@@ -25,6 +37,25 @@ export const SOURCES = [
|
||||
},],
|
||||
},
|
||||
|
||||
{
|
||||
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)',
|
||||
@@ -69,40 +100,6 @@ export const SOURCES = [
|
||||
retries: 2
|
||||
},
|
||||
|
||||
{
|
||||
key: 'anipm-sub',
|
||||
sourceFile: 'anipm',
|
||||
label: 'AniPM (Sub)',
|
||||
proxyParam: 'apsub',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
skipProxy: true,
|
||||
skipVerify: true,
|
||||
multiUrl: true,
|
||||
verifyHeaders: {
|
||||
Referer: 'https://ani.pm/',
|
||||
Origin: 'https://ani.pm',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
key: 'anipm-dub',
|
||||
sourceFile: 'anipm',
|
||||
label: 'AniPM (Dub)',
|
||||
proxyParam: 'apdub',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
skipProxy: true,
|
||||
skipVerify: true,
|
||||
multiUrl: true,
|
||||
verifyHeaders: {
|
||||
Referer: 'https://ani.pm/',
|
||||
Origin: 'https://ani.pm',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
key: 'bcine',
|
||||
label: 'Bcine',
|
||||
@@ -173,6 +170,25 @@ export const SOURCES = [
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'downloadeverything',
|
||||
label: 'DownloadEverything',
|
||||
sourceFile: 'downloadeverything',
|
||||
proxyParam: 'de',
|
||||
timeout: 35000,
|
||||
jitter: 500,
|
||||
retries: 1,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /cloudflarestorage\.com|pixeldrain\.com|hakunaymatata\.com/i,
|
||||
headers: {
|
||||
Referer: 'https://downloadeverythingfromeverywhere.com/',
|
||||
Origin: 'https://downloadeverythingfromeverywhere.com',
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'flaxmovies',
|
||||
label: 'FlaxMovies',
|
||||
@@ -314,16 +330,6 @@ export const SOURCES = [
|
||||
skipProxy: true,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'mapple',
|
||||
label: 'Mapple',
|
||||
sourceFile: 'mapple',
|
||||
proxyParam: 'mp',
|
||||
timeout: 15000,
|
||||
retries: 1,
|
||||
jitter: 0
|
||||
},
|
||||
|
||||
{
|
||||
key: 'meowtv',
|
||||
label: 'MeowTV',
|
||||
@@ -336,6 +342,44 @@ export const SOURCES = [
|
||||
skipProxy: true
|
||||
},
|
||||
|
||||
{
|
||||
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: 'peestream',
|
||||
label: 'PeeStream',
|
||||
@@ -372,28 +416,6 @@ export const SOURCES = [
|
||||
multiUrl: true,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'nova',
|
||||
label: 'Nova',
|
||||
sourceFile: 'nova',
|
||||
proxyParam: 'nv',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 1,
|
||||
multiUrl: true,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'megasource',
|
||||
label: 'MegaSource',
|
||||
sourceFile: 'megasource',
|
||||
proxyParam: 'mg',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 1,
|
||||
multiUrl: true,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'purstream',
|
||||
sourceFile: 'purstream',
|
||||
@@ -416,6 +438,44 @@ export const SOURCES = [
|
||||
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: 'vadapav',
|
||||
label: 'Vadapav',
|
||||
sourceFile: 'vadapav',
|
||||
proxyParam: 'vp',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /vadapav\.mov/i,
|
||||
headers: {
|
||||
Referer: 'https://stremio.vadapav.mov/',
|
||||
Origin: 'https://stremio.vadapav.mov',
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'vidapi',
|
||||
label: 'VidAPI',
|
||||
@@ -438,23 +498,6 @@ export const SOURCES = [
|
||||
sourcesTimeout: 10000,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'videasy',
|
||||
label: 'Videasy',
|
||||
sourceFile: 'videasy',
|
||||
proxyParam: 'vy',
|
||||
timeout: 40000,
|
||||
sourcesTimeout: 10000,
|
||||
jitter: 900,
|
||||
retries: 3,
|
||||
multiUrl: true,
|
||||
verifyHeaders: {
|
||||
Accept: 'application/json, /; q=0.01',
|
||||
Referer: 'https://player.videasy.net/',
|
||||
Origin: 'https://player.videasy.net',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
key: 'vidfast',
|
||||
label: 'Vidfast',
|
||||
@@ -487,7 +530,6 @@ export const SOURCES = [
|
||||
retries: 2,
|
||||
skipProxy: true,
|
||||
multiUrl: true,
|
||||
disabled: true, // Temporarily disabled because their site is down, all streams are returning 403's
|
||||
},
|
||||
|
||||
{
|
||||
@@ -603,25 +645,41 @@ export const SOURCES = [
|
||||
},
|
||||
|
||||
{
|
||||
key: 'xpass',
|
||||
sourceFile: 'xpass',
|
||||
label: 'XPass',
|
||||
proxyParam: 'xp',
|
||||
timeout: 20000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
skipProxy: true,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'zxcstream',
|
||||
label: 'ZxcStream',
|
||||
sourceFile: 'zxcstream',
|
||||
proxyParam: 'zxc',
|
||||
key: 'vuflix',
|
||||
label: 'Vuflix',
|
||||
sourceFile: 'vuflix',
|
||||
proxyParam: 'vfx',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /vuflix\.co/i,
|
||||
headers: {
|
||||
Referer: 'https://vuflix.co/',
|
||||
Origin: 'https://vuflix.co',
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
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',
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
@@ -1,12 +1,53 @@
|
||||
import { getDownloads, getDownloadsTv } from "./sources/trendimovies.js";
|
||||
import SOURCES from "../config.js";
|
||||
|
||||
export async function fetchDownloads(id, s = null, e = null) {
|
||||
try {
|
||||
const res = s != null && e != null
|
||||
? await getDownloadsTv(id, s, e)
|
||||
: await getDownloads(id);
|
||||
return res || [];
|
||||
} catch {
|
||||
return [];
|
||||
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,63 +0,0 @@
|
||||
const BASE = "https://trendimovies.com";
|
||||
|
||||
const HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Referer": "https://trendimovies.com/",
|
||||
};
|
||||
|
||||
async function fetchPage(path) {
|
||||
const res = await fetch(`${BASE}${path}`, { headers: HEADERS });
|
||||
if (!res.ok) throw new Error(`trendimovies ${res.status}`);
|
||||
return res.text();
|
||||
}
|
||||
|
||||
function parseLinks(html) {
|
||||
const m = html.match(/DownloadSection[^>]*props="([^"]+)"/s) ||
|
||||
html.match(/props="([^"]+)"[^>]*ssr[^>]*client="load"[^>]*opts="[^"]*DownloadSection/s);
|
||||
if (!m) {
|
||||
const alt = html.match(/"DownloadSection"/);
|
||||
if (!alt) return [];
|
||||
const propsMatch = html.match(/props="({[^"]*DownloadSection[^"]*})"/s);
|
||||
if (!propsMatch) return [];
|
||||
return extractLinks(propsMatch[1].replace(/"/g, '"').replace(/'/g, "'"));
|
||||
}
|
||||
return extractLinks(m[1].replace(/"/g, '"').replace(/'/g, "'"));
|
||||
}
|
||||
|
||||
function extractLinks(propsJson) {
|
||||
try {
|
||||
const parsed = JSON.parse(propsJson);
|
||||
const rawLinks = parsed?.links?.[1];
|
||||
if (!Array.isArray(rawLinks)) return [];
|
||||
return rawLinks
|
||||
.map(item => item?.[1])
|
||||
.filter(l => l && typeof l === "object")
|
||||
.map(l => ({
|
||||
url: unwrap(l.url),
|
||||
quality: unwrap(l.quality) || "HD",
|
||||
size: unwrap(l.file_size) || null,
|
||||
type: "mkv",
|
||||
active: unwrap(l.is_active) !== false,
|
||||
}))
|
||||
.filter(l => l.url && l.active);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function unwrap(v) {
|
||||
if (Array.isArray(v) && v.length === 2 && (v[0] === 0 || v[0] === 1)) return v[1];
|
||||
return v;
|
||||
}
|
||||
|
||||
export async function getDownloads(tmdbId) {
|
||||
const html = await fetchPage(`/movie/${tmdbId}`);
|
||||
return parseLinks(html);
|
||||
}
|
||||
|
||||
export async function getDownloadsTv(tmdbId, season, episode) {
|
||||
const html = await fetchPage(`/tv/${tmdbId}/season/${season}/episode/${episode}`);
|
||||
return parseLinks(html);
|
||||
}
|
||||
1
src/sdk.d.ts
vendored
1
src/sdk.d.ts
vendored
@@ -59,7 +59,6 @@ export interface VylaSDKOptions {
|
||||
tmdbApiKey?: string;
|
||||
}
|
||||
|
||||
// Descriptions written with Claude.
|
||||
export default class VylaSDK {
|
||||
tmdbApiKey?: string;
|
||||
|
||||
|
||||
@@ -63,6 +63,6 @@ export default class VylaSDK {
|
||||
return await fetchSubtitles(paths);
|
||||
}
|
||||
async getDownloads(id, s = null, e = null) {
|
||||
return await fetchDownloads(id, s, e);
|
||||
return await fetchDownloads(this, id, s, e);
|
||||
}
|
||||
}
|
||||
101
src/sources/111477.js
Normal file
101
src/sources/111477.js
Normal file
@@ -0,0 +1,101 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
324
src/sources/4khdhub.js
Normal file
324
src/sources/4khdhub.js
Normal file
@@ -0,0 +1,324 @@
|
||||
import { getUA } from '../utils/helpers.js';
|
||||
|
||||
const INITIAL_BASE_URL = 'https://4khdhub.one';
|
||||
let cachedBaseUrl = null;
|
||||
let cachedBaseUrlExpiry = 0;
|
||||
|
||||
function rot13(str) {
|
||||
return str.replace(/[a-zA-Z]/g, (c) => {
|
||||
const code = c.charCodeAt(0);
|
||||
if (code >= 65 && code <= 90) {
|
||||
return String.fromCharCode(((code - 65 + 13) % 26) + 65);
|
||||
}
|
||||
return String.fromCharCode(((code - 97 + 13) % 26) + 97);
|
||||
});
|
||||
}
|
||||
|
||||
function levenshtein(a, b) {
|
||||
if (a === b) return 0;
|
||||
if (!a.length) return b.length;
|
||||
if (!b.length) return a.length;
|
||||
const la = a.length;
|
||||
const lb = b.length;
|
||||
const d = Array.from({ length: la + 1 }, () => new Array(lb + 1).fill(0));
|
||||
for (let i = 0; i <= la; i++) d[i][0] = i;
|
||||
for (let j = 0; j <= lb; j++) d[0][j] = j;
|
||||
for (let i = 1; i <= la; i++) {
|
||||
for (let j = 1; j <= lb; j++) {
|
||||
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
||||
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
|
||||
}
|
||||
}
|
||||
return d[la][lb];
|
||||
}
|
||||
|
||||
async function getBaseUrl(ua) {
|
||||
if (cachedBaseUrl && Date.now() < cachedBaseUrlExpiry) {
|
||||
return cachedBaseUrl;
|
||||
}
|
||||
|
||||
let currentUrl = INITIAL_BASE_URL;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
try {
|
||||
const res = await fetch(currentUrl, {
|
||||
method: 'HEAD',
|
||||
headers: { 'User-Agent': ua },
|
||||
redirect: 'manual',
|
||||
signal: AbortSignal.timeout(10000),
|
||||
});
|
||||
|
||||
if (res.status >= 300 && res.status < 400) {
|
||||
const location = res.headers.get('location');
|
||||
if (location) {
|
||||
currentUrl = new URL(location, currentUrl).toString();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = new URL(currentUrl);
|
||||
cachedBaseUrl = `${parsed.protocol}//${parsed.host}`;
|
||||
cachedBaseUrlExpiry = Date.now() + 3600000;
|
||||
return cachedBaseUrl;
|
||||
}
|
||||
|
||||
async function resolveMetadata(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(8000),
|
||||
});
|
||||
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;
|
||||
return { title, year };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeStreamUrl(rawUrl) {
|
||||
try {
|
||||
const parsed = new URL(rawUrl);
|
||||
parsed.pathname = parsed.pathname.replaceAll('::', '%3A%3A');
|
||||
return parsed.toString();
|
||||
} catch {
|
||||
return rawUrl.replaceAll('::', '%3A%3A');
|
||||
}
|
||||
}
|
||||
|
||||
async function validateStreamUrl(url, ua) {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': ua,
|
||||
Range: 'bytes=0-1024',
|
||||
},
|
||||
signal: AbortSignal.timeout(4000),
|
||||
});
|
||||
|
||||
const contentType = (res.headers.get('content-type') || '').toLowerCase();
|
||||
const isSuccess = (res.status >= 200 && res.status < 300) || res.status === 206;
|
||||
const isHtmlOrJsonError = contentType.includes('text/html') || contentType.includes('application/json');
|
||||
|
||||
if (!isSuccess || isHtmlOrJsonError) return false;
|
||||
return true;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveRedirectUrl(redirectUrl, ua) {
|
||||
try {
|
||||
const res = await fetch(redirectUrl, {
|
||||
headers: { 'User-Agent': ua },
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const html = await res.text();
|
||||
|
||||
let nextUrl = '';
|
||||
const downloadMatch = html.match(/<a id="download" href="(.*?)"/);
|
||||
if (downloadMatch) {
|
||||
nextUrl = downloadMatch[1];
|
||||
} else {
|
||||
const varMatch = html.match(/var url = '(.*?)';/);
|
||||
if (varMatch) {
|
||||
nextUrl = varMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
if (!nextUrl) {
|
||||
const match = html.match(/'o','(.*?)'/);
|
||||
if (!match) return null;
|
||||
const rawData = match[1];
|
||||
const step1 = Buffer.from(rawData, 'base64').toString('utf8');
|
||||
const step2 = Buffer.from(step1, 'base64').toString('utf8');
|
||||
const step3 = rot13(step2);
|
||||
const step4 = Buffer.from(step3, 'base64').toString('utf8');
|
||||
const data = JSON.parse(step4);
|
||||
return Buffer.from(data.o, 'base64').toString('utf8');
|
||||
}
|
||||
|
||||
const interRes = await fetch(nextUrl, {
|
||||
headers: { 'User-Agent': ua },
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
if (!interRes.ok) return null;
|
||||
const interHtml = await interRes.text();
|
||||
|
||||
const finalMatch = interHtml.match(/<a href="([^"]+)"[^>]*class="[^"]*btn-success/);
|
||||
if (finalMatch) {
|
||||
return finalMatch[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function extractSourceResults(itemHtml, ua) {
|
||||
const sizeMatch = itemHtml.match(/([\d.]+ ?[GM]B)/);
|
||||
const sizeStr = sizeMatch ? sizeMatch[1] : null;
|
||||
|
||||
const heightMatch = itemHtml.match(/\d{3,}p/);
|
||||
const heightStr = heightMatch ? heightMatch[0] : null;
|
||||
|
||||
const fileTitleMatch = itemHtml.match(/class="(?:file-title|episode-file-title)"[^>]*>([^<]+)/);
|
||||
const fileTitle = fileTitleMatch ? fileTitleMatch[1].trim() : 'Unknown';
|
||||
|
||||
const displayParts = [fileTitle];
|
||||
if (heightStr) displayParts.push(heightStr);
|
||||
if (sizeStr) displayParts.push(sizeStr);
|
||||
|
||||
let hubCloudUrl = null;
|
||||
let hubDriveUrl = null;
|
||||
|
||||
const linkRegex = /<a\s+[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
|
||||
let match;
|
||||
while ((match = linkRegex.exec(itemHtml)) !== null) {
|
||||
const href = match[1];
|
||||
const text = match[2];
|
||||
if (text.includes('HubCloud') && !hubCloudUrl) {
|
||||
hubCloudUrl = href;
|
||||
} else if (text.includes('HubDrive') && !hubDriveUrl) {
|
||||
hubDriveUrl = href;
|
||||
}
|
||||
}
|
||||
|
||||
const redirectUrl = hubCloudUrl || hubDriveUrl;
|
||||
if (!redirectUrl) return null;
|
||||
|
||||
const resolvedUrl = await resolveRedirectUrl(redirectUrl, ua);
|
||||
if (!resolvedUrl) return null;
|
||||
|
||||
const safeUrl = sanitizeStreamUrl(resolvedUrl);
|
||||
const isValid = await validateStreamUrl(safeUrl, ua);
|
||||
if (!isValid) return null;
|
||||
|
||||
return {
|
||||
url: safeUrl,
|
||||
label: displayParts.join(' · '),
|
||||
headers: {
|
||||
'User-Agent': ua,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e, sdk }) {
|
||||
const isTv = Boolean(s && e);
|
||||
const ua = getUA();
|
||||
|
||||
const meta = await resolveMetadata(sdk, id, isTv);
|
||||
if (!meta?.title) return null;
|
||||
|
||||
const baseUrl = await getBaseUrl(ua);
|
||||
const searchUrl = `${baseUrl}/?s=${encodeURIComponent(meta.title)}`;
|
||||
|
||||
const searchRes = await fetch(searchUrl, {
|
||||
headers: { 'User-Agent': ua },
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
if (!searchRes.ok) return null;
|
||||
const searchHtml = await searchRes.text();
|
||||
|
||||
const cardRegex = /<a\s+([^>]*\bclass="[^"]*movie-card[^"]*"[^>]*)>([\s\S]*?)<\/a>/gi;
|
||||
const formatFilter = isTv ? 'Series' : 'Movies';
|
||||
let targetPageUrl = null;
|
||||
|
||||
let cardMatch;
|
||||
while ((cardMatch = cardRegex.exec(searchHtml)) !== null) {
|
||||
const attributes = cardMatch[1];
|
||||
const inner = cardMatch[2];
|
||||
|
||||
const hrefMatch = attributes.match(/href="([^"]+)"/);
|
||||
if (!hrefMatch) continue;
|
||||
const href = hrefMatch[1];
|
||||
|
||||
const formatMatches = [...inner.matchAll(/class="movie-card-format"[^>]*>([\s\S]*?)<\//gi)];
|
||||
const formatTexts = formatMatches.map(m => m[1].trim());
|
||||
if (!formatTexts.some(t => t.includes(formatFilter))) continue;
|
||||
|
||||
if (meta.year != null) {
|
||||
const metaMatch = inner.match(/class="movie-card-meta"[^>]*>([\s\S]*?)<\//i);
|
||||
const yearMatch = metaMatch ? metaMatch[1].match(/\d{4}/) : null;
|
||||
if (yearMatch) {
|
||||
const cardYear = parseInt(yearMatch[0], 10);
|
||||
if (Math.abs(cardYear - meta.year) > 1) continue;
|
||||
}
|
||||
}
|
||||
|
||||
const titleMatch = inner.match(/class="movie-card-title"[^>]*>([\s\S]*?)<\//i);
|
||||
let cardTitle = titleMatch ? titleMatch[1] : '';
|
||||
cardTitle = cardTitle.replace(/\[.*?\]/g, '').trim();
|
||||
|
||||
const diff = levenshtein(cardTitle.toLowerCase(), meta.title.toLowerCase());
|
||||
const isMatch = diff < 5 || (cardTitle.toLowerCase().includes(meta.title.toLowerCase()) && diff < 16);
|
||||
if (!isMatch) continue;
|
||||
|
||||
targetPageUrl = new URL(href, baseUrl).toString();
|
||||
break;
|
||||
}
|
||||
|
||||
if (!targetPageUrl) return null;
|
||||
|
||||
const pageRes = await fetch(targetPageUrl, {
|
||||
headers: { 'User-Agent': ua },
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
if (!pageRes.ok) return null;
|
||||
const pageHtml = await pageRes.text();
|
||||
|
||||
const matchedHtmlBlocks = [];
|
||||
|
||||
if (isTv) {
|
||||
const seasonStr = String(s).padStart(2, '0');
|
||||
const episodeStr = String(e).padStart(2, '0');
|
||||
|
||||
const epItemRegex = /class="[^"]*episode-item[^"]*"[^>]*>([\s\S]*?)(?=(?:class="[^"]*episode-item"|<\/main>|$))/gi;
|
||||
let epMatch;
|
||||
while ((epMatch = epItemRegex.exec(pageHtml)) !== null) {
|
||||
const epBlock = epMatch[1];
|
||||
const titleMatch = epBlock.match(/class="episode-title"[^>]*>([\s\S]*?)<\//i);
|
||||
const titleText = titleMatch ? titleMatch[1] : '';
|
||||
if (!titleText.includes(`S${seasonStr}`)) continue;
|
||||
|
||||
const dlItemRegex = /class="[^"]*episode-download-item[^"]*"[^>]*>([\s\S]*?)(?=(?:class="[^"]*episode-download-item"|<\/div>\s*<\/div>|$))/gi;
|
||||
let dlMatch;
|
||||
while ((dlMatch = dlItemRegex.exec(epBlock)) !== null) {
|
||||
const dlBlock = dlMatch[1];
|
||||
if (dlBlock.includes(`Episode-${episodeStr}`)) {
|
||||
matchedHtmlBlocks.push(dlBlock);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const dlItemRegex = /class="[^"]*download-item[^"]*"[^>]*>([\s\S]*?)(?=(?:class="[^"]*download-item"|<\/main>|$))/gi;
|
||||
let dlMatch;
|
||||
while ((dlMatch = dlItemRegex.exec(pageHtml)) !== null) {
|
||||
matchedHtmlBlocks.push(dlMatch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedHtmlBlocks.length === 0) return null;
|
||||
|
||||
const results = await Promise.all(matchedHtmlBlocks.map((block) => extractSourceResults(block, ua)));
|
||||
const allUrls = results.filter(Boolean);
|
||||
|
||||
if (allUrls.length === 0) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
allUrls,
|
||||
headers: allUrls[0].headers,
|
||||
};
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import { getTmdbInfo, fetchJson } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = "https://ani.pm";
|
||||
|
||||
function cleanTitle(t) { return t ? t.toLowerCase().replace(/[^a-z0-9]/g, '') : ''; }
|
||||
|
||||
async function searchCatalog(query) {
|
||||
try {
|
||||
const data = await fetchJson(`${BASE_URL}/api/anime/catalog?q=${encodeURIComponent(query)}&page=1`, { signal: AbortSignal.timeout(6000) });
|
||||
return Array.isArray(data?.items) ? data.items : [];
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
async function resolveEntry(titles, year) {
|
||||
const orderedTitles = titles.length > 1 ? [...titles].reverse() : titles;
|
||||
let best = null, bestScore = -1;
|
||||
for (const title of orderedTitles) {
|
||||
const items = await searchCatalog(title);
|
||||
const targetClean = cleanTitle(title);
|
||||
for (const item of items) {
|
||||
const itemTitles = [item.title, item.native].filter(Boolean).map(cleanTitle);
|
||||
let score = 0;
|
||||
if (itemTitles.some(t => t === targetClean)) score += 5;
|
||||
else if (itemTitles.some(t => t.includes(targetClean) || targetClean.includes(t))) score += 3;
|
||||
else continue;
|
||||
if (item.type === 'TV') score += 4;
|
||||
else if (item.type === 'Movie' || item.type === 'Special' || item.type === 'ONA') score -= 2;
|
||||
if (year && item.year) {
|
||||
const diff = Math.abs(item.year - year);
|
||||
if (diff === 0) score += 3;
|
||||
else if (diff === 1) score += 1;
|
||||
else if (diff > 2) score -= 3;
|
||||
}
|
||||
if (score > bestScore) { bestScore = score; best = item; }
|
||||
}
|
||||
if (bestScore >= 9) break;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e, audio }) {
|
||||
try {
|
||||
const info = await getTmdbInfo(id, s ? 'tv' : 'movie', s);
|
||||
if (!info || !info.isAnime) return null;
|
||||
const entry = await resolveEntry(info.titles, info.year);
|
||||
if (!entry) return null;
|
||||
const episodeNum = e || 1;
|
||||
const params = new URLSearchParams({ title: entry.title || '', ep: String(episodeNum) });
|
||||
if (entry.year) params.set('year', entry.year);
|
||||
if (entry.anilistId) params.set('anilistId', entry.anilistId);
|
||||
if (entry.malId) params.set('malId', entry.malId);
|
||||
const servers = await fetchJson(`${BASE_URL}/api/anime/src/servers?${params}`, { signal: AbortSignal.timeout(10000) });
|
||||
if (!servers) return null;
|
||||
const audiosToTry = audio === "all" ? ["sub", "dub"] : (audio === "dub" ? ["dub", "sub"] : ["sub", "dub"]);
|
||||
for (const aud of audiosToTry) {
|
||||
const list = servers[aud];
|
||||
if (!Array.isArray(list) || !list.length) continue;
|
||||
const allUrls = list.filter(srv => (srv.kind === "hls" || srv.kind === "file") && srv.url)
|
||||
.sort((a, b) => (b.priority || 0) - (a.priority || 0))
|
||||
.map(srv => ({ url: srv.url.startsWith('/') ? `${BASE_URL}${srv.url}` : srv.url, type: srv.kind === "hls" ? "hls" : "mp4", audio: aud, server: srv.provider ? `AniPM-${srv.provider}` : "AniPM", headers: { Referer: `${BASE_URL}/`, Origin: BASE_URL }, skipProxy: false }));
|
||||
if (allUrls.length) return { allUrls };
|
||||
}
|
||||
return null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
export async function getSources(args) {
|
||||
const stream = await getStream(args);
|
||||
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
|
||||
}
|
||||
338
src/sources/downloadeverything.js
Normal file
338
src/sources/downloadeverything.js
Normal file
@@ -0,0 +1,338 @@
|
||||
import { getUA } from '../utils/helpers.js';
|
||||
|
||||
const SLAVE_URL = 'https://slave.downloadeverythingfromeverywhere.com/';
|
||||
const REFERER = 'https://downloadeverythingfromeverywhere.com/';
|
||||
const ORIGIN = 'https://downloadeverythingfromeverywhere.com';
|
||||
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36';
|
||||
|
||||
function extractJsonObjects(text) {
|
||||
const objects = [];
|
||||
let depth = 0;
|
||||
let start = -1;
|
||||
let inString = false;
|
||||
let escape = false;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i];
|
||||
if (escape) {
|
||||
escape = false;
|
||||
continue;
|
||||
}
|
||||
if (char === '\\') {
|
||||
escape = true;
|
||||
continue;
|
||||
}
|
||||
if (char === '"') {
|
||||
inString = !inString;
|
||||
continue;
|
||||
}
|
||||
if (!inString) {
|
||||
if (char === '{') {
|
||||
if (depth === 0) start = i;
|
||||
depth++;
|
||||
} else if (char === '}') {
|
||||
depth--;
|
||||
if (depth === 0 && start !== -1) {
|
||||
const jsonStr = text.substring(start, i + 1);
|
||||
try {
|
||||
objects.push(JSON.parse(jsonStr));
|
||||
} catch { }
|
||||
start = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return objects;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveHubCloud(hubUrl, ua) {
|
||||
try {
|
||||
const res1 = await fetch(hubUrl, {
|
||||
headers: {
|
||||
'User-Agent': ua,
|
||||
Referer: REFERER,
|
||||
},
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
|
||||
const html = await res1.text();
|
||||
const hubPhpMatch = html.match(/https?:\/\/[^\s"<>]+\/hubcloud\.php\?[^\s"<>]+/i);
|
||||
if (!hubPhpMatch) return null;
|
||||
|
||||
const phpUrl = hubPhpMatch[0];
|
||||
const phpRes = await fetch(phpUrl, {
|
||||
headers: {
|
||||
'User-Agent': ua,
|
||||
Referer: hubUrl,
|
||||
},
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
|
||||
const phpBody = await phpRes.text();
|
||||
|
||||
const r2Match = phpBody.match(/https?:\/\/[a-zA-Z0-9.\-_]+\.r2\.cloudflarestorage\.com\/[^\s"<>]+/i);
|
||||
if (r2Match) {
|
||||
return r2Match[0].replaceAll('&', '&');
|
||||
}
|
||||
|
||||
const pixelMatch = phpBody.match(/https?:\/\/pixel\.hubcloud\.[a-z]+\/\?id=[^\s"<>]+/i);
|
||||
if (pixelMatch) {
|
||||
return pixelMatch[0];
|
||||
}
|
||||
} catch { }
|
||||
return null;
|
||||
}
|
||||
|
||||
async function resolveClicknUpload(clicknUrl, ua) {
|
||||
try {
|
||||
const res1 = await fetch(clicknUrl, {
|
||||
headers: {
|
||||
'User-Agent': ua,
|
||||
Referer: REFERER,
|
||||
},
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
|
||||
const body1 = await res1.text();
|
||||
const formMatch1 = body1.match(/<form[^>]+method=["']POST["'][^>]*>([\s\S]*?)<\/form>/i);
|
||||
if (!formMatch1) return null;
|
||||
|
||||
const inputMatches1 = [...formMatch1[1].matchAll(/<input[^>]+name=["']([^"']+)["'][^>]+value=["']([^"']*)["']/gi)];
|
||||
const params1 = new URLSearchParams();
|
||||
for (const m of inputMatches1) {
|
||||
params1.set(m[1], m[2]);
|
||||
}
|
||||
params1.set('method_free', 'Slow Download');
|
||||
|
||||
const rawCookies = res1.headers.getSetCookie?.() || [res1.headers.get('set-cookie')].filter(Boolean);
|
||||
const cookieHeader = rawCookies.join('; ');
|
||||
|
||||
const res2 = await fetch(clicknUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'User-Agent': ua,
|
||||
Referer: clicknUrl,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
...(cookieHeader ? { Cookie: cookieHeader } : {}),
|
||||
},
|
||||
body: params1.toString(),
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
|
||||
const body2 = await res2.text();
|
||||
const formMatch2 = body2.match(/<form[^>]+method=["']POST["'][^>]*>([\s\S]*?)<\/form>/i);
|
||||
if (!formMatch2) return null;
|
||||
|
||||
const inputMatches2 = [...formMatch2[1].matchAll(/<input[^>]+name=["']([^"']+)["'][^>]+value=["']([^"']*)["']/gi)];
|
||||
const params2 = new URLSearchParams();
|
||||
for (const m of inputMatches2) {
|
||||
params2.set(m[1], m[2]);
|
||||
}
|
||||
params2.set('down_script', '1');
|
||||
|
||||
await new Promise(r => setTimeout(r, 4500));
|
||||
|
||||
const res3 = await fetch(clicknUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'User-Agent': ua,
|
||||
Referer: clicknUrl,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
...(cookieHeader ? { Cookie: cookieHeader } : {}),
|
||||
},
|
||||
body: params2.toString(),
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
|
||||
const body3 = await res3.text();
|
||||
const directMatch = body3.match(/https?:\/\/[a-zA-Z0-9.\-_:]+\/d\/[a-zA-Z0-9_\-\/]+/i) ||
|
||||
body3.match(/window\.open\(["'](https?:\/\/[^"']+)["']\)/i);
|
||||
|
||||
if (directMatch) {
|
||||
const found = directMatch[1] || directMatch[0];
|
||||
if (found.includes('clicknupload.') && !found.includes('/d/')) {
|
||||
return null;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
} catch { }
|
||||
return null;
|
||||
}
|
||||
|
||||
async function resolveItem(item, fallbackTitle, ua) {
|
||||
const rawUrl = String(item.url || '').trim();
|
||||
if (!rawUrl) return null;
|
||||
|
||||
if (
|
||||
rawUrl.includes('111477.xyz') ||
|
||||
rawUrl.includes('vadapav.mov') ||
|
||||
rawUrl.includes('driveseed.org') ||
|
||||
rawUrl.includes('new3.gdflix.io') ||
|
||||
rawUrl.includes('rapidrar.cr') ||
|
||||
rawUrl.includes('megaup.net') ||
|
||||
rawUrl.includes('telegram.dog') ||
|
||||
rawUrl.includes('t.me')
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let directStreamUrl = null;
|
||||
let provider = item.site || 'DownloadEverything';
|
||||
let streamHeaders = { 'User-Agent': ua };
|
||||
|
||||
try {
|
||||
if (rawUrl.includes('hakunaymatata.com')) {
|
||||
directStreamUrl = rawUrl;
|
||||
provider = 'Moviebox';
|
||||
streamHeaders = { 'User-Agent': 'Lavf/60.16.100' };
|
||||
} else if (rawUrl.includes('pixeldrain.dev') || rawUrl.includes('pixeldrain.com')) {
|
||||
const m = rawUrl.match(/pixeldrain\.(?:dev|com)\/(?:u|l)\/([a-zA-Z0-9_-]+)/);
|
||||
if (m) {
|
||||
directStreamUrl = `https://pixeldrain.com/api/file/${m[1]}`;
|
||||
provider = 'Pixeldrain';
|
||||
}
|
||||
} else if (rawUrl.includes('hubcloud.') || rawUrl.includes('vcloud.zip')) {
|
||||
directStreamUrl = await resolveHubCloud(rawUrl, ua);
|
||||
if (directStreamUrl) {
|
||||
provider = 'HubCloud';
|
||||
}
|
||||
} else if (rawUrl.includes('clicknupload.')) {
|
||||
directStreamUrl = await resolveClicknUpload(rawUrl, ua);
|
||||
if (directStreamUrl) {
|
||||
provider = 'ClicknUpload';
|
||||
}
|
||||
} else if (
|
||||
/\.(?:mp4|mkv)(?:\?|$)/i.test(rawUrl) &&
|
||||
!rawUrl.includes('111477.xyz') &&
|
||||
!rawUrl.includes('vadapav.mov') &&
|
||||
!rawUrl.includes('.cyou/res/')
|
||||
) {
|
||||
try {
|
||||
const check = await fetch(rawUrl, {
|
||||
method: 'HEAD',
|
||||
headers: { 'User-Agent': ua },
|
||||
signal: AbortSignal.timeout(2000),
|
||||
});
|
||||
if (check.status === 200 || check.status === 206 || (check.status >= 300 && check.status < 400)) {
|
||||
directStreamUrl = rawUrl;
|
||||
provider = item.site || 'DirectStream';
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
} catch { }
|
||||
|
||||
if (!directStreamUrl) return null;
|
||||
|
||||
const tags = Array.isArray(item.tags) ? item.tags.map(String) : [];
|
||||
const qualityMatch = tags.find(t => /2160p|4k|1080p|720p|480p/i.test(t)) || '1080p';
|
||||
const rawTitle = item.name || item.release || fallbackTitle;
|
||||
|
||||
return {
|
||||
url: directStreamUrl,
|
||||
label: `[${provider}] ${rawTitle} (${qualityMatch})`,
|
||||
headers: streamHeaders,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e, sdk }) {
|
||||
const isTv = Boolean(s && e);
|
||||
const mediaType = isTv ? 'series' : 'movie';
|
||||
const ua = USER_AGENT;
|
||||
|
||||
const details = await resolveDetails(sdk, id, isTv);
|
||||
const title = details?.title || String(id);
|
||||
const tmdbId = parseInt(id, 10);
|
||||
|
||||
const payload = {
|
||||
mode: mediaType,
|
||||
title,
|
||||
...(details?.year ? { year: String(details.year) } : {}),
|
||||
...(tmdbId ? { tmdb_id: tmdbId } : {}),
|
||||
...(details?.imdbId ? { imdb_id: details.imdbId } : {}),
|
||||
...(isTv && s != null ? { season: Number(s) } : {}),
|
||||
...(isTv && e != null ? { episode: Number(e) } : {}),
|
||||
};
|
||||
|
||||
const res = await fetch(SLAVE_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'User-Agent': ua,
|
||||
Origin: ORIGIN,
|
||||
Referer: REFERER,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/x-ndjson',
|
||||
'x-defe-manual': '1',
|
||||
'sec-ch-ua': '"Microsoft Edge";v="153", "Not_A Brand";v="8", "Chromium";v="153"',
|
||||
'sec-ch-ua-mobile': '?0',
|
||||
'sec-ch-ua-platform': '"Windows"',
|
||||
'sec-fetch-dest': 'empty',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'sec-fetch-site': 'same-site',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
signal: AbortSignal.timeout(16000),
|
||||
});
|
||||
|
||||
if (!res.ok) return null;
|
||||
|
||||
let fullText = '';
|
||||
if (res.body?.getReader) {
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
fullText += decoder.decode(value, { stream: true });
|
||||
}
|
||||
} catch { }
|
||||
} else {
|
||||
fullText = await res.text();
|
||||
}
|
||||
|
||||
const objects = extractJsonObjects(fullText);
|
||||
const candidateItems = [];
|
||||
|
||||
for (const parsed of objects) {
|
||||
if (parsed?.t === 'hit' && Array.isArray(parsed.links)) {
|
||||
const site = parsed.site || 'DownloadEverything';
|
||||
for (const l of parsed.links) {
|
||||
if (l && typeof l === 'object') {
|
||||
candidateItems.push({ site, ...l });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resolved = await Promise.all(candidateItems.map(item => resolveItem(item, title, ua)));
|
||||
const allUrls = resolved.filter(Boolean);
|
||||
|
||||
if (allUrls.length === 0) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
allUrls,
|
||||
headers: allUrls[0].headers,
|
||||
};
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
import { USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = "https://mapple.club";
|
||||
const PROXY_HEADERS = { Referer: `${BASE_URL}/`, Origin: BASE_URL, "User-Agent": USER_AGENT };
|
||||
|
||||
const K = new Uint32Array([0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2]);
|
||||
const W_BUF = new Uint32Array(64);
|
||||
|
||||
function sha256Into(buf, total, out) {
|
||||
let h0 = 0x6a09e667, h1 = 0xbb67ae85, h2 = 0x3c6ef372, h3 = 0xa54ff53a, h4 = 0x510e527f, h5 = 0x9b05688c, h6 = 0x1f83d9ab, h7 = 0x5be0cd19;
|
||||
const dv = new DataView(buf.buffer, buf.byteOffset, total);
|
||||
for (let off = 0; off < total; off += 64) {
|
||||
for (let i = 0; i < 16; i++) W_BUF[i] = dv.getUint32(off + i * 4, false);
|
||||
for (let i = 16; i < 64; i++) {
|
||||
const a = W_BUF[i - 15], b = W_BUF[i - 2];
|
||||
const s0 = ((a >>> 7) | (a << 25)) ^ ((a >>> 18) | (a << 14)) ^ (a >>> 3);
|
||||
const s1 = ((b >>> 17) | (b << 15)) ^ ((b >>> 19) | (b << 13)) ^ (b >>> 10);
|
||||
W_BUF[i] = (W_BUF[i - 16] + s0 + W_BUF[i - 7] + s1) | 0;
|
||||
}
|
||||
let a = h0, b = h1, c = h2, d = h3, e = h4, f = h5, g = h6, h = h7;
|
||||
for (let i = 0; i < 64; i++) {
|
||||
const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
|
||||
const ch = (e & f) ^ (~e & g);
|
||||
const t1 = (h + S1 + ch + K[i] + W_BUF[i]) | 0;
|
||||
const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
|
||||
const maj = (a & b) ^ (a & c) ^ (b & c);
|
||||
const t2 = (S0 + maj) | 0;
|
||||
h = g; g = f; f = e; e = (d + t1) | 0; d = c; c = b; b = a; a = (t1 + t2) | 0;
|
||||
}
|
||||
h0 = (h0 + a) | 0; h1 = (h1 + b) | 0; h2 = (h2 + c) | 0; h3 = (h3 + d) | 0; h4 = (h4 + e) | 0; h5 = (h5 + f) | 0; h6 = (h6 + g) | 0; h7 = (h7 + h) | 0;
|
||||
}
|
||||
const odv = new DataView(out.buffer, out.byteOffset, 32);
|
||||
odv.setUint32(0, h0 >>> 0, false); odv.setUint32(4, h1 >>> 0, false); odv.setUint32(8, h2 >>> 0, false); odv.setUint32(12, h3 >>> 0, false); odv.setUint32(16, h4 >>> 0, false); odv.setUint32(20, h5 >>> 0, false); odv.setUint32(24, h6 >>> 0, false); odv.setUint32(28, h7 >>> 0, false);
|
||||
}
|
||||
|
||||
function solvePoW(challenge, difficulty) {
|
||||
const encoder = new TextEncoder();
|
||||
const cBuf = encoder.encode(challenge);
|
||||
const maxLen = cBuf.length + 16;
|
||||
const total = (maxLen + 1 + 8 + 63) & ~63;
|
||||
const buf = new Uint8Array(total);
|
||||
buf.set(cBuf);
|
||||
const hash = new Uint8Array(32);
|
||||
const maskBytes = Math.floor(difficulty / 8);
|
||||
const maskBits = difficulty % 8;
|
||||
const finalMask = maskBits > 0 ? (0xff << (8 - maskBits)) & 0xff : 0;
|
||||
const dv = new DataView(buf.buffer);
|
||||
for (let nonce = 0; nonce < 10000000; nonce++) {
|
||||
const str = nonce.toString();
|
||||
const len = cBuf.length + str.length;
|
||||
for (let i = 0; i < str.length; i++) buf[cBuf.length + i] = str.charCodeAt(i);
|
||||
buf[len] = 0x80;
|
||||
for (let i = len + 1; i < total - 8; i++) buf[i] = 0;
|
||||
dv.setUint32(total - 4, len * 8, false);
|
||||
dv.setUint32(total - 8, 0, false);
|
||||
sha256Into(buf, total, hash);
|
||||
let ok = true;
|
||||
for (let i = 0; i < maskBytes; i++) if (hash[i] !== 0) { ok = false; break; }
|
||||
if (ok && (finalMask === 0 || (hash[maskBytes] & finalMask) === 0)) return { found: true, nonce: str };
|
||||
}
|
||||
return { found: false };
|
||||
}
|
||||
|
||||
const SERVER_LIST = [{ id: "mapple", name: "Mapple" }, { id: "s1", name: "Nexus" }, { id: "s2", name: "Cipher" }, { id: "s3", name: "Pulse" }, { id: "s4", name: "Vertex" }, { id: "s10", name: "Chimp" }];
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, server } = args;
|
||||
const isTv = s != null && e != null;
|
||||
const mediaType = isTv ? "tv" : "movie";
|
||||
const tvSlug = isTv ? `${s}-${e}` : "";
|
||||
const pageUrl = isTv ? `${BASE_URL}/watch/tv/${id}/${s}/${e}` : `${BASE_URL}/watch/movie/${id}`;
|
||||
try {
|
||||
const pageRes = await fetch(pageUrl, { headers: PROXY_HEADERS, signal: AbortSignal.timeout(10000) });
|
||||
if (!pageRes.ok) return null;
|
||||
const html = await pageRes.text();
|
||||
const reqMatch = html.match(/window\.__REQUEST_TOKEN__\s*=\s*"([^"]+)"/);
|
||||
if (!reqMatch) return null;
|
||||
const requestToken = reqMatch[1];
|
||||
const cookies = pageRes.headers.getSetCookie();
|
||||
const cookieHeader = cookies.map(c => c.split(';')[0]).join('; ');
|
||||
|
||||
const initHeaders = { "Content-Type": "application/json", ...PROXY_HEADERS, Referer: pageUrl };
|
||||
if (cookieHeader) initHeaders.Cookie = cookieHeader;
|
||||
|
||||
const initRes = await fetch(`${BASE_URL}/api/playback-init`, {
|
||||
method: "POST",
|
||||
headers: initHeaders,
|
||||
body: JSON.stringify({ mediaId: Number(id), mediaType, requestToken }),
|
||||
signal: AbortSignal.timeout(10000)
|
||||
});
|
||||
const initData = await initRes.json();
|
||||
let streamToken = initData.token;
|
||||
if (initData.requiresPow && initData.pow) {
|
||||
const powRes = solvePoW(initData.pow.challenge, initData.pow.difficulty);
|
||||
if (powRes.found) {
|
||||
const solveRes = await fetch(`${BASE_URL}/api/playback-init`, {
|
||||
method: "POST",
|
||||
headers: initHeaders,
|
||||
body: JSON.stringify({ mediaId: Number(id), mediaType, requestToken, pow: { challengeId: initData.pow.challengeId, nonce: powRes.nonce } }),
|
||||
signal: AbortSignal.timeout(10000)
|
||||
});
|
||||
streamToken = (await solveRes.json()).token;
|
||||
}
|
||||
}
|
||||
if (!streamToken) return null;
|
||||
let targets = SERVER_LIST;
|
||||
if (server && server !== "all") {
|
||||
const clean = server.replace("Mapple - ", "");
|
||||
targets = SERVER_LIST.filter(sv => sv.name === clean);
|
||||
if (!targets.length) targets = SERVER_LIST;
|
||||
}
|
||||
const allUrls = [];
|
||||
for (const srv of targets) {
|
||||
try {
|
||||
const encryptRes = await fetch(`${BASE_URL}/api/encrypt`, {
|
||||
method: "POST",
|
||||
headers: initHeaders,
|
||||
body: JSON.stringify({
|
||||
data: {
|
||||
mediaId: Number(id),
|
||||
mediaType,
|
||||
tv_slug: tvSlug,
|
||||
source: srv.id,
|
||||
apikey: "mptv_sk_a8f29c4e7b3d1f"
|
||||
}
|
||||
}),
|
||||
signal: AbortSignal.timeout(8000)
|
||||
});
|
||||
const encryptData = await encryptRes.json();
|
||||
if (!encryptData.encrypted) continue;
|
||||
|
||||
const streamUrl = `${BASE_URL}/api/stream-encrypted?data=${encodeURIComponent(encryptData.encrypted)}&requestToken=${encodeURIComponent(requestToken)}&token=${encodeURIComponent(streamToken)}`;
|
||||
const streamRes = await fetch(streamUrl, {
|
||||
headers: { ...PROXY_HEADERS, Referer: pageUrl, ...(cookieHeader ? { Cookie: cookieHeader } : {}) },
|
||||
signal: AbortSignal.timeout(8000)
|
||||
});
|
||||
const streamData = await streamRes.json();
|
||||
if (streamData.success && streamData.data?.stream_url) {
|
||||
let fileUrl = streamData.data.stream_url;
|
||||
if (fileUrl.includes('omena-puu') || fileUrl.includes('nocach')) fileUrl += fileUrl.includes('?') ? '&format=.m3u8' : '?format=.m3u8';
|
||||
allUrls.push({ url: fileUrl, server: `Mapple - ${srv.name}`, headers: PROXY_HEADERS, skipProxy: false, skipVerify: true, skipHlsCheck: true });
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
return allUrls.length ? { allUrls } : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
export async function getSources() { return SERVER_LIST.map(s => `Mapple - ${s.name}`); }
|
||||
export const SKIP_VERIFY = true;
|
||||
export const MULTI_URL = true;
|
||||
@@ -1,47 +0,0 @@
|
||||
import { fetchJson } from '../utils/helpers.js';
|
||||
|
||||
const CONFIGURATION = 'W3siaWQiOiJkZWZhdWx0IiwibmFtZSI6Ik1lZ2FTb3VyY2UgZGVmYXVsdCIsInVybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS96b3JldS9tZWdhc291cmNlX3NjcmFwZXJzL3Jhdy9yZWZzL2hlYWRzL21haW4vZGVmYXVsdF9zY3JhcGVyLnB5IiwiZGVzY3JpcHRpb24iOiJEZWZhdWx0IHNjcmFwZXIgaG9zdGVkIG9uIEdpdEh1Yi4ifV0';
|
||||
const BASE_URL = `https://megasource.wasmer.app/${CONFIGURATION}`;
|
||||
|
||||
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;
|
||||
if (!tmdbApiKey) 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: `MegaSource - ${stream.title ? ` ${stream.title}` : ''}`,
|
||||
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
||||
skipProxy: Object.keys(headers).length > 0 && stream.behaviorHints?.notWebReady === false
|
||||
};
|
||||
}).filter(Boolean);
|
||||
|
||||
return allUrls.length ? { allUrls } : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ const API_BASE = 'https://api.meowtv.ru';
|
||||
const REFERER = 'https://meowtv.ru/';
|
||||
const ENC_DEC_API = 'https://enc-dec.app/api/dec-meowtv';
|
||||
|
||||
const HEADERS = {
|
||||
const BASE_HEADERS = {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Accept': 'application/json',
|
||||
'Referer': REFERER,
|
||||
@@ -13,13 +13,46 @@ const HEADERS = {
|
||||
};
|
||||
|
||||
const SERVERS = [
|
||||
'hindiv3', 'tcloud'
|
||||
'dcloud', 'acme', 'tcloud', 'ipcloud', 'turkce', 'hindiv3'
|
||||
];
|
||||
|
||||
const TIMEOUT_MS = 15000;
|
||||
|
||||
export const VERIFY_HEADERS = { 'User-Agent': USER_AGENT, 'Referer': REFERER, 'Origin': 'https://meowtv.ru' };
|
||||
|
||||
let cachedTicket = null;
|
||||
let ticketExpiry = 0;
|
||||
|
||||
/**
|
||||
* Fetches a stream ticket or returns a cached valid ticket.
|
||||
*/
|
||||
async function getTicket(forceRefresh = false) {
|
||||
const now = Date.now();
|
||||
if (!forceRefresh && cachedTicket && now < ticketExpiry - 10000) {
|
||||
return cachedTicket;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetchJson(`${API_BASE}/streams/ticket`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...BASE_HEADERS,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({})
|
||||
});
|
||||
|
||||
if (res?.ticket) {
|
||||
cachedTicket = res.ticket;
|
||||
ticketExpiry = typeof res.exp === 'number' ? res.exp : now + 60000;
|
||||
return cachedTicket;
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function decryptPayload(payload) {
|
||||
try {
|
||||
const response = await fetchJson(ENC_DEC_API, {
|
||||
@@ -47,6 +80,12 @@ export async function getStream({ id, s, e, server }) {
|
||||
targets = SERVERS.includes(cleanName) ? [cleanName] : [targets[0]];
|
||||
}
|
||||
|
||||
const ticket = await getTicket();
|
||||
const requestHeaders = {
|
||||
...BASE_HEADERS,
|
||||
...(ticket ? { 'x-stream-ticket': ticket } : {})
|
||||
};
|
||||
|
||||
const results = await Promise.all(targets.map(async srv => {
|
||||
try {
|
||||
return await Promise.race([
|
||||
@@ -56,7 +95,7 @@ export async function getStream({ id, s, e, server }) {
|
||||
: `/streams/movie/${id}?s=${encodeURIComponent(srv)}`;
|
||||
|
||||
const payload = await fetchJson(`${API_BASE}${path}`, {
|
||||
headers: HEADERS
|
||||
headers: requestHeaders
|
||||
});
|
||||
|
||||
if (!payload) return null;
|
||||
|
||||
117
src/sources/movienight.js
Normal file
117
src/sources/movienight.js
Normal file
@@ -0,0 +1,117 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
282
src/sources/movy.js
Normal file
282
src/sources/movy.js
Normal file
@@ -0,0 +1,282 @@
|
||||
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,46 +0,0 @@
|
||||
import { fetchJson } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = `https://nova-streamz.vercel.app`;
|
||||
|
||||
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;
|
||||
if (!tmdbApiKey) 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: `Nova - ${stream.name.replace('Nova ', '') || 'Server'}${stream.title ? ` ${stream.title.split(' | ')[0] || ''}` : ''}`,
|
||||
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
||||
skipProxy: Object.keys(headers).length > 0 && stream.behaviorHints?.notWebReady === false
|
||||
};
|
||||
}).filter(Boolean);
|
||||
|
||||
return allUrls.length ? { allUrls } : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { fetchJson, USER_AGENT, getTmdbInfo } from '../utils/helpers.js';
|
||||
|
||||
const DOMAIN = 'https://purstream.club';
|
||||
const API_BASE = 'https://api.purstream.club/api/v1';
|
||||
const DOMAIN = 'https://purstream.ad';
|
||||
const API_BASE = 'https://api.purstream.ad/api/v1';
|
||||
|
||||
const HEADERS = {
|
||||
'User-Agent': USER_AGENT,
|
||||
|
||||
127
src/sources/streamaggregator.js
Normal file
127
src/sources/streamaggregator.js
Normal file
@@ -0,0 +1,127 @@
|
||||
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: 'application/x-ndjson, text/plain, */*',
|
||||
},
|
||||
signal: AbortSignal.timeout(18000),
|
||||
});
|
||||
|
||||
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 item = JSON.parse(line);
|
||||
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,
|
||||
};
|
||||
}
|
||||
85
src/sources/vadapav.js
Normal file
85
src/sources/vadapav.js
Normal file
@@ -0,0 +1,85 @@
|
||||
import { getUA } from '../utils/helpers.js';
|
||||
|
||||
const ADDON_BASE = 'https://stremio.vadapav.mov';
|
||||
|
||||
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 imdbId = await resolveImdbId(sdk, id, isTv);
|
||||
const targetIds = [];
|
||||
if (imdbId && imdbId.startsWith('tt')) {
|
||||
targetIds.push(imdbId);
|
||||
}
|
||||
targetIds.push(`tmdb:${id}`);
|
||||
|
||||
const seenUrls = new Set();
|
||||
const allUrls = [];
|
||||
|
||||
const defaultHeaders = {
|
||||
'User-Agent': ua,
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
};
|
||||
|
||||
for (const targetId of targetIds) {
|
||||
const streamPath = isTv
|
||||
? `${ADDON_BASE}/stream/series/${targetId}:${s}:${e}.json`
|
||||
: `${ADDON_BASE}/stream/movie/${targetId}.json`;
|
||||
|
||||
try {
|
||||
const res = await fetch(streamPath, {
|
||||
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() : 'vadapav.mov';
|
||||
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,94 +0,0 @@
|
||||
import { getTmdbInfo, fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const SPEEDRACE_BASE = "https://api.speedracelight.com";
|
||||
const HEADERS = { "Accept": "*/*", "Origin": "https://player.videasy.to", "Referer": "https://player.videasy.to/", "User-Agent": USER_AGENT };
|
||||
const SERVERS = [
|
||||
{ id: 'cdn', name: 'Yoru' },
|
||||
{ id: 'm4uhd', name: 'Breach' },
|
||||
{ id: 'vsrc', name: 'Neon' },
|
||||
{ id: 'hdmovie', name: 'Vyse' },
|
||||
{ id: 'meine', name: 'Killjoy' },
|
||||
{ id: 'lamovie', name: 'Omen' },
|
||||
{ id: 'superflix', name: 'Raze' }
|
||||
];
|
||||
|
||||
export async function getStream({ id, s, e, server, 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 seedData = await fetchJson(`${SPEEDRACE_BASE}/seed?mediaId=${id}`, { headers: HEADERS, signal: AbortSignal.timeout(5000) });
|
||||
const seed = seedData?.seed;
|
||||
if (!seed) return null;
|
||||
|
||||
const encTitle = encodeURIComponent(encodeURIComponent(info.titles[0]));
|
||||
let targets = SERVERS;
|
||||
|
||||
if (server && server !== 'all') {
|
||||
const cleanName = server.replace('VidEasy - ', '');
|
||||
targets = SERVERS.filter(sv => sv.name === cleanName);
|
||||
if (!targets.length) targets = SERVERS;
|
||||
}
|
||||
|
||||
const settled = await Promise.allSettled(targets.map(async srv => {
|
||||
let url = `${SPEEDRACE_BASE}/${srv.id}/sources-with-title?title=${encTitle}&mediaType=${isTv ? 'tv' : 'movie'}&year=${info.year || ''}&tmdbId=${id}&imdbId=${info.imdbId || "tt0000000"}&enc=2&seed=${seed}`;
|
||||
if (isTv) url += `&episodeId=${e}&seasonId=${s}`;
|
||||
|
||||
const encText = await fetchText(url, { headers: HEADERS, signal: AbortSignal.timeout(10000) });
|
||||
const decJson = await fetchJson("https://enc-dec.app/api/dec-videasy", {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: encText, id: String(id), seed }),
|
||||
signal: AbortSignal.timeout(10000)
|
||||
});
|
||||
|
||||
if (decJson.status !== 200 || !decJson.result) throw new Error();
|
||||
|
||||
let sourcesArray = [];
|
||||
let subsArray = [];
|
||||
|
||||
if (Array.isArray(decJson.result)) {
|
||||
sourcesArray = decJson.result;
|
||||
} else if (decJson.result.sources && Array.isArray(decJson.result.sources)) {
|
||||
sourcesArray = decJson.result.sources;
|
||||
if (decJson.result.subtitles && Array.isArray(decJson.result.subtitles)) {
|
||||
subsArray = decJson.result.subtitles.map(sub => ({
|
||||
url: sub.url || sub.file,
|
||||
lang: sub.language || sub.lang || sub.label || 'Unknown'
|
||||
})).filter(sub => sub.url);
|
||||
}
|
||||
} else {
|
||||
sourcesArray = [decJson.result];
|
||||
}
|
||||
|
||||
return sourcesArray.map(res => {
|
||||
const streamUrl = res.url || res.file || res.link || res.playlist || res.stream;
|
||||
return streamUrl ? {
|
||||
url: streamUrl,
|
||||
quality: res.quality || "Auto",
|
||||
server: `VidEasy - ${srv.name}`,
|
||||
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: HEADERS,
|
||||
subtitles: subsArray.length > 0 ? subsArray : undefined,
|
||||
skipProxy: false,
|
||||
skipVerify: true,
|
||||
skipHlsCheck: true
|
||||
} : null;
|
||||
}).filter(Boolean);
|
||||
}));
|
||||
|
||||
const allUrls = [];
|
||||
for (const r of settled) {
|
||||
if (r.status === 'fulfilled' && r.value) allUrls.push(...r.value);
|
||||
}
|
||||
|
||||
return allUrls.length ? { allUrls } : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
export async function getSources() {
|
||||
return SERVERS.map(s => `VidEasy - ${s.name}`);
|
||||
}
|
||||
@@ -1,113 +1,187 @@
|
||||
import { USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = "https://vidvault.ru";
|
||||
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,
|
||||
'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(`${BASE_URL}/api/get-token`, {
|
||||
method: "GET",
|
||||
headers: HEADERS
|
||||
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;
|
||||
return data?.t;
|
||||
}
|
||||
|
||||
function getCaptionType(url) {
|
||||
return url.toLowerCase().includes(".vtt") ? "vtt" : "srt";
|
||||
return String(url).toLowerCase().includes('.vtt') ? 'vtt' : 'srt';
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args;
|
||||
const type = (s != null && e != null) ? "tv" : "movie";
|
||||
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 (type === "tv") {
|
||||
payload.season = String(s);
|
||||
payload.episode = String(e);
|
||||
const payload = {
|
||||
type,
|
||||
tmdbId: String(id),
|
||||
};
|
||||
|
||||
if (isTv) {
|
||||
payload.season = Number(s);
|
||||
payload.episode = Number(e);
|
||||
}
|
||||
|
||||
const res = await fetch(`${BASE_URL}/api/download-proxy`, {
|
||||
method: "POST",
|
||||
const res = await fetch(`${API_BASE}/download-proxy`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
...HEADERS,
|
||||
"Content-Type": "application/json",
|
||||
"x-request-token": token
|
||||
'Content-Type': 'application/json',
|
||||
'x-request-token': token,
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
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;
|
||||
if (mp4) {
|
||||
const country = mp4.country || "";
|
||||
const lanName = mp4.lanName || "";
|
||||
const detailPath = mp4.detailPath || "";
|
||||
const downloadData = mp4?.downloadInfo?.data || mp4?.data?.data || mp4?.data || mp4;
|
||||
|
||||
if (downloadData) {
|
||||
const streams = downloadData.streams || downloadData.downloads || [];
|
||||
for (const item of streams) {
|
||||
if (!item?.url) continue;
|
||||
|
||||
const res = item.resolutions || item.resolution || 'Auto';
|
||||
const sizeStr = item.size ? ` (${typeof item.size === 'number' ? formatBytes(item.size) : item.size})` : '';
|
||||
const proxiedUrl = `${DL_PROXY_BASE}/${encodeURIComponent(item.url)}?n=${titleTag}`;
|
||||
|
||||
const downloads = mp4.downloadInfo?.data?.downloads || [];
|
||||
for (const d of downloads) {
|
||||
if (!d?.url) continue;
|
||||
allUrls.push({
|
||||
quality: `${d.resolution}p ${detailPath} ${country} ${lanName}`.trim(),
|
||||
url: d.url,
|
||||
type: "mp4",
|
||||
headers: HEADERS
|
||||
url: proxiedUrl,
|
||||
server: `VidVault - MP4 ${res}p${sizeStr}`.trim(),
|
||||
quality: typeof res === 'number' ? `${res}p` : res,
|
||||
type: 'mp4',
|
||||
headers: HEADERS,
|
||||
});
|
||||
}
|
||||
|
||||
const srtCaptions = mp4.downloadInfo?.data?.captions || [];
|
||||
for (const c of srtCaptions) {
|
||||
if (!c?.url) continue;
|
||||
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: c.lanName || c.lan || "Unknown",
|
||||
type: getCaptionType(c.url),
|
||||
language: c.lan || "",
|
||||
url: c.url
|
||||
label: cap.lanName || cap.lan || 'Unknown',
|
||||
type: getCaptionType(cap.url),
|
||||
language: cap.lan || '',
|
||||
url: subUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const mkvFiles = data?.mkvData?.files || [];
|
||||
for (const f of mkvFiles) {
|
||||
if (!f?.url) continue;
|
||||
for (const file of mkvFiles) {
|
||||
if (!file?.url) continue;
|
||||
const sizeStr = file.size ? ` (${typeof file.size === 'number' ? formatBytes(file.size) : file.size})` : '';
|
||||
allUrls.push({
|
||||
quality: `MKV ${f.size || ""}`.trim(),
|
||||
url: f.url,
|
||||
type: "mp4",
|
||||
headers: HEADERS
|
||||
url: file.url,
|
||||
server: `VidVault - MKV 480p${sizeStr}`.trim(),
|
||||
quality: '480p',
|
||||
type: 'mp4',
|
||||
headers: HEADERS,
|
||||
});
|
||||
}
|
||||
|
||||
const mkvV2 = data?.mkvV2Data;
|
||||
if (mkvV2?.url) {
|
||||
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({
|
||||
quality: `${mkvV2.quality || ""} ${mkvV2.language || ""} ${mkvV2.country || ""} (${mkvV2.size || ""})`.trim(),
|
||||
url: mkvV2.url,
|
||||
type: "mp4",
|
||||
headers: HEADERS
|
||||
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?.url) {
|
||||
allUrls.push({
|
||||
quality: `${mkvV3.quality || ""} ${mkvV3.language || ""} ${mkvV3.country || ""} (${mkvV3.size || ""})`.trim(),
|
||||
url: mkvV3.url,
|
||||
type: "mp4",
|
||||
headers: HEADERS
|
||||
});
|
||||
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 } : null;
|
||||
return allUrls.length ? { allUrls, subtitles: subtitles.length ? subtitles : undefined } : null;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
|
||||
249
src/sources/vuflix.js
Normal file
249
src/sources/vuflix.js
Normal file
@@ -0,0 +1,249 @@
|
||||
import { getUA } from '../utils/helpers.js';
|
||||
|
||||
const API_BASE = 'https://vuflix.co';
|
||||
const REFERER = 'https://vuflix.co/';
|
||||
const ORIGIN = 'https://vuflix.co';
|
||||
|
||||
let cachedProviders = null;
|
||||
let lastProvidersFetch = 0;
|
||||
|
||||
const FALLBACK_PROVIDERS = [
|
||||
{ id: 'vsembed', name: 'Sigma' },
|
||||
{ id: 'moonflix', name: 'Source 40' },
|
||||
{ id: 'megasource', name: 'Source 39' },
|
||||
{ id: 'hdghar', name: 'Source 44' },
|
||||
{ id: 'moviebox', name: 'Pi' },
|
||||
{ id: 'cineplay', name: '4K' },
|
||||
{ id: 'huhu', name: 'Beta' },
|
||||
{ id: 'bingr', name: 'Upsilon' },
|
||||
{ id: 'onlyflix', name: 'Gamma' },
|
||||
{ id: 'vaplayer', name: 'Alpha' },
|
||||
{ id: 'flixhqz', name: 'Gamma' },
|
||||
{ id: 'castle', name: 'Source 40' },
|
||||
{ id: 'cinejoy', name: '4K2' },
|
||||
{ id: 'filesun', name: 'Tau' },
|
||||
{ id: 'yoru', name: 'Yoru' },
|
||||
];
|
||||
|
||||
async function getProviders(ua) {
|
||||
if (cachedProviders && Date.now() - lastProvidersFetch < 900000) {
|
||||
return cachedProviders;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/player/providers`, {
|
||||
headers: {
|
||||
'User-Agent': ua,
|
||||
Referer: REFERER,
|
||||
Origin: ORIGIN,
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
},
|
||||
signal: AbortSignal.timeout(6000),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data?.ok && Array.isArray(data.providers)) {
|
||||
const list = data.providers
|
||||
.filter(p => p && typeof p.id === 'string' && p.id.trim().length > 0)
|
||||
.map(p => ({
|
||||
id: p.id.trim(),
|
||||
name: p.publicLabel || p.providerName || p.name || p.id,
|
||||
}));
|
||||
|
||||
if (list.length > 0) {
|
||||
cachedProviders = list;
|
||||
lastProvidersFetch = Date.now();
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
|
||||
return FALLBACK_PROVIDERS;
|
||||
}
|
||||
|
||||
function unwrapUrl(rawUrl, ua) {
|
||||
const defaultHeaders = {
|
||||
'User-Agent': ua,
|
||||
Referer: REFERER,
|
||||
Origin: ORIGIN,
|
||||
};
|
||||
|
||||
if (!rawUrl || typeof rawUrl !== 'string') {
|
||||
return { url: '', headers: defaultHeaders };
|
||||
}
|
||||
|
||||
if (!rawUrl.includes('v-relay?t=') && !rawUrl.includes('a-relay?t=')) {
|
||||
return { url: rawUrl, headers: defaultHeaders };
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedUrl = new URL(rawUrl);
|
||||
const t = parsedUrl.searchParams.get('t');
|
||||
if (!t) return { url: rawUrl, headers: defaultHeaders };
|
||||
|
||||
let b64 = t.replace(/-/g, '+').replace(/_/g, '/');
|
||||
while (b64.length % 4 !== 0) {
|
||||
b64 += '=';
|
||||
}
|
||||
|
||||
const jsonStr = Buffer.from(b64, 'base64').toString('utf8');
|
||||
const parsed = JSON.parse(jsonStr);
|
||||
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
const directUrl = (parsed.u || '').trim();
|
||||
const headersMap = { ...defaultHeaders };
|
||||
|
||||
if (parsed.h && typeof parsed.h === 'object') {
|
||||
for (const [key, value] of Object.entries(parsed.h)) {
|
||||
headersMap[key] = String(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (directUrl) {
|
||||
return { url: directUrl, headers: headersMap };
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
|
||||
return { url: rawUrl, headers: defaultHeaders };
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
const isTv = Boolean(s && e);
|
||||
const mediaType = isTv ? 'tv' : 'movie';
|
||||
const ua = getUA();
|
||||
|
||||
const baseParams = new URLSearchParams({
|
||||
type: mediaType,
|
||||
tmdbId: String(id),
|
||||
});
|
||||
|
||||
if (isTv) {
|
||||
baseParams.set('season', String(s));
|
||||
baseParams.set('episode', String(e));
|
||||
}
|
||||
|
||||
const providers = await getProviders(ua);
|
||||
const seenUrls = new Set();
|
||||
const allUrls = [];
|
||||
|
||||
const defaultHeaders = {
|
||||
'User-Agent': ua,
|
||||
Referer: REFERER,
|
||||
Origin: ORIGIN,
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
};
|
||||
|
||||
const tasks = providers.map(async (prov) => {
|
||||
try {
|
||||
const url = `${API_BASE}/api/player/sources?${baseParams.toString()}&provider=${encodeURIComponent(prov.id)}`;
|
||||
const res = await fetch(url, {
|
||||
headers: defaultHeaders,
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
if (!data?.ok || !Array.isArray(data.sources)) return;
|
||||
|
||||
for (const item of data.sources) {
|
||||
if (!item || typeof item !== 'object') continue;
|
||||
|
||||
const providerName = item.providerName || item.publicLabel || prov.name;
|
||||
const primaryRawUrl = (item.url || '').trim();
|
||||
const itemType = (item.type || 'hls').toLowerCase();
|
||||
const itemLabel = item.label || '';
|
||||
|
||||
if (Array.isArray(item.qualities) && item.qualities.length > 0) {
|
||||
for (const q of item.qualities) {
|
||||
if (!q || typeof q !== 'object') continue;
|
||||
const qRawUrl = (q.url || '').trim();
|
||||
if (!qRawUrl) continue;
|
||||
|
||||
const unwrapped = unwrapUrl(qRawUrl, ua);
|
||||
if (!unwrapped.url || seenUrls.has(unwrapped.url)) continue;
|
||||
seenUrls.add(unwrapped.url);
|
||||
|
||||
const qQuality = q.quality || 'Auto';
|
||||
allUrls.push({
|
||||
url: unwrapped.url,
|
||||
label: `${providerName} · ${qQuality}`,
|
||||
headers: unwrapped.headers,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(item.candidates) && item.candidates.length > 0) {
|
||||
let candIndex = 1;
|
||||
for (const c of item.candidates) {
|
||||
if (!c || typeof c !== 'object') continue;
|
||||
const cRawUrl = (c.url || '').trim();
|
||||
if (!cRawUrl) continue;
|
||||
|
||||
const unwrapped = unwrapUrl(cRawUrl, ua);
|
||||
if (!unwrapped.url || seenUrls.has(unwrapped.url)) continue;
|
||||
seenUrls.add(unwrapped.url);
|
||||
|
||||
const cQuality = c.quality || '1080p';
|
||||
allUrls.push({
|
||||
url: unwrapped.url,
|
||||
label: `${providerName} · Mirror ${candIndex} · ${cQuality}`,
|
||||
headers: unwrapped.headers,
|
||||
});
|
||||
candIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(item.audioTracks) && item.audioTracks.length > 0) {
|
||||
for (const a of item.audioTracks) {
|
||||
if (!a || typeof a !== 'object') continue;
|
||||
const aRawUrl = (a.switchUrl || a.url || '').trim();
|
||||
if (!aRawUrl) continue;
|
||||
|
||||
const unwrapped = unwrapUrl(aRawUrl, ua);
|
||||
if (!unwrapped.url || seenUrls.has(unwrapped.url)) continue;
|
||||
seenUrls.add(unwrapped.url);
|
||||
|
||||
const rawLabel = (a.label || a.name || a.language || 'Audio').trim();
|
||||
const cleanLabel = rawLabel.replace(/\s*audio\s*$/i, '').trim() || 'Audio';
|
||||
|
||||
allUrls.push({
|
||||
url: unwrapped.url,
|
||||
label: `${providerName} · ${cleanLabel} Audio`,
|
||||
headers: unwrapped.headers,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (primaryRawUrl) {
|
||||
const unwrapped = unwrapUrl(primaryRawUrl, ua);
|
||||
if (unwrapped.url && !seenUrls.has(unwrapped.url)) {
|
||||
seenUrls.add(unwrapped.url);
|
||||
|
||||
let displayQuality = item.quality || (itemType === 'mp4' ? 'MP4' : 'HD');
|
||||
let cleanLabel = itemLabel || providerName;
|
||||
let label = cleanLabel.startsWith('[') ? cleanLabel : `${providerName} · ${displayQuality}`;
|
||||
|
||||
allUrls.push({
|
||||
url: unwrapped.url,
|
||||
label,
|
||||
headers: unwrapped.headers,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
});
|
||||
|
||||
await Promise.all(tasks);
|
||||
|
||||
if (allUrls.length === 0) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
allUrls,
|
||||
headers: allUrls[0].headers,
|
||||
};
|
||||
}
|
||||
129
src/sources/xdownloader.js
Normal file
129
src/sources/xdownloader.js
Normal file
@@ -0,0 +1,129 @@
|
||||
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,40 +0,0 @@
|
||||
import { fetchJson, fetchText, USER_AGENT } from '../utils/helpers.js';
|
||||
|
||||
const BASE = "https://play.xpass.top";
|
||||
const HEADERS = { "accept": "*/*", "accept-language": "en-US,en;q=0.9", "cache-control": "no-cache", "pragma": "no-cache", "User-Agent": USER_AGENT, "Origin": BASE, "Referrer": `${BASE}/`, "Cookie": "auth_token=de21073d24bca9b50f189b402ac870734cf945f2085cb7e1a4fc453fcfe4f57e" };
|
||||
|
||||
export async function getStream({ id, s, e, server }) {
|
||||
const isTv = s && e && s !== 'null' && e !== 'null';
|
||||
const hdrs = { ...HEADERS, referer: `${BASE}/e/${isTv ? 'tv' : 'movie'}/${id}?autostart=true` };
|
||||
try {
|
||||
let sources = [];
|
||||
if (!isTv) {
|
||||
const text = await fetchText(`${BASE}/e/movie/${id}?autostart=true`, { headers: hdrs, signal: AbortSignal.timeout(7000) });
|
||||
const match = text.match(/var backups=(\[[\s\S]*?\])/);
|
||||
if (match) sources = JSON.parse(match[1]);
|
||||
} else {
|
||||
sources = await fetchJson(`${BASE}/data/tv/${id}/${s}/${e}?autostart=true&force=true`, { headers: hdrs, signal: AbortSignal.timeout(7000) });
|
||||
}
|
||||
if (!sources?.length) return null;
|
||||
if (server && server !== 'all') {
|
||||
const exact = sources.find(src => src.name === server.replace('XPass - ', ''));
|
||||
if (exact) sources = [exact]; else return null;
|
||||
}
|
||||
const allUrls = [];
|
||||
for (const source of sources) {
|
||||
if (!source.url) continue;
|
||||
try {
|
||||
const mdata = await fetchJson(`${BASE}${source.url}`, { headers: hdrs, signal: AbortSignal.timeout(5000) });
|
||||
if (!mdata?.playlist?.[0]?.sources) continue;
|
||||
const targetStream = mdata.playlist[0].sources.find(st => st.type === 'hls') || mdata.playlist[0].sources[0];
|
||||
if (targetStream?.file) allUrls.push({ server: `XPass - ${source.name}`, type: targetStream.type === 'hls' ? 'hls' : 'mp4', url: targetStream.file, headers: { "Origin": HEADERS.Origin, "Referer": HEADERS.Referrer, "User-Agent": HEADERS["User-Agent"] } });
|
||||
} catch { }
|
||||
}
|
||||
return allUrls.length ? { allUrls } : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
export async function getSources(args) {
|
||||
const stream = await getStream(args);
|
||||
return stream?.allUrls ? stream.allUrls.map(u => u.server) : [];
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import crypto from 'crypto';
|
||||
import { getTmdbInfo, getUA } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = 'https://player.zxcstream.xyz';
|
||||
const AES_KEY = '7f4c9e2a81d63b05c4f7a9e8126d3b50e1a8c7f23d9465ab0c6e9f1d4a7b832c';
|
||||
const SERVERS = ['berkas', 'orion', 'aquarius', 'resshin'];
|
||||
|
||||
function decryptCryptoJS(ciphertextB64, passphrase) {
|
||||
try {
|
||||
const buf = Buffer.from(ciphertextB64, 'base64');
|
||||
if (buf.slice(0, 8).toString('utf8') !== 'Salted__') {
|
||||
const key = crypto.createHash('md5').update(passphrase).digest();
|
||||
const iv = Buffer.alloc(16, 0);
|
||||
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv);
|
||||
return Buffer.concat([decipher.update(buf), decipher.final()]).toString('utf8');
|
||||
}
|
||||
|
||||
const salt = buf.slice(8, 16);
|
||||
const cipherBytes = buf.slice(16);
|
||||
|
||||
let hash = Buffer.alloc(0);
|
||||
let keyAndIv = Buffer.alloc(0);
|
||||
while (keyAndIv.length < 48) {
|
||||
hash = crypto.createHash('md5').update(Buffer.concat([hash, Buffer.from(passphrase), salt])).digest();
|
||||
keyAndIv = Buffer.concat([keyAndIv, hash]);
|
||||
}
|
||||
|
||||
const key = keyAndIv.slice(0, 32);
|
||||
const iv = keyAndIv.slice(32, 48);
|
||||
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
||||
return Buffer.concat([decipher.update(cipherBytes), decipher.final()]).toString('utf8');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStream({ id, s, e }) {
|
||||
try {
|
||||
const isTv = Boolean(s && e);
|
||||
const mediaType = isTv ? 'tv' : 'movie';
|
||||
|
||||
let title = 'Media';
|
||||
let year = '2024';
|
||||
|
||||
try {
|
||||
const info = await getTmdbInfo(id, mediaType, s);
|
||||
if (info?.title) title = info.title;
|
||||
if (info?.year) year = String(info.year);
|
||||
} catch { }
|
||||
|
||||
const headers = {
|
||||
'User-Agent': getUA(),
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
};
|
||||
|
||||
const serverResults = await Promise.all(
|
||||
SERVERS.map(async (server) => {
|
||||
try {
|
||||
const tokenRes = await fetch(`${BASE_URL}/backend/you-are-gay`, {
|
||||
method: 'POST',
|
||||
headers: { ...headers, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
id,
|
||||
media_type: mediaType,
|
||||
path: server,
|
||||
...(isTv ? { season: Number(s || 1), episode: Number(e || 1) } : {}),
|
||||
}),
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
|
||||
if (!tokenRes.ok) return [];
|
||||
const tokenData = await tokenRes.json();
|
||||
if (!tokenData?.token) return [];
|
||||
|
||||
const params = new URLSearchParams({
|
||||
id,
|
||||
b: mediaType,
|
||||
ts: String(tokenData.ts),
|
||||
token: tokenData.token,
|
||||
title,
|
||||
year,
|
||||
date: year,
|
||||
});
|
||||
|
||||
if (isTv) {
|
||||
params.append('season', String(s || 1));
|
||||
params.append('episode', String(e || 1));
|
||||
}
|
||||
|
||||
const sourcesRes = await fetch(`${BASE_URL}/backend_/sources/${server}?${params.toString()}`, {
|
||||
headers,
|
||||
signal: AbortSignal.timeout(8000),
|
||||
});
|
||||
|
||||
if (!sourcesRes.ok) return [];
|
||||
const sourcesData = await sourcesRes.json();
|
||||
|
||||
const links = [];
|
||||
if (Array.isArray(sourcesData?.links)) {
|
||||
for (const item of sourcesData.links) {
|
||||
if (!item.link) continue;
|
||||
const decryptedUrl = decryptCryptoJS(item.link, AES_KEY);
|
||||
if (decryptedUrl && decryptedUrl.startsWith('http')) {
|
||||
links.push({
|
||||
url: decryptedUrl,
|
||||
type: decryptedUrl.includes('.m3u8') ? 'hls' : decryptedUrl.includes('.mpd') ? 'dash' : 'mp4',
|
||||
quality: item.resolution || 'Auto',
|
||||
server: `ZxcStream-${server.toUpperCase()}`,
|
||||
headers: {
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
},
|
||||
skipProxy: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return links;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const allUrls = serverResults.flat();
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return { allUrls };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSources(args) {
|
||||
const stream = await getStream(args);
|
||||
return stream?.allUrls ? [...new Set(stream.allUrls.map(u => u.server))] : [];
|
||||
}
|
||||
@@ -3,36 +3,24 @@ import { getUA } from './utils/helpers.js';
|
||||
const SUBTITLE_BASES = [
|
||||
'https://sub.vdrk.site/v1',
|
||||
'https://sub.vdrk.site/v2',
|
||||
'https://fed-subs.pstream.mov'
|
||||
];
|
||||
const VYLIAN_MESSAGES = [
|
||||
"Thanks for using Vyla!"
|
||||
'https://opensubtitles-v3.strem.io'
|
||||
];
|
||||
|
||||
function generateVylianVtt() {
|
||||
let vttContent = 'WEBVTT\n\n';
|
||||
|
||||
let currentTime = 1;
|
||||
while (currentTime <= 7200) {
|
||||
const duration = Math.floor(Math.random() * 4) + 1;
|
||||
const message = VYLIAN_MESSAGES[Math.floor(Math.random() * VYLIAN_MESSAGES.length)];
|
||||
|
||||
const startTime = formatVttTime(currentTime);
|
||||
const endTime = formatVttTime(currentTime + duration);
|
||||
|
||||
vttContent += `${startTime} --> ${endTime}\n${message}\n\n`;
|
||||
|
||||
currentTime += duration + Math.floor(Math.random() * 60) + 30;
|
||||
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}`;
|
||||
}
|
||||
|
||||
return vttContent;
|
||||
}
|
||||
|
||||
function formatVttTime(seconds) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}.000`;
|
||||
}
|
||||
|
||||
export async function fetchSubtitles(paths = []) {
|
||||
@@ -42,7 +30,7 @@ export async function fetchSubtitles(paths = []) {
|
||||
try {
|
||||
const res = await fetch(`${base}${path}`, {
|
||||
headers: { 'User-Agent': getUA() },
|
||||
signal: AbortSignal.timeout(5000),
|
||||
signal: AbortSignal.timeout(6000),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -63,6 +51,20 @@ export async function fetchSubtitles(paths = []) {
|
||||
: [];
|
||||
}
|
||||
|
||||
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 [];
|
||||
|
||||
@@ -93,34 +95,27 @@ export async function fetchSubtitles(paths = []) {
|
||||
})
|
||||
);
|
||||
|
||||
const val = results.flat();
|
||||
|
||||
val.push({
|
||||
label: 'Vylian',
|
||||
file: 'data:text/vtt;base64,' + Buffer.from(generateVylianVtt()).toString('base64'),
|
||||
type: 'vtt',
|
||||
source: 'easter-egg'
|
||||
});
|
||||
|
||||
return val;
|
||||
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: `/movie/tt${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: `/tv/tt${id}/${season}/${episode}` }
|
||||
{ base: SUBTITLE_BASES[2], path: `/subtitles/series/${imdbId}:${season}:${episode}.json` }
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import dotenv from "dotenv";
|
||||
const CONFIG = {
|
||||
DEBUG: true, // Set to true to print outgoing requests, headers, formats, and responses
|
||||
|
||||
MOVIE_ID: "1084242", // Movie TMDB ID to test
|
||||
MOVIE_ID: "936075", // Movie TMDB ID to test
|
||||
TV_ID: "37854", // TV TMDB ID to test, for anime I recommend using a random ID like 37854
|
||||
TV_SEASON: "1", // TV Season to test
|
||||
TV_EPISODE: "1", // TV Episode to test
|
||||
|
||||
Reference in New Issue
Block a user