mirror of
https://gitlab.com/vyla-entertainment/sdk.git
synced 2026-11-13 21:57:27 -05:00
added allaboutmovies, added cinebros, added movienerds, added netplayz, added taroflix, added vidspark
This commit is contained in:
102
src/config.js
102
src/config.js
@@ -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',
|
||||
|
||||
81
src/sources/allaboutmovies.js
Normal file
81
src/sources/allaboutmovies.js
Normal 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
159
src/sources/cinebros.js
Normal 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
111
src/sources/movienerds.js
Normal 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
149
src/sources/netplayz.js
Normal 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
297
src/sources/taroflix.js
Normal 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
139
src/sources/vidspark.js
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user