mirror of
https://gitlab.com/vyla-entertainment/sdk.git
synced 2026-11-12 21:27:28 -05:00
added 4movies, aether, castle, framextv, meowtv, and re-added vidora
This commit is contained in:
107
src/config.js
107
src/config.js
@@ -18,6 +18,25 @@ export const SOURCES = [
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: '4movies',
|
||||
label: '4Movies',
|
||||
sourceFile: '4movies',
|
||||
proxyParam: '4m',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /4movie\.in|aapanel\.devcorp\.me/i,
|
||||
headers: {
|
||||
Referer: 'https://4movie.in/',
|
||||
Origin: 'https://4movie.in',
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: '123anime',
|
||||
label: '123Anime',
|
||||
@@ -56,6 +75,37 @@ export const SOURCES = [
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'aether',
|
||||
label: 'Aether',
|
||||
sourceFile: 'aether',
|
||||
proxyParam: 'aet',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /raccoonunbutteredbagel\.zip|1x2\.space|kaxate\.fun|mechanicalserenity\.xyz|reelvault\.click|visionaryfounderslab\.site|goldenkite\.top|aether\.ist/i,
|
||||
headers: {
|
||||
Referer: 'https://aether.st/',
|
||||
Origin: 'https://aether.st',
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'castle',
|
||||
label: 'Castle',
|
||||
sourceFile: 'castle',
|
||||
proxyParam: 'cst',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'cinemaarmy',
|
||||
label: 'CinemaArmy',
|
||||
@@ -151,6 +201,25 @@ export const SOURCES = [
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'framextv',
|
||||
label: 'FrameXTV',
|
||||
sourceFile: 'framextv',
|
||||
proxyParam: 'fxt',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /framextv\.cc|streamos\.top|workers\.dev|films365\.org/i,
|
||||
headers: {
|
||||
Referer: 'https://framextv.cc/',
|
||||
Origin: 'https://framextv.cc',
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'frost',
|
||||
label: 'FrostStream',
|
||||
@@ -278,6 +347,25 @@ export const SOURCES = [
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
key: 'meowtv',
|
||||
label: 'MeowTV',
|
||||
sourceFile: 'meowtv',
|
||||
proxyParam: 'mtv',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /meowtv\.ru/i,
|
||||
headers: {
|
||||
Referer: 'https://meowtv.ru/',
|
||||
Origin: 'https://meowtv.ru',
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'movenerds',
|
||||
label: 'MoverNerds',
|
||||
@@ -481,6 +569,25 @@ export const SOURCES = [
|
||||
multiUrl: true,
|
||||
},
|
||||
|
||||
{
|
||||
key: 'vidora',
|
||||
label: 'Vidora',
|
||||
sourceFile: 'vidora',
|
||||
proxyParam: 'vd',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /vidoraplayer\.duckdns\.org/i,
|
||||
headers: {
|
||||
Referer: 'https://vidoraplayer.duckdns.org/',
|
||||
Origin: 'https://vidoraplayer.duckdns.org',
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'vidup',
|
||||
label: 'VidUp',
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { HEALTH_PROBE_ID } from "./config.js";
|
||||
|
||||
const withTimeout = (promise, ms) => Promise.race([promise, new Promise(r => setTimeout(() => r(null), ms))]);
|
||||
const MOVIE_PROBE_ID = "936075";
|
||||
const TV_PROBE_ID = "37854";
|
||||
const TV_PROBE_SEASON = "1";
|
||||
const TV_PROBE_EPISODE = "1";
|
||||
|
||||
const withTimeout = (promise, ms) =>
|
||||
Promise.race([
|
||||
promise,
|
||||
new Promise(resolve => setTimeout(() => resolve(null), ms))
|
||||
]);
|
||||
|
||||
async function withRetry(fn, attempts = 2, delay = 300) {
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
try {
|
||||
@@ -8,31 +17,62 @@ async function withRetry(fn, attempts = 2, delay = 300) {
|
||||
if (result != null) return result;
|
||||
} catch (err) {
|
||||
if (i === attempts - 1) throw err;
|
||||
await new Promise(r => setTimeout(r, delay + Math.random() * delay * 0.5));
|
||||
await new Promise(resolve =>
|
||||
setTimeout(resolve, delay + Math.random() * delay * 0.5)
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function hasStream(result) {
|
||||
const urls = [
|
||||
result?.url,
|
||||
...(result?.allUrls || []).map(item => item?.url)
|
||||
];
|
||||
|
||||
return urls.some(url =>
|
||||
typeof url === "string" && url.startsWith("http")
|
||||
);
|
||||
}
|
||||
|
||||
export async function probeSource(sdk, cfg, mod) {
|
||||
const audio = /dub$/.test(cfg.key) ? 'dub' : 'sub';
|
||||
const streamArgs = {
|
||||
id: HEALTH_PROBE_ID,
|
||||
s: null,
|
||||
e: null,
|
||||
clientIP: null,
|
||||
absoluteBase: null,
|
||||
audio,
|
||||
sdk,
|
||||
config: cfg,
|
||||
const audio = /dub$/.test(cfg.key) ? "dub" : "sub";
|
||||
const start = Date.now();
|
||||
|
||||
const test = async (id, s = null, e = null) => {
|
||||
const streamArgs = {
|
||||
id,
|
||||
s,
|
||||
e,
|
||||
clientIP: null,
|
||||
absoluteBase: null,
|
||||
audio,
|
||||
sdk,
|
||||
config: cfg,
|
||||
};
|
||||
|
||||
const result = await withTimeout(
|
||||
withRetry(
|
||||
() => mod.getStream(streamArgs),
|
||||
cfg.retries,
|
||||
300
|
||||
),
|
||||
cfg.timeout
|
||||
);
|
||||
|
||||
return hasStream(result);
|
||||
};
|
||||
|
||||
const t = Date.now();
|
||||
const result = await withTimeout(
|
||||
withRetry(() => mod.getStream(streamArgs), cfg.retries, 300),
|
||||
cfg.timeout
|
||||
);
|
||||
const [movie, tv] = await Promise.all([
|
||||
test(MOVIE_PROBE_ID),
|
||||
test(TV_PROBE_ID, TV_PROBE_SEASON, TV_PROBE_EPISODE)
|
||||
]);
|
||||
|
||||
const firstUrl = result?.url ?? result?.allUrls?.[0]?.url;
|
||||
return { ok: !!(typeof firstUrl === 'string' && firstUrl.startsWith('http')), ms: Date.now() - t };
|
||||
return {
|
||||
ok: movie || tv,
|
||||
movie,
|
||||
tv,
|
||||
ms: Date.now() - start
|
||||
};
|
||||
}
|
||||
77
src/sources/4movies.js
Normal file
77
src/sources/4movies.js
Normal file
@@ -0,0 +1,77 @@
|
||||
const BASE_URL = 'https://4movie.in';
|
||||
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';
|
||||
|
||||
const HEADERS = {
|
||||
'User-Agent': UA,
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
};
|
||||
|
||||
const PLAYBACK_HEADERS = {
|
||||
'User-Agent': UA,
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
};
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
|
||||
let query = isTv
|
||||
? `tmdb=${id}&type=tv&season=${s}&episode=${e}`
|
||||
: `tmdb=${id}&type=movie`;
|
||||
|
||||
if (args?.title) query += `&title=${encodeURIComponent(args.title)}`;
|
||||
if (args?.year) query += `&year=${encodeURIComponent(args.year)}`;
|
||||
|
||||
const targetUrl = `${BASE_URL}/api/hubcloud?${query}`;
|
||||
|
||||
const res = await fetch(targetUrl, { headers: HEADERS }).catch(() => null);
|
||||
if (!res || !res.ok) return null;
|
||||
|
||||
const data = await res.json().catch(() => null);
|
||||
if (!data?.ok || !Array.isArray(data.streams) || !data.streams.length) return null;
|
||||
|
||||
const allUrls = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const stream of data.streams) {
|
||||
if (stream.rawUrl && !seen.has(stream.rawUrl)) {
|
||||
seen.add(stream.rawUrl);
|
||||
allUrls.push({
|
||||
url: stream.rawUrl,
|
||||
server: `4Movies - ${stream.name || 'Direct'}`,
|
||||
quality: stream.quality || 'Auto',
|
||||
type: stream.type || (stream.rawUrl.includes('.m3u8') ? 'hls' : 'mp4'),
|
||||
headers: PLAYBACK_HEADERS,
|
||||
});
|
||||
}
|
||||
|
||||
if (stream.url) {
|
||||
const proxyUrl = stream.url.startsWith('http')
|
||||
? stream.url
|
||||
: `${BASE_URL}${stream.url.startsWith('/') ? '' : '/'}${stream.url}`;
|
||||
|
||||
if (!seen.has(proxyUrl)) {
|
||||
seen.add(proxyUrl);
|
||||
allUrls.push({
|
||||
url: proxyUrl,
|
||||
server: `4Movies - ${stream.name ? `${stream.name} (Proxy)` : 'Proxy'}`,
|
||||
quality: stream.quality || 'Auto',
|
||||
type: stream.type || 'hls',
|
||||
headers: PLAYBACK_HEADERS,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
99
src/sources/aether.js
Normal file
99
src/sources/aether.js
Normal file
@@ -0,0 +1,99 @@
|
||||
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';
|
||||
|
||||
const HEADERS = {
|
||||
'User-Agent': UA,
|
||||
Origin: 'https://aether.st',
|
||||
Referer: 'https://aether.st/',
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
};
|
||||
|
||||
const PLAYBACK_HEADERS = {
|
||||
'User-Agent': UA,
|
||||
Referer: 'https://aether.st/',
|
||||
Origin: 'https://aether.st',
|
||||
};
|
||||
|
||||
function extractStreamUrls(data, target) {
|
||||
const urls = [];
|
||||
if (!data) return urls;
|
||||
|
||||
if (Array.isArray(data.streams)) {
|
||||
for (const s of data.streams) {
|
||||
if (s?.url) {
|
||||
urls.push({
|
||||
url: s.url,
|
||||
name: `${target.name} (${s.name || s.language || 'Default'})`,
|
||||
quality: s.quality || 'Auto',
|
||||
type: s.type || (s.url.includes('.m3u8') ? 'hls' : 'mp4'),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const singleUrl = data.directUrl || data.url || data.stream;
|
||||
if (typeof singleUrl === 'string' && /^https?:\/\//.test(singleUrl)) {
|
||||
urls.push({
|
||||
url: singleUrl,
|
||||
name: `${target.name}${data.server ? ` - ${data.server}` : ''}`,
|
||||
quality: data.quality || 'Auto',
|
||||
type: data.streamType || data.container || (singleUrl.includes('.m3u8') ? 'hls' : 'mp4'),
|
||||
});
|
||||
}
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
|
||||
const endpoints = [
|
||||
{ name: 'Meridian', url: isTv ? `https://meridian.raccoonunbutteredbagel.zip/show/${id}/${s}/${e}` : `https://meridian.raccoonunbutteredbagel.zip/movie/${id}` },
|
||||
{ name: 'Rock', url: isTv ? `https://rock.raccoonunbutteredbagel.zip/tv/${id}/${s}/${e}` : `https://rock.raccoonunbutteredbagel.zip/movie/${id}` },
|
||||
{ name: 'Shore', url: isTv ? `https://shore.raccoonunbutteredbagel.zip/show/${id}?s=${s}&e=${e}` : `https://shore.raccoonunbutteredbagel.zip/movie/${id}` },
|
||||
{ name: 'Filler', url: isTv ? `https://filler.raccoonunbutteredbagel.zip/tv/${id}/${s}/${e}` : `https://filler.raccoonunbutteredbagel.zip/movie/${id}` },
|
||||
{ name: 'Tiki', url: isTv ? `https://tiki.raccoonunbutteredbagel.zip/tv/${id}/${s}/${e}` : `https://tiki.raccoonunbutteredbagel.zip/movie/${id}` },
|
||||
{ name: 'Link', url: isTv ? `https://link.raccoonunbutteredbagel.zip/tv/${id}/${s}/${e}` : `https://link.raccoonunbutteredbagel.zip/movie/${id}` },
|
||||
{ name: 'VFast Rapid', url: isTv ? `https://vfast.raccoonunbutteredbagel.zip/tv/${id}/${s}/${e}?server=vRapid` : `https://vfast.raccoonunbutteredbagel.zip/movie/${id}?server=vRapid` },
|
||||
{ name: 'VFast Blaze', url: isTv ? `https://vfast.raccoonunbutteredbagel.zip/tv/${id}/${s}/${e}?server=vBlaze` : `https://vfast.raccoonunbutteredbagel.zip/movie/${id}?server=vBlaze` },
|
||||
];
|
||||
|
||||
const responses = await Promise.allSettled(
|
||||
endpoints.map(async target => {
|
||||
const res = await fetch(target.url, { headers: HEADERS });
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
return { target, data };
|
||||
})
|
||||
);
|
||||
|
||||
const allUrls = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const res of responses) {
|
||||
if (res.status !== 'fulfilled' || !res.value?.data) continue;
|
||||
const { target, data } = res.value;
|
||||
const found = extractStreamUrls(data, target);
|
||||
|
||||
for (const item of found) {
|
||||
if (!item?.url || seen.has(item.url)) continue;
|
||||
seen.add(item.url);
|
||||
allUrls.push({
|
||||
url: item.url,
|
||||
server: `Aether - ${item.name}`,
|
||||
quality: item.quality || 'Auto',
|
||||
type: item.type || (item.url.includes('.m3u8') ? 'hls' : 'mp4'),
|
||||
headers: PLAYBACK_HEADERS,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
415
src/sources/castle.js
Normal file
415
src/sources/castle.js
Normal file
@@ -0,0 +1,415 @@
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
const TMDB_BASE_URL = 'https://api.themoviedb.org/3';
|
||||
const CASTLE_BASE = 'https://api.hlowb.com';
|
||||
const PKG = 'com.external.castle';
|
||||
const CHANNEL = 'IndiaA';
|
||||
const CLIENT = '1';
|
||||
const LANG = 'en-US';
|
||||
const SUFFIX = 'T!BgJB';
|
||||
const APK_SIGN_KEY = 'ED0955EB04E67A1D9F3305B95454FED485261475';
|
||||
|
||||
const WORKING_HEADERS = {
|
||||
'User-Agent': 'okhttp/4.9.3',
|
||||
Accept: 'application/json',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
Connection: 'Keep-Alive',
|
||||
Referer: CASTLE_BASE,
|
||||
};
|
||||
|
||||
export const CASTLE_PLAYBACK_HEADERS = {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
|
||||
Accept:
|
||||
'video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'Accept-Encoding': 'identity',
|
||||
Connection: 'keep-alive',
|
||||
'Sec-Fetch-Dest': 'video',
|
||||
'Sec-Fetch-Mode': 'no-cors',
|
||||
'Sec-Fetch-Site': 'cross-site',
|
||||
DNT: '1',
|
||||
};
|
||||
|
||||
const QUALITY_MAP = { 1: '480p', 2: '720p', 3: '1080p' };
|
||||
|
||||
function safeJsonParse(text) {
|
||||
const safe = text.replace(/:(\s*)(-?\d{16,})(\s*[,}\]])/g, ':$1"$2"$3');
|
||||
return JSON.parse(safe);
|
||||
}
|
||||
|
||||
class CastleScraper {
|
||||
constructor(apiKey) {
|
||||
this.tmdbKey =
|
||||
apiKey ||
|
||||
process.env.CASTLE_TMDB_API_KEY ||
|
||||
process.env.TMDB_API_KEY ||
|
||||
'439c478a771f35c05022f9feabcca01c';
|
||||
this.securityKey = null;
|
||||
}
|
||||
|
||||
deriveKey(apiKeyB64) {
|
||||
const apiKeyBytes = Buffer.from(apiKeyB64, 'base64');
|
||||
let keyMaterial = Buffer.concat([apiKeyBytes, Buffer.from(SUFFIX, 'ascii')]);
|
||||
if (keyMaterial.length < 16) {
|
||||
keyMaterial = Buffer.concat([
|
||||
keyMaterial,
|
||||
Buffer.alloc(16 - keyMaterial.length),
|
||||
]);
|
||||
} else if (keyMaterial.length > 16) {
|
||||
keyMaterial = keyMaterial.subarray(0, 16);
|
||||
}
|
||||
return keyMaterial;
|
||||
}
|
||||
|
||||
decryptCastle(encryptedB64, securityKeyB64) {
|
||||
const aesKey = this.deriveKey(securityKeyB64);
|
||||
const iv = aesKey;
|
||||
const encrypted = Buffer.from(encryptedB64, 'base64');
|
||||
const decipher = crypto.createDecipheriv('aes-128-cbc', aesKey, iv);
|
||||
decipher.setAutoPadding(true);
|
||||
return Buffer.concat([
|
||||
decipher.update(encrypted),
|
||||
decipher.final(),
|
||||
]).toString('utf-8');
|
||||
}
|
||||
|
||||
async request(url, init) {
|
||||
const r = await fetch(url, {
|
||||
...init,
|
||||
headers: { ...WORKING_HEADERS, ...(init?.headers || {}) },
|
||||
});
|
||||
if (!r.ok) throw new Error(`Castle request failed ${r.status} for ${url}`);
|
||||
return r;
|
||||
}
|
||||
|
||||
async extractCipher(response) {
|
||||
const text = (await response.text()).trim();
|
||||
if (!text) throw new Error('Empty response');
|
||||
try {
|
||||
const j = JSON.parse(text);
|
||||
if (j && typeof j.data === 'string') return j.data.trim();
|
||||
} catch { }
|
||||
return text;
|
||||
}
|
||||
|
||||
async decryptedJson(response, securityKey) {
|
||||
const cipher = await this.extractCipher(response);
|
||||
return safeJsonParse(this.decryptCastle(cipher, securityKey));
|
||||
}
|
||||
|
||||
async getSecurityKey() {
|
||||
if (this.securityKey) return this.securityKey;
|
||||
const url = `${CASTLE_BASE}/v0.1/system/getSecurityKey/1?channel=${CHANNEL}&clientType=${CLIENT}&lang=${LANG}`;
|
||||
const r = await this.request(url);
|
||||
const data = await r.json();
|
||||
if (data.code !== 200 || !data.data) {
|
||||
throw new Error(`Security key API error: ${JSON.stringify(data)}`);
|
||||
}
|
||||
this.securityKey = data.data;
|
||||
return this.securityKey;
|
||||
}
|
||||
|
||||
async searchCastle(securityKey, keyword, page = 1, size = 30) {
|
||||
const params = new URLSearchParams({
|
||||
channel: CHANNEL,
|
||||
clientType: CLIENT,
|
||||
keyword,
|
||||
lang: LANG,
|
||||
mode: '1',
|
||||
packageName: PKG,
|
||||
page: String(page),
|
||||
size: String(size),
|
||||
});
|
||||
const url = `${CASTLE_BASE}/film-api/v1.1.0/movie/searchByKeyword?${params}`;
|
||||
return this.decryptedJson(await this.request(url), securityKey);
|
||||
}
|
||||
|
||||
async getDetails(securityKey, movieId) {
|
||||
const url = `${CASTLE_BASE}/film-api/v1.1/movie?channel=${CHANNEL}&clientType=${CLIENT}&lang=${LANG}&movieId=${movieId}&packageName=${PKG}`;
|
||||
return this.decryptedJson(await this.request(url), securityKey);
|
||||
}
|
||||
|
||||
async getVideo2(securityKey, movieId, episodeId, resolution = 2) {
|
||||
const url = `${CASTLE_BASE}/film-api/v2.0.1/movie/getVideo2?clientType=${CLIENT}&packageName=${PKG}&channel=${CHANNEL}&lang=${LANG}`;
|
||||
const body = {
|
||||
mode: '1',
|
||||
appMarket: 'GuanWang',
|
||||
clientType: '1',
|
||||
woolUser: 'false',
|
||||
apkSignKey: APK_SIGN_KEY,
|
||||
androidVersion: '13',
|
||||
movieId: String(movieId),
|
||||
episodeId: String(episodeId),
|
||||
isNewUser: 'true',
|
||||
resolution: String(resolution),
|
||||
packageName: PKG,
|
||||
};
|
||||
const r = await this.request(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return this.decryptedJson(r, securityKey);
|
||||
}
|
||||
|
||||
async getVideoV1(securityKey, movieId, episodeId, languageId, resolution = 2) {
|
||||
const params = new URLSearchParams({
|
||||
apkSignKey: APK_SIGN_KEY,
|
||||
channel: CHANNEL,
|
||||
clientType: CLIENT,
|
||||
episodeId: String(episodeId),
|
||||
lang: LANG,
|
||||
languageId: String(languageId),
|
||||
mode: '1',
|
||||
movieId: String(movieId),
|
||||
packageName: PKG,
|
||||
resolution: String(resolution),
|
||||
});
|
||||
const url = `${CASTLE_BASE}/film-api/v1.9.1/movie/getVideo?${params}`;
|
||||
return this.decryptedJson(await this.request(url), securityKey);
|
||||
}
|
||||
|
||||
extractDataBlock(obj) {
|
||||
if (obj && typeof obj.data === 'object' && obj.data) return obj.data;
|
||||
return obj && typeof obj === 'object' ? obj : {};
|
||||
}
|
||||
|
||||
qualityValue(quality) {
|
||||
if (!quality) return 0;
|
||||
let q = String(quality).toLowerCase();
|
||||
q = q.replace(/^(sd|hd|fhd|uhd|4k)\s*/, '').replace(/p$/, '').trim();
|
||||
if (q === '4k' || q === '2160') return 2160;
|
||||
const known = [1440, 1080, 720, 480, 360, 240];
|
||||
const n = parseInt(q, 10);
|
||||
if (known.includes(n)) return n;
|
||||
return n > 0 ? n : 0;
|
||||
}
|
||||
|
||||
formatSize(size) {
|
||||
if (typeof size === 'number' && size > 0) {
|
||||
return size > 1_000_000_000
|
||||
? `${(size / 1_000_000_000).toFixed(2)} GB`
|
||||
: `${Math.round(size / 1_000_000)} MB`;
|
||||
}
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
async getTmdbDetails(tmdbId, mediaType) {
|
||||
const endpoint = mediaType === 'tv' ? 'tv' : 'movie';
|
||||
const url = `${TMDB_BASE_URL}/${endpoint}/${tmdbId}?api_key=${this.tmdbKey}&append_to_response=external_ids`;
|
||||
const r = await this.request(url);
|
||||
const data = await r.json();
|
||||
const title = mediaType === 'tv' ? data.name : data.title;
|
||||
const releaseDate =
|
||||
mediaType === 'tv' ? data.first_air_date : data.release_date;
|
||||
const year = releaseDate ? parseInt(String(releaseDate).split('-')[0], 10) : null;
|
||||
return { title, year, tmdbId };
|
||||
}
|
||||
|
||||
findMovieId(searchResult, tmdbInfo) {
|
||||
const data = this.extractDataBlock(searchResult);
|
||||
const rows = data.rows || [];
|
||||
if (!rows.length) throw new Error('No search results found');
|
||||
const searchTitle = (tmdbInfo.title || '').toLowerCase();
|
||||
|
||||
for (const item of rows) {
|
||||
const itemTitle = String(item.title || item.name || '').toLowerCase();
|
||||
if (
|
||||
itemTitle &&
|
||||
(itemTitle.includes(searchTitle) || searchTitle.includes(itemTitle))
|
||||
) {
|
||||
const id = item.id || item.redirectId || item.redirectIdStr;
|
||||
if (id) return String(id);
|
||||
}
|
||||
}
|
||||
const first = rows[0];
|
||||
const id = first.id || first.redirectId || first.redirectIdStr;
|
||||
if (id) return String(id);
|
||||
throw new Error('Could not extract movie ID from search results');
|
||||
}
|
||||
|
||||
processVideoResponse(videoData, media, season, episode, resolution, languageInfo) {
|
||||
const streams = [];
|
||||
const data = this.extractDataBlock(videoData);
|
||||
const videoUrl = data.videoUrl;
|
||||
if (!videoUrl) return streams;
|
||||
|
||||
let mediaTitle = media.title || 'Unknown';
|
||||
if (media.year) mediaTitle += ` (${media.year})`;
|
||||
if (season && episode) {
|
||||
mediaTitle = `${media.title} S${String(season).padStart(2, '0')}E${String(
|
||||
episode
|
||||
).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
const quality = QUALITY_MAP[resolution] || `${resolution}p`;
|
||||
|
||||
if (Array.isArray(data.videos)) {
|
||||
for (const video of data.videos) {
|
||||
let q = String(
|
||||
video.resolutionDescription || video.resolution || quality
|
||||
).replace(/^(SD|HD|FHD)\s+/i, '');
|
||||
const name = languageInfo
|
||||
? `Castle ${languageInfo} - ${q}`
|
||||
: `Castle - ${q}`;
|
||||
streams.push({
|
||||
name,
|
||||
title: mediaTitle,
|
||||
url: video.url || videoUrl,
|
||||
quality: q,
|
||||
size: this.formatSize(video.size),
|
||||
provider: 'castle',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const name = languageInfo
|
||||
? `Castle ${languageInfo} - ${quality}`
|
||||
: `Castle - ${quality}`;
|
||||
streams.push({
|
||||
name,
|
||||
title: mediaTitle,
|
||||
url: videoUrl,
|
||||
quality,
|
||||
size: this.formatSize(data.size),
|
||||
provider: 'castle',
|
||||
});
|
||||
}
|
||||
return streams;
|
||||
}
|
||||
|
||||
async fetchSources(tmdbId, season = 0, episode = 0) {
|
||||
const mediaType = season && episode ? 'tv' : 'movie';
|
||||
try {
|
||||
const tmdbInfo = await this.getTmdbDetails(tmdbId, mediaType);
|
||||
const securityKey = await this.getSecurityKey();
|
||||
|
||||
const searchTerm = tmdbInfo.year
|
||||
? `${tmdbInfo.title} ${tmdbInfo.year}`
|
||||
: tmdbInfo.title;
|
||||
const searchResult = await this.searchCastle(securityKey, searchTerm);
|
||||
let movieId = this.findMovieId(searchResult, tmdbInfo);
|
||||
|
||||
let details = await this.getDetails(securityKey, movieId);
|
||||
|
||||
if (mediaType === 'tv') {
|
||||
const data = this.extractDataBlock(details);
|
||||
const seasons = data.seasons || [];
|
||||
const s = seasons.find((x) => x.number === season);
|
||||
if (s && s.movieId && String(s.movieId) !== movieId) {
|
||||
movieId = String(s.movieId);
|
||||
details = await this.getDetails(securityKey, movieId);
|
||||
}
|
||||
}
|
||||
|
||||
const data = this.extractDataBlock(details);
|
||||
const episodes = data.episodes || [];
|
||||
|
||||
let episodeId = null;
|
||||
if (mediaType === 'tv') {
|
||||
const ep = episodes.find((e) => e.number === episode);
|
||||
if (ep && ep.id) episodeId = String(ep.id);
|
||||
} else if (episodes.length) {
|
||||
episodeId = String(episodes[0].id);
|
||||
}
|
||||
if (!episodeId) throw new Error('Could not find episode ID');
|
||||
|
||||
const epObj = episodes.find((e) => String(e.id) === episodeId);
|
||||
const tracks = epObj?.tracks || [];
|
||||
|
||||
const resolution = 2;
|
||||
let allStreams = [];
|
||||
|
||||
for (let i = 0; i < tracks.length; i++) {
|
||||
const track = tracks[i];
|
||||
const langName =
|
||||
track.languageName || track.abbreviate || `Lang${i + 1}`;
|
||||
if (track.existIndividualVideo && track.languageId) {
|
||||
try {
|
||||
const videoData = await this.getVideoV1(
|
||||
securityKey,
|
||||
movieId,
|
||||
episodeId,
|
||||
String(track.languageId),
|
||||
resolution
|
||||
);
|
||||
const langStreams = this.processVideoResponse(
|
||||
videoData,
|
||||
tmdbInfo,
|
||||
season,
|
||||
episode,
|
||||
resolution,
|
||||
`[${langName}]`
|
||||
);
|
||||
allStreams.push(...langStreams);
|
||||
} catch (e) {
|
||||
console.error(`[Castle] ${langName}: v1 failed - ${e?.message || e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!allStreams.length) {
|
||||
const videoData = await this.getVideo2(
|
||||
securityKey,
|
||||
movieId,
|
||||
episodeId,
|
||||
resolution
|
||||
);
|
||||
allStreams = this.processVideoResponse(
|
||||
videoData,
|
||||
tmdbInfo,
|
||||
season,
|
||||
episode,
|
||||
resolution,
|
||||
'[Shared]'
|
||||
);
|
||||
}
|
||||
|
||||
allStreams.sort(
|
||||
(a, b) => this.qualityValue(b.quality) - this.qualityValue(a.quality)
|
||||
);
|
||||
return allStreams;
|
||||
} catch (e) {
|
||||
console.error(`[Castle] Error: ${e?.message || e}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, sdk } = args || {};
|
||||
const scraper = new CastleScraper(sdk?.tmdbApiKey);
|
||||
const tmdbId = parseInt(String(id), 10);
|
||||
const season = s != null ? parseInt(String(s), 10) : 0;
|
||||
const episode = e != null ? parseInt(String(e), 10) : 0;
|
||||
|
||||
const streams = await scraper.fetchSources(tmdbId, season, episode).catch(() => []);
|
||||
if (!streams || !streams.length) return null;
|
||||
|
||||
const seen = new Set();
|
||||
const allUrls = [];
|
||||
|
||||
for (const item of streams) {
|
||||
if (!item?.url || seen.has(item.url)) continue;
|
||||
seen.add(item.url);
|
||||
allUrls.push({
|
||||
url: item.url,
|
||||
server: item.name || 'Castle',
|
||||
quality: item.quality || 'Auto',
|
||||
type: item.url.includes('.m3u8')
|
||||
? 'hls'
|
||||
: item.url.includes('.mpd')
|
||||
? 'dash'
|
||||
: 'mp4',
|
||||
headers: CASTLE_PLAYBACK_HEADERS,
|
||||
});
|
||||
}
|
||||
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
126
src/sources/framextv.js
Normal file
126
src/sources/framextv.js
Normal file
@@ -0,0 +1,126 @@
|
||||
const FRAMEX_BASE = 'https://api.framextv.cc';
|
||||
const STREAMOS_BASE = 'https://api.streamos.top';
|
||||
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';
|
||||
|
||||
const FRAMEX_HEADERS = {
|
||||
'User-Agent': UA,
|
||||
Referer: 'https://framextv.cc/',
|
||||
Origin: 'https://framextv.cc',
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
};
|
||||
|
||||
const STREAMOS_HEADERS = {
|
||||
'User-Agent': UA,
|
||||
Referer: 'https://streamos.top/',
|
||||
Origin: 'https://streamos.top',
|
||||
};
|
||||
|
||||
const STREAMOS_SERVERS = [
|
||||
'auto',
|
||||
'miami',
|
||||
'boise',
|
||||
'vegas',
|
||||
'reno',
|
||||
'oakland',
|
||||
'seattle',
|
||||
'phoenix',
|
||||
'atlanta',
|
||||
'portland',
|
||||
'austin',
|
||||
'dallas',
|
||||
'tampa',
|
||||
'orlando',
|
||||
'munich',
|
||||
'berlin',
|
||||
'france',
|
||||
'india',
|
||||
'mexico',
|
||||
'delhi',
|
||||
];
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
|
||||
const framexQuery = isTv
|
||||
? `type=tv&id=${id}&season=${s}&episode=${e}&codecs=hevc`
|
||||
: `type=movie&id=${id}&codecs=hevc`;
|
||||
|
||||
const framexUrls = [
|
||||
`${FRAMEX_BASE}/api/stream?${framexQuery}`,
|
||||
`${FRAMEX_BASE}/api/stream?${framexQuery}&provider=sparky`,
|
||||
`${FRAMEX_BASE}/api/stream?${framexQuery}&provider=titan`,
|
||||
];
|
||||
|
||||
const streamOsBaseQuery = isTv
|
||||
? `id=${id}&season=${s}&episode=${e}&kind=tv`
|
||||
: `id=${id}&kind=movie`;
|
||||
|
||||
const allUrls = [];
|
||||
const seen = new Set();
|
||||
|
||||
const framexPromises = Promise.all(
|
||||
framexUrls.map(url =>
|
||||
fetch(url, { headers: FRAMEX_HEADERS })
|
||||
.then(r => (r.ok ? r.json() : null))
|
||||
.catch(() => null)
|
||||
)
|
||||
);
|
||||
|
||||
const streamOsChecks = Promise.all(
|
||||
STREAMOS_SERVERS.slice(0, 8).map(async server => {
|
||||
const serverParam = server === 'auto' ? '' : `&server=${server}`;
|
||||
const url = `${STREAMOS_BASE}/api/relay/hls?${streamOsBaseQuery}${serverParam}`;
|
||||
try {
|
||||
const res = await fetch(url, { method: 'HEAD', headers: STREAMOS_HEADERS });
|
||||
if (res.ok) {
|
||||
return {
|
||||
url,
|
||||
server: `StreamOS - ${server.toUpperCase()}`,
|
||||
quality: '1080p',
|
||||
type: 'hls',
|
||||
headers: STREAMOS_HEADERS,
|
||||
};
|
||||
}
|
||||
} catch { }
|
||||
return null;
|
||||
})
|
||||
);
|
||||
|
||||
const [framexResults, streamOsResults] = await Promise.all([framexPromises, streamOsChecks]);
|
||||
|
||||
for (const data of framexResults) {
|
||||
if (!data?.success || !Array.isArray(data.sources)) continue;
|
||||
for (const src of data.sources) {
|
||||
if (!src?.url || seen.has(src.url)) continue;
|
||||
seen.add(src.url);
|
||||
allUrls.push({
|
||||
url: src.url,
|
||||
server: `FrameX - ${src.server || data.provider || 'Direct'}`,
|
||||
quality: src.quality || 'Auto',
|
||||
type: src.type || (src.url.includes('.m3u8') ? 'hls' : 'mp4'),
|
||||
headers: {
|
||||
'User-Agent': UA,
|
||||
Referer: 'https://framextv.cc/',
|
||||
Origin: 'https://framextv.cc',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const stream of streamOsResults) {
|
||||
if (stream && !seen.has(stream.url)) {
|
||||
seen.add(stream.url);
|
||||
allUrls.push(stream);
|
||||
}
|
||||
}
|
||||
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
140
src/sources/meowtv.js
Normal file
140
src/sources/meowtv.js
Normal file
@@ -0,0 +1,140 @@
|
||||
const BASE_URL = 'https://api.meowtv.ru';
|
||||
const DEC_API = 'https://enc-dec.app/api';
|
||||
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';
|
||||
|
||||
const HEADERS = {
|
||||
'User-Agent': UA,
|
||||
Origin: 'https://meowtv.ru',
|
||||
Referer: 'https://meowtv.ru/',
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
};
|
||||
|
||||
const SERVERS = [
|
||||
{ id: 'acme', label: 'Acme' },
|
||||
{ id: 'dcloud', label: 'Dcloud' },
|
||||
{ id: 'tik', label: 'TCloud' },
|
||||
{ id: 'ipcloud', label: 'IPcloud' },
|
||||
{ id: 'turkce', label: 'Türkçe', movieOnly: true },
|
||||
{ id: 'hindi', label: 'Hindi' },
|
||||
{ id: 'english', label: 'English' },
|
||||
];
|
||||
|
||||
async function decryptMeow(data) {
|
||||
try {
|
||||
const res = await fetch(`${DEC_API}/dec-meowtv`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': UA,
|
||||
},
|
||||
body: JSON.stringify({ data }),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
const json = await res.json();
|
||||
return json?.status === 200 ? json.result : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function extractUrls(result) {
|
||||
const urls = [];
|
||||
if (!result) return urls;
|
||||
if (typeof result === 'string' && /^https?:\/\//.test(result)) {
|
||||
urls.push({ url: result, quality: 'Auto', type: result.includes('.m3u8') ? 'hls' : 'mp4' });
|
||||
} else if (result?.url && typeof result.url === 'string') {
|
||||
urls.push({
|
||||
url: result.url,
|
||||
quality: result.quality || 'Auto',
|
||||
type: result.type || (result.url.includes('.m3u8') ? 'hls' : 'mp4'),
|
||||
});
|
||||
} else if (Array.isArray(result?.sources)) {
|
||||
for (const s of result.sources) {
|
||||
if (s?.url) {
|
||||
urls.push({
|
||||
url: s.url,
|
||||
quality: s.quality || 'Auto',
|
||||
type: s.type || (s.url.includes('.m3u8') ? 'hls' : 'mp4'),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (result?.stream) {
|
||||
const sUrl = typeof result.stream === 'string' ? result.stream : result.stream?.url;
|
||||
if (sUrl) {
|
||||
urls.push({
|
||||
url: sUrl,
|
||||
quality: result.stream?.quality || 'Auto',
|
||||
type: sUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
});
|
||||
}
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
|
||||
const ticketRes = await fetch(`${BASE_URL}/streams/ticket`, {
|
||||
method: 'POST',
|
||||
headers: HEADERS,
|
||||
}).then(r => (r.ok ? r.json() : null)).catch(() => null);
|
||||
|
||||
const ticket = ticketRes?.ticket;
|
||||
if (!ticket) return null;
|
||||
|
||||
const requestHeaders = {
|
||||
...HEADERS,
|
||||
'X-Stream-Ticket': ticket,
|
||||
};
|
||||
|
||||
const targetServers = isTv ? SERVERS.filter(srv => !srv.movieOnly) : SERVERS;
|
||||
|
||||
const responses = await Promise.allSettled(
|
||||
targetServers.map(async server => {
|
||||
const url = isTv
|
||||
? `${BASE_URL}/streams/tv/${id}/${s}/${e}?s=${encodeURIComponent(server.id)}`
|
||||
: `${BASE_URL}/streams/movie/${id}?s=${encodeURIComponent(server.id)}`;
|
||||
|
||||
const res = await fetch(url, { headers: requestHeaders });
|
||||
if (!res.ok) return null;
|
||||
const data = await res.json();
|
||||
const decrypted = await decryptMeow(data);
|
||||
return { server, decrypted };
|
||||
})
|
||||
);
|
||||
|
||||
const allUrls = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const res of responses) {
|
||||
if (res.status !== 'fulfilled' || !res.value?.decrypted) continue;
|
||||
const { server, decrypted } = res.value;
|
||||
const found = extractUrls(decrypted);
|
||||
|
||||
for (const item of found) {
|
||||
if (!item?.url || seen.has(item.url)) continue;
|
||||
seen.add(item.url);
|
||||
allUrls.push({
|
||||
url: item.url,
|
||||
server: `MeowTV - ${server.label}`,
|
||||
quality: item.quality || 'Auto',
|
||||
type: item.type || (item.url.includes('.m3u8') ? 'hls' : 'mp4'),
|
||||
headers: {
|
||||
Referer: 'https://meowtv.ru/',
|
||||
Origin: 'https://meowtv.ru',
|
||||
'User-Agent': UA,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
94
src/sources/vidora.js
Normal file
94
src/sources/vidora.js
Normal file
@@ -0,0 +1,94 @@
|
||||
const BASE_URL = 'https://vidoraplayer.duckdns.org';
|
||||
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36';
|
||||
|
||||
const HEADERS = {
|
||||
'User-Agent': UA,
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
Accept: 'application/x-ndjson, text/plain, */*',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
};
|
||||
|
||||
function parseNdjson(text) {
|
||||
if (!text) return [];
|
||||
const normalized = text.replace(/\}\s*\{/g, '}\n{');
|
||||
const lines = normalized.split('\n');
|
||||
const items = [];
|
||||
for (const raw of lines) {
|
||||
const line = raw.trim();
|
||||
if (!line) continue;
|
||||
try {
|
||||
items.push(JSON.parse(line));
|
||||
} catch { }
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e } = args || {};
|
||||
const isTv = s != null && e != null;
|
||||
|
||||
const query = isTv
|
||||
? `type=tv&id=${id}&season=${s}&episode=${e}`
|
||||
: `type=movie&id=${id}`;
|
||||
|
||||
const urls = [
|
||||
`${BASE_URL}/api/sources?${query}`,
|
||||
`${BASE_URL}/api/sources?${query}&gen=2`,
|
||||
];
|
||||
|
||||
const responses = await Promise.all(
|
||||
urls.map(url =>
|
||||
fetch(url, { headers: HEADERS })
|
||||
.then(r => (r.ok ? r.text() : ''))
|
||||
.catch(() => '')
|
||||
)
|
||||
);
|
||||
|
||||
const allUrls = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const text of responses) {
|
||||
const events = parseNdjson(text);
|
||||
for (const item of events) {
|
||||
const list = [];
|
||||
if (item?.event === 'source' && item?.source) {
|
||||
list.push(item.source);
|
||||
} else if (item?.event === 'done' && Array.isArray(item?.sources)) {
|
||||
list.push(...item.sources);
|
||||
} else if (item?.url) {
|
||||
list.push(item);
|
||||
}
|
||||
|
||||
for (const src of list) {
|
||||
if (!src?.url) continue;
|
||||
const fullUrl = src.url.startsWith('http')
|
||||
? src.url
|
||||
: `${BASE_URL}${src.url.startsWith('/') ? '' : '/'}${src.url}`;
|
||||
|
||||
if (!seen.has(fullUrl)) {
|
||||
seen.add(fullUrl);
|
||||
allUrls.push({
|
||||
url: fullUrl,
|
||||
server: `Vidora - ${src.label || src.server || src.providerGroup || 'Default'}`,
|
||||
quality: src.quality || 'Auto',
|
||||
type: src.type || (fullUrl.includes('.m3u8') ? 'hls' : 'mp4'),
|
||||
headers: {
|
||||
Referer: `${BASE_URL}/`,
|
||||
Origin: BASE_URL,
|
||||
'User-Agent': UA,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!allUrls.length) return null;
|
||||
|
||||
return {
|
||||
url: allUrls[0].url,
|
||||
headers: allUrls[0].headers,
|
||||
allUrls,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user