added allaboutmovies, added cinebros, added movienerds, added netplayz, added taroflix, added vidspark

This commit is contained in:
2026-11-07 20:43:08 -08:00
parent e83ee6c1b8
commit 4eda9819ce
7 changed files with 1037 additions and 1 deletions

View File

@@ -89,6 +89,24 @@ export const SOURCES = [
}],
},
{
key: 'allaboutmovies',
label: 'AllAboutMovies',
sourceFile: 'allaboutmovies',
proxyParam: 'aam',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
cdnHeaders: [{
pattern: /lorenzo-rapattoni-pc\.workers\.dev/i,
headers: {
Referer: 'https://allaboutmovies.pages.dev/',
Origin: 'https://allaboutmovies.pages.dev',
},
}],
},
{
key: 'anizone',
label: 'AniZone',
@@ -154,6 +172,24 @@ export const SOURCES = [
multiUrl: true,
},
{
key: 'cinebros',
label: 'CineBros',
sourceFile: 'cinebros',
proxyParam: 'cb',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
cdnHeaders: [{
pattern: /cinebros\.net|medialink\.[a-z0-9-]+\.workers\.dev/i,
headers: {
Referer: 'https://cinebros.net/',
Origin: 'https://cinebros.net',
},
}],
},
{
key: 'cinemaarmy',
label: 'CinemaArmy',
@@ -437,6 +473,17 @@ export const SOURCES = [
}],
},
{
key: 'movienerds',
label: 'MovieNerds',
sourceFile: 'movienerds',
proxyParam: 'mvi',
timeout: 35000,
jitter: 500,
retries: 2,
multiUrl: true,
},
{
key: 'movy',
label: 'Movy',
@@ -466,6 +513,24 @@ export const SOURCES = [
multiUrl: true,
},
{
key: 'netplayz',
label: 'NetPlayz',
sourceFile: 'netplayz',
proxyParam: 'npz',
timeout: 30000,
jitter: 500,
retries: 2,
multiUrl: true,
cdnHeaders: [{
pattern: /fcxmb\.com|hscow\.com/i,
headers: {
Referer: 'https://netplayz.icu/',
Origin: 'https://netplayz.icu',
},
}],
},
{
key: 'nexa',
label: 'Nexa',
@@ -515,7 +580,6 @@ export const SOURCES = [
timeout: 20000,
jitter: 500,
retries: 2,
skipProxy: true
},
{
@@ -547,6 +611,24 @@ export const SOURCES = [
}],
},
{
key: 'taroflix',
label: 'TaroFlix',
sourceFile: 'taroflix',
proxyParam: 'tf',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
cdnHeaders: [{
pattern: /starkio\.cyou|taroflix\.net|workers\.dev/i,
headers: {
Referer: 'https://taroflix.net/',
Origin: 'https://taroflix.net',
},
}],
},
{
key: 'streamaggregator',
label: 'StreamAggregator',
@@ -706,6 +788,24 @@ export const SOURCES = [
}],
},
{
key: 'vidspark',
label: 'VidSpark',
sourceFile: 'vidspark',
proxyParam: 'vsp',
timeout: 25000,
jitter: 500,
retries: 2,
multiUrl: true,
cdnHeaders: [{
pattern: /vidspark\.to|paretocdn\.site|netrocdn\.site/i,
headers: {
Referer: 'https://vidspark.to/',
Origin: 'https://vidspark.to',
},
}],
},
{
key: 'vidup',
label: 'VidUp',

View File

@@ -0,0 +1,81 @@
import { USER_AGENT } from '../utils/helpers.js';
const BASE_URL = 'https://allaboutmovies.pages.dev';
const API_HEADERS = {
Accept: 'application/json',
'User-Agent': USER_AGENT,
Referer: `${BASE_URL}/`,
};
const PLAYBACK_HEADERS = {
'User-Agent': USER_AGENT,
};
function buildApiUrl(id, s, e) {
if (s != null && e != null) {
return `${BASE_URL}/api/scrape/tv/${id}/${s}/${e}`;
}
return `${BASE_URL}/api/scrape/movie/${id}`;
}
function normalizeQuality(q) {
if (!q || q === 'Auto') return 'Auto';
const str = String(q).trim();
return /^\d+$/.test(str) ? `${str}p` : str;
}
export async function getStream(args) {
const { id, s, e, config } = args || {};
if (!id) return null;
const url = buildApiUrl(id, s, e);
try {
const res = await fetch(url, {
headers: API_HEADERS,
signal: AbortSignal.timeout(config?.timeout || 25000),
});
if (!res.ok) return null;
const data = await res.json();
if (!data || !Array.isArray(data.servers)) return null;
const readyServers = data.servers.filter(
srv => srv.status === 'ready' && typeof srv.url === 'string' && srv.url.startsWith('http')
);
if (!readyServers.length) return null;
const allUrls = [];
const seen = new Set();
for (const srv of readyServers) {
if (seen.has(srv.url)) continue;
seen.add(srv.url);
const quality = normalizeQuality(srv.quality);
const type = srv.type || (srv.url.includes('.m3u8') ? 'hls' : srv.url.includes('.mpd') ? 'dash' : 'mp4');
const serverName = srv.name || srv.key || 'Server';
allUrls.push({
url: srv.url,
server: `AllAboutMovies - ${serverName}`,
quality,
type,
headers: PLAYBACK_HEADERS,
});
}
if (!allUrls.length) return null;
return {
url: allUrls[0].url,
headers: allUrls[0].headers,
allUrls,
};
} catch {
return null;
}
}

159
src/sources/cinebros.js Normal file
View File

@@ -0,0 +1,159 @@
import { USER_AGENT, getTmdbMetadata, resolveTmdbKey } from '../utils/helpers.js';
const BASE_URL = 'https://cinebros.net';
const HEADERS = {
'User-Agent': USER_AGENT,
Accept: '*/*',
'Accept-Language': 'en-US,en;q=0.9',
Origin: BASE_URL,
Referer: `${BASE_URL}/`,
};
const PLAYBACK_HEADERS = {
'User-Agent': USER_AGENT,
Referer: `${BASE_URL}/`,
Origin: BASE_URL,
};
function parseJsonStream(text) {
if (!text) return [];
const items = [];
let depth = 0;
let inString = false;
let escape = false;
let startIndex = -1;
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) startIndex = i;
depth++;
} else if (char === '}') {
depth--;
if (depth === 0 && startIndex !== -1) {
const chunk = text.slice(startIndex, i + 1);
try {
items.push(JSON.parse(chunk));
} catch { }
startIndex = -1;
}
}
}
}
return items;
}
function getStreamScore(item) {
let score = 0;
const qMatch = String(item.quality || '').match(/\d+/);
if (qMatch) score += parseInt(qMatch[0], 10);
if (item.url && item.url.includes('/stream/')) score += 10000;
if (item.url && item.url.includes('blob=')) score -= 5000;
return score;
}
export async function getStream(args) {
const { id, s, e, sdk } = args || {};
if (!id) return null;
const isTv = s != null && e != null;
const apiKey = resolveTmdbKey(args, sdk);
const meta = await getTmdbMetadata(id, isTv, apiKey).catch(() => null);
const title = args?.title || meta?.title || '';
const year = args?.year || meta?.year || '';
const runtime = args?.runtime || meta?.runtime || (isTv ? '24' : '90');
const params = new URLSearchParams({
tmdbId: String(id),
type: isTv ? 'tv' : 'movie',
title: title || String(id),
});
if (isTv) {
params.set('seasonId', String(s || 1));
params.set('episodeId', String(e || 1));
}
if (year) params.set('year', String(year));
if (runtime) params.set('runtime', String(runtime));
const referer = isTv
? `${BASE_URL}/watch/tv/${id}/${s || 1}/${e || 1}`
: `${BASE_URL}/watch/movie/${id}`;
const targetUrl = `${BASE_URL}/api/streams?${params.toString()}`;
try {
const res = await fetch(targetUrl, {
headers: {
...HEADERS,
Referer: referer,
},
signal: AbortSignal.timeout(args?.config?.timeout || 25000),
});
if (!res.ok) return null;
const text = await res.text();
const items = parseJsonStream(text);
const allUrls = [];
const seen = new Set();
for (const item of items) {
const streamUrl = item?.url;
if (!streamUrl || typeof streamUrl !== 'string' || !streamUrl.startsWith('http')) {
continue;
}
if (seen.has(streamUrl)) continue;
seen.add(streamUrl);
let quality = 'Auto';
if (item.quality) {
const qStr = String(item.quality).trim();
quality = /^\d+$/.test(qStr) ? `${qStr}p` : qStr;
}
const serverLabel = item.name ? `CineBros - ${item.name}` : `CineBros (${quality})`;
const type = item.type || (streamUrl.includes('.m3u8') || streamUrl.includes('/stream/')
? 'hls'
: streamUrl.includes('.mpd')
? 'dash'
: 'mp4');
allUrls.push({
url: streamUrl,
server: serverLabel,
quality,
type,
headers: PLAYBACK_HEADERS,
});
}
if (!allUrls.length) return null;
allUrls.sort((a, b) => getStreamScore(b) - getStreamScore(a));
return {
url: allUrls[0].url,
headers: allUrls[0].headers,
allUrls,
};
} catch {
return null;
}
}

111
src/sources/movienerds.js Normal file
View File

@@ -0,0 +1,111 @@
import { USER_AGENT } from '../utils/helpers.js';
const API_BASE = 'https://api.movienerds.online';
const STREAM_ORIGIN = 'https://vidrock.net';
const API_HEADERS = {
Accept: 'application/json',
'x-client-app': 'movienerds',
'x-app-key': 'mn_sec_2026_stream_auth',
};
const PLAYBACK_HEADERS = {
'User-Agent': USER_AGENT,
Referer: `${STREAM_ORIGIN}/`,
Origin: STREAM_ORIGIN,
};
const BAD_PATTERNS = [
'apopheniaatlas.site', 'lifestylefreedomlab.site', 'aoneroom.com/other',
'/other/202', 'mb=1', 'b164fbfb4347792950bdfbfb563d39d9', 'dummy',
'teaser', 'trailer', 'promo', 'sample', 'fastcloud.live', 'bigtits',
'tiktokcdn.com', 'tiktokcdn', 'tos-alisg', 'tos-maliva', 'byteoversea',
'ibytedtos', 'ibyteimg', 'bytedance', 'staticreverie.site', 'staticreverie',
];
function isBadUrl(url) {
if (!url) return true;
const lower = String(url).toLowerCase();
let decoded = '';
try { decoded = decodeURIComponent(lower); } catch { }
return BAD_PATTERNS.some(p => lower.includes(p) || decoded.includes(p));
}
function isValidSource(src) {
if (!src || !src.url) return false;
const url = String(src.url).toLowerCase();
const rawUrl = String(src.rawUrl || '').toLowerCase();
if (url.includes('unavailable') || rawUrl.includes('unavailable')) return false;
if (!url.startsWith('http://') && !url.startsWith('https://')) return false;
return !isBadUrl(url) && !isBadUrl(rawUrl);
}
export async function getStream(args) {
const { id, s, e, config } = args || {};
if (!id) return null;
const isTv = s != null && e != null;
const params = new URLSearchParams({
tmdbId: String(id),
type: isTv ? 'tv' : 'movie',
lang: 'en',
});
if (isTv) {
params.set('season', String(s));
params.set('episode', String(e));
}
try {
const res = await fetch(`${API_BASE}/api/stream?${params.toString()}`, {
headers: API_HEADERS,
signal: AbortSignal.timeout(config?.timeout || 35000),
});
if (!res.ok) return null;
const data = await res.json();
const sources = Array.isArray(data?.sources) ? data.sources.filter(isValidSource) : [];
const direct = sources.filter(src => src.type !== 'embed' && src.isDirect !== false);
const embed = sources.filter(src => src.type === 'embed' || src.isDirect === false);
const sorted = direct.length ? [...direct, ...embed] : embed;
if (!sorted.length) return null;
const allUrls = [];
const seen = new Set();
for (const src of sorted) {
if (seen.has(src.url)) continue;
seen.add(src.url);
const rawUrl = String(src.url);
const type = src.type === 'embed'
? 'hls'
: (rawUrl.includes('.m3u8') ? 'hls' : (rawUrl.includes('.mpd') ? 'dash' : 'mp4'));
const quality = src.quality
? (/^\d+$/.test(String(src.quality)) ? `${src.quality}p` : String(src.quality))
: 'Auto';
const serverName = src.server || src.name || 'Direct';
allUrls.push({
url: rawUrl,
server: `MovieNerds - ${serverName}`,
quality,
type,
headers: PLAYBACK_HEADERS,
});
}
if (!allUrls.length) return null;
return {
url: allUrls[0].url,
headers: allUrls[0].headers,
allUrls,
};
} catch {
return null;
}
}

149
src/sources/netplayz.js Normal file
View File

@@ -0,0 +1,149 @@
import crypto from 'node:crypto';
import { USER_AGENT, getTmdbMetadata, resolveTmdbKey } from '../utils/helpers.js';
const API_BASE = 'https://api.hwnou.com';
const KEY = Buffer.from('fJATm*kgfJT!BgJB', 'utf8');
const OKHTTP_UA = 'okhttp/3.2.0';
const APK_SIGN_KEY = 'ED0955EB04E67A1D9F3305B95454FED485261475';
const STREAM_HEADERS = {
'User-Agent': USER_AGENT,
Referer: 'https://thugtv.online/',
Origin: 'https://thugtv.online',
};
function decrypt(raw) {
const decipher = crypto.createDecipheriv('aes-128-cbc', KEY, KEY);
const decoded = Buffer.from(raw, 'base64');
return Buffer.concat([decipher.update(decoded), decipher.final()]).toString('utf8');
}
function parseDecrypted(raw) {
const text = decrypt(raw).trim();
const quoted = text.replace(/(?<=[:\[,]\s*)(\d{16,})(?=\s*[,\]}])/g, '"$1"');
return JSON.parse(quoted);
}
async function hwnouFetch(url, { method = 'GET', body = null, signal } = {}) {
try {
const headers = {
Accept: '*/*',
'Accept-Language': 'en-US,en;q=0.9',
'User-Agent': OKHTTP_UA,
};
if (body) headers['Content-Type'] = 'application/json;charset=utf-8';
const res = await fetch(url, { method, body, headers, signal });
if (!res.ok) return null;
const text = await res.text();
if (!text) return null;
return parseDecrypted(text);
} catch {
return null;
}
}
function pickBestVideo(ep) {
const videos = (ep?.videos || []).filter(v => v && v.premiumProPermission === false);
if (!videos.length) return (ep?.videos || [])[0] || null;
videos.sort((a, b) => (b.resolution || 0) - (a.resolution || 0));
return videos[0];
}
function pickLanguageId(ep) {
const tracks = ep?.tracks || [];
const pref = tracks.find(t => t && t.languageId === 1003);
return (pref || tracks[0] || {}).languageId || null;
}
function normalizeTitle(title) {
return String(title).toLowerCase().replace(/ /g, '+');
}
export async function getStream(args) {
const { id, s, e, sdk, signal } = args || {};
if (!id) return null;
const isTv = s != null && e != null;
const episodeNum = isTv ? (parseInt(e, 10) || 1) : 1;
let title = args?.title;
if (!title) {
try {
const meta = await getTmdbMetadata(id, isTv, resolveTmdbKey(args, sdk));
title = meta?.title;
} catch { }
}
if (!title) return null;
try {
const searchUrl = `${API_BASE}/film-api/v2.1.2/movie/searchByKeyword?keyword=${encodeURIComponent(normalizeTitle(title))}&page=1&size=20&mode=1&secure=0&channel=IndiaA&appMarket=GuanWang&packageName=com.external.castle&clientType=1`;
const searchData = await hwnouFetch(searchUrl, { signal });
const rows = searchData?.data?.rows || [];
if (!rows.length) return null;
const wantMovie = !isTv;
const candidates = rows.filter(r => (r.movieType === 2) === wantMovie);
if (!candidates.length) return null;
const exact = candidates.find(r => String(r.title || '').toLowerCase() === String(title).toLowerCase());
const match = exact || candidates[0];
const movieId = match.id;
if (!movieId) return null;
const detailsUrl = `${API_BASE}/film-api/v2.1.2/movie?movieId=${movieId}&packageName=com.external.castle&clientType=1`;
const detailsData = await hwnouFetch(detailsUrl, { signal });
const episodes = detailsData?.data?.episodes || [];
if (!episodes.length) return null;
const ep = isTv
? (episodes.find(x => Number(x.number) === episodeNum) || episodes[0])
: episodes[0];
const video = pickBestVideo(ep);
if (!video) return null;
const languageId = pickLanguageId(ep);
const streamApiUrl = `${API_BASE}/film-api/v2.0.7/movie/getVideo2?clientType=1&packageName=PKG&channel=CHANNEL&lang=en-US`;
const streamBody = JSON.stringify({
appMarket: 'GuanWang',
woolUser: 'false',
isp: 'Android',
useVipCdn: 'false',
movieId,
episodeId: ep.id,
isNewUser: 'true',
resolution: video.resolution,
mode: '1',
clientType: '1',
apkSignKey: APK_SIGN_KEY,
androidVersion: '11',
networkType: 'NETWORK_WIFI',
packageName: 'com.external.castle',
languageId,
});
const streamData = await hwnouFetch(streamApiUrl, { method: 'POST', body: streamBody, signal });
const videoUrl = streamData?.data?.videoUrl;
if (!videoUrl) return null;
const quality = video.resolutionDescription || (video.resolution ? `${video.resolution}p` : 'Auto');
const type = videoUrl.includes('.m3u8') ? 'hls' : (videoUrl.includes('.mpd') ? 'dash' : 'mp4');
const allUrls = [{
url: videoUrl,
server: `NetPlayz - ${quality}`,
quality,
type,
headers: STREAM_HEADERS,
}];
return {
url: allUrls[0].url,
headers: allUrls[0].headers,
allUrls,
};
} catch {
return null;
}
}

297
src/sources/taroflix.js Normal file
View File

@@ -0,0 +1,297 @@
import crypto from 'node:crypto';
import { USER_AGENT } from '../utils/helpers.js';
const TARO_ORIGIN = 'https://taroflix.net';
const BACKEND_BASE = 'https://starkio.cyou';
const FALLBACK_SCHEME = {
v: 1,
salt: 'c1n3t4r-0bf5c4t10n-s4lt-v1-2024-09',
xk: 'c1n3t4r-0uter-xk-v1',
transform: 'none',
mode: 'on',
};
const HEADERS = {
'User-Agent': USER_AGENT,
Accept: '*/*',
'Accept-Language': 'en-US,en;q=0.9',
Referer: `${BACKEND_BASE}/`,
Origin: BACKEND_BASE,
};
const PLAYBACK_HEADERS = {
'User-Agent': USER_AGENT,
Referer: `${BACKEND_BASE}/`,
Origin: BACKEND_BASE,
};
// Known active servers ranked by reliability
// Hemlic [Multi], Siddle, and Tyler yield the workers.dev/v1... stream directly
const ACTIVE_SERVERS = [
{ name: 'Hemlic [Multi]', responseType: 'vidly' },
{ name: 'Siddle', responseType: 'vidly' },
{ name: 'Tyler', responseType: 'vidlink' },
{ name: 'Cipher [Multi]', responseType: 'cipher' },
{ name: 'Eternal', responseType: 'xpass' },
{ name: 'Posiden', responseType: 'qualities', quality: '1080p' },
{ name: 'Helix', responseType: 'sources', quality: '1080p' },
{ name: 'Rimora', responseType: 'vidnest', quality: '1080p' },
];
let wasmExports = null;
let schemeCache = null;
let schemeAt = 0;
async function getScheme(force = false) {
if (!force && schemeCache && Date.now() - schemeAt < 120000) {
return schemeCache;
}
try {
const res = await fetch(`${BACKEND_BASE}/api/scheme`, {
headers: HEADERS,
signal: AbortSignal.timeout(5000),
});
if (res.ok) {
const data = await res.json();
if (data && typeof data.v === 'number') {
schemeCache = data;
schemeAt = Date.now();
return schemeCache;
}
}
} catch { }
schemeCache = FALLBACK_SCHEME;
schemeAt = Date.now();
return schemeCache;
}
async function loadWasm() {
if (wasmExports) return wasmExports;
const res = await fetch(`${BACKEND_BASE}/decrypt.wasm`, {
headers: HEADERS,
signal: AbortSignal.timeout(8000),
});
if (!res.ok) throw new Error(`Failed to load decrypt.wasm: ${res.status}`);
const bytes = await res.arrayBuffer();
const env = { abort: () => { } };
const { instance } = await WebAssembly.instantiate(bytes, { env });
wasmExports = instance.exports;
return wasmExports;
}
function deriveKey(pub, sch) {
const basis = (sch.v >= 2) ? `${pub}|${sch.v}|${sch.salt}` : `${pub}${sch.salt}`;
return new Uint8Array(crypto.createHash('sha256').update(basis, 'utf8').digest());
}
function untransform(out, pub, sch) {
if (sch.transform === 'reverse') {
return out.slice().reverse();
}
if (sch.transform === 'xor2') {
const basis = `${sch.xk}|${sch.v}|${pub}`;
const k2 = new Uint8Array(crypto.createHash('sha256').update(basis, 'utf8').digest());
const copy = new Uint8Array(out);
for (let i = 0; i < copy.length; i++) {
copy[i] ^= k2[i % 32];
}
return copy;
}
return out;
}
async function decWith(raw, pub, sch) {
const [key, exports] = await Promise.all([
deriveKey(pub, sch),
loadWasm(),
]);
const { memory, allocBuffer, freeBuffer, normalizeBuffer } = exports;
const binary = Buffer.from(raw.enc, 'base64');
const encBytes = new Uint8Array(binary);
const encPtr = allocBuffer(encBytes.length);
const keyPtr = allocBuffer(32);
const outPtr = allocBuffer(encBytes.length);
try {
let view = new Uint8Array(memory.buffer);
view.set(encBytes, encPtr);
view.set(key, keyPtr);
const len = normalizeBuffer(encPtr, encBytes.length, keyPtr, outPtr);
view = new Uint8Array(memory.buffer);
const out = new Uint8Array(len);
for (let i = 0; i < len; i++) {
out[i] = view[outPtr + i];
}
const untransformed = untransform(out, pub, sch);
return JSON.parse(new TextDecoder().decode(untransformed));
} finally {
freeBuffer(encPtr);
freeBuffer(keyPtr);
freeBuffer(outPtr);
}
}
async function decryptResponse(raw, pub) {
if (!raw || !raw.enc) return raw;
try {
const sch = await getScheme();
if (sch.mode === 'kill') return null;
try {
return await decWith(raw, pub, sch);
} catch {
const fresh = await getScheme(true);
if (fresh && fresh.mode !== 'kill' && fresh.v !== sch.v) {
return await decWith(raw, pub, fresh);
}
return null;
}
} catch {
return null;
}
}
function parseStreamData(data, srv) {
if (!data) return [];
const type = srv.responseType || 'stream';
const serverName = srv.name || 'Default';
const urls = [];
const add = (url, quality = 'Auto', isPrimary = false) => {
if (!url || typeof url !== 'string' || !url.startsWith('http')) return;
const cleanUrl = url.replace(/#\.mp4$/i, '');
let qStr = String(quality || 'Auto').trim();
if (/^\d+$/.test(qStr)) qStr = `${qStr}p`;
urls.push({
url: cleanUrl,
server: `TaroFlix - ${serverName}`,
quality: qStr,
type: cleanUrl.includes('.m3u8') || cleanUrl.includes('workers.dev') ? 'hls' : (cleanUrl.includes('.mpd') ? 'dash' : 'mp4'),
headers: PLAYBACK_HEADERS,
isPrimary,
});
};
// Vidly: Hemlic [Multi] and Siddle
if (type === 'vidly' && Array.isArray(data.streams)) {
for (const s of data.streams) {
if (s?.url) {
add(s.url, s.quality || s.label || 'Auto', true);
}
}
} else if (type === 'vidlink') {
// Tyler combined playlist
if (typeof data.combinedPlaylistUrl === 'string' && data.combinedPlaylistUrl) {
add(data.combinedPlaylistUrl, 'Auto', true);
}
if (data.stream?.playlist) {
add(data.stream.playlist, 'Auto', false);
}
} else if (type === 'cipher') {
const bigbang = (data.servers || []).find(s => s && s.name === 'BigBang');
add(bigbang?.url || data.url);
} else if (type === 'xpass' && Array.isArray(data.sources)) {
const sel = data.sources.find(s => s.server === data.selectedServer || s.label === data.selectedServer) || data.sources[0];
add(sel?.file);
} else if (type === 'qualities' && Array.isArray(data.qualities)) {
for (const q of data.qualities) {
add(q.url, q.quality);
}
} else if (type === 'sources' && Array.isArray(data.sources)) {
for (const s of data.sources) {
add(s.url, s.name || (s.resolution ? `${s.resolution}p` : 'Auto'));
}
} else if (type === 'lordflix') {
if (typeof data.playlist === 'string') add(data.playlist);
else if (Array.isArray(data.stream) && data.stream.length) {
const prim = data.stream.find(s => s.id === 'primary') || data.stream[0];
add(prim?.playlist);
}
} else if (type === 'vidnest') {
const vd = data.data;
if (vd?.url) add(vd.url);
else if (Array.isArray(vd?.streams)) {
for (const s of vd.streams) {
add(s.url, s.resolution || s.language || 'Auto');
}
}
} else {
const fallback = data.url || data.stream || data.streamUrl;
if (fallback) add(fallback);
}
return urls;
}
function scoreStream(item) {
let score = 0;
// Highest priority for the exact workers.dev / v1 stream
if (item.url?.includes('workers.dev')) score += 50000;
if (item.url?.includes('/v1.')) score += 30000;
if (item.isPrimary) score += 20000;
if (item.server?.includes('Hemlic') || item.server?.includes('Siddle')) score += 15000;
if (item.server?.includes('Tyler')) score += 10000;
if (item.type === 'hls') score += 1000;
return score;
}
export async function getStream(args) {
const { id, s, e } = args || {};
if (!id) return null;
const isTv = s != null && e != null;
const queryType = isTv ? `series&season=${s}&episode=${e}` : 'movie';
const referer = isTv
? `${BACKEND_BASE}/api/cinero/series/${id}/${s}/${e}`
: `${BACKEND_BASE}/api/cinero/movie/${id}`;
const requestHeaders = {
...HEADERS,
Referer: referer,
};
// Race top servers: Hemlic [Multi], Siddle, Tyler, Cipher, Eternal
const candidateBatch = ACTIVE_SERVERS.slice(0, 5);
const settled = await Promise.allSettled(
candidateBatch.map(async srv => {
const apiUrl = `${BACKEND_BASE}/api/proxy?server=${encodeURIComponent(srv.name)}&tmdb=${id}&type=${queryType}`;
const res = await fetch(apiUrl, {
headers: requestHeaders,
signal: AbortSignal.timeout(10000),
});
if (!res.ok) return null;
const raw = await res.json().catch(() => null);
if (!raw) return null;
const decrypted = await decryptResponse(raw, String(id));
return parseStreamData(decrypted, srv);
})
);
const allUrls = [];
const seen = new Set();
for (const res of settled) {
if (res.status !== 'fulfilled' || !Array.isArray(res.value)) continue;
for (const item of res.value) {
if (!item?.url || seen.has(item.url)) continue;
seen.add(item.url);
allUrls.push(item);
}
}
if (!allUrls.length) return null;
allUrls.sort((a, b) => scoreStream(b) - scoreStream(a));
return {
url: allUrls[0].url,
headers: allUrls[0].headers,
allUrls,
};
}

139
src/sources/vidspark.js Normal file
View File

@@ -0,0 +1,139 @@
import { USER_AGENT, fetchJson, fetchText } from '../utils/helpers.js';
const BASE_URL = 'https://vidspark.to';
const PLAYER_KEY = '3a67e8866ae1d2bb9e81fe7f73315a56eb3bdf5e3e755c7554c8be6910aa6b13';
const PLAYBACK_HEADERS = {
'User-Agent': USER_AGENT,
Referer: `${BASE_URL}/`,
Origin: BASE_URL,
};
async function resolveHlsVariants(masterUrl, headers) {
try {
const text = await fetchText(masterUrl, {
headers,
signal: AbortSignal.timeout(5000),
});
if (!text || !text.includes('#EXTM3U') || !text.includes('#EXT-X-STREAM-INF')) {
return null;
}
const baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1);
const lines = text.split('\n').map(l => l.trim());
const variants = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith('#EXT-X-STREAM-INF:')) {
let quality = 'Auto';
const resMatch = line.match(/RESOLUTION=\d+x(\d+)/i);
if (resMatch) quality = `${resMatch[1]}p`;
const nextLine = lines[i + 1];
if (nextLine && !nextLine.startsWith('#')) {
const fullUrl = nextLine.startsWith('http') ? nextLine : `${baseUrl}${nextLine}`;
variants.push({ url: fullUrl, quality });
}
}
}
return variants.length ? variants : null;
} catch {
return null;
}
}
function parseQualityNumber(q) {
const m = String(q).match(/\d+/);
return m ? parseInt(m[0], 10) : 0;
}
export async function getStream(args) {
const { id, s, e } = args || {};
if (!id) return null;
const isTv = s != null && e != null;
const targetUrl = isTv
? `${BASE_URL}/api/vidora/v1/tv/${id}/${s}/${e}`
: `${BASE_URL}/api/vidora/v1/movie/${id}`;
const referer = isTv
? `${BASE_URL}/tv/${id}/${s}/${e}`
: `${BASE_URL}/movie/${id}`;
const headers = {
'User-Agent': USER_AGENT,
Accept: 'application/json, text/plain, */*',
'Accept-Language': 'en-US,en;q=0.9',
Referer: referer,
Origin: BASE_URL,
'x-player-key': PLAYER_KEY,
};
try {
const data = await fetchJson(targetUrl, {
headers,
signal: AbortSignal.timeout(args?.config?.timeout || 25000),
});
if (!data?.result || !Array.isArray(data.sources) || !data.sources.length) {
return null;
}
const allUrls = [];
const seen = new Set();
for (const sourceItem of data.sources) {
const streamUrl = sourceItem?.url;
if (!streamUrl || typeof streamUrl !== 'string' || !streamUrl.startsWith('http')) {
continue;
}
const serverLabel = sourceItem.source
? sourceItem.source.charAt(0).toUpperCase() + sourceItem.source.slice(1)
: 'Direct';
if (streamUrl.includes('.m3u8')) {
const variants = await resolveHlsVariants(streamUrl, PLAYBACK_HEADERS);
if (variants && variants.length) {
for (const v of variants) {
if (seen.has(v.url)) continue;
seen.add(v.url);
allUrls.push({
url: v.url,
server: `VidSpark - ${serverLabel} (${v.quality})`,
quality: v.quality,
type: 'hls',
headers: PLAYBACK_HEADERS,
});
}
}
}
if (!seen.has(streamUrl)) {
seen.add(streamUrl);
allUrls.push({
url: streamUrl,
server: `VidSpark - ${serverLabel}`,
quality: 'Auto',
type: streamUrl.includes('.m3u8') ? 'hls' : (streamUrl.includes('.mpd') ? 'dash' : 'mp4'),
headers: PLAYBACK_HEADERS,
});
}
}
if (!allUrls.length) return null;
allUrls.sort((a, b) => parseQualityNumber(b.quality) - parseQualityNumber(a.quality));
return {
url: allUrls[0].url,
headers: allUrls[0].headers,
allUrls,
};
} catch {
return null;
}
}