mirror of
https://gitlab.com/vyla-entertainment/sdk.git
synced 2026-11-09 19:57:23 -05:00
added chillflix, and trendimovies, removed nexa
This commit is contained in:
@@ -172,6 +172,24 @@ export const SOURCES = [
|
|||||||
multiUrl: true,
|
multiUrl: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
key: 'chillflix',
|
||||||
|
label: 'ChillFlix',
|
||||||
|
sourceFile: 'chillflix',
|
||||||
|
proxyParam: 'cf',
|
||||||
|
timeout: 25000,
|
||||||
|
jitter: 500,
|
||||||
|
retries: 2,
|
||||||
|
multiUrl: true,
|
||||||
|
cdnHeaders: [{
|
||||||
|
pattern: /chillflix\.pw|chillflix\.lol|nextgencloudfabric\.com/i,
|
||||||
|
headers: {
|
||||||
|
Referer: 'https://www.chillflix.pw/',
|
||||||
|
Origin: 'https://www.chillflix.pw',
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
key: 'cinebros',
|
key: 'cinebros',
|
||||||
label: 'CineBros',
|
label: 'CineBros',
|
||||||
@@ -531,24 +549,6 @@ export const SOURCES = [
|
|||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
key: 'nexa',
|
|
||||||
label: 'Nexa',
|
|
||||||
sourceFile: 'nexa',
|
|
||||||
proxyParam: 'nx',
|
|
||||||
timeout: 30000,
|
|
||||||
jitter: 500,
|
|
||||||
retries: 2,
|
|
||||||
multiUrl: true,
|
|
||||||
cdnHeaders: [{
|
|
||||||
pattern: /nexa\.mov|providers\.nexa\.mov/i,
|
|
||||||
headers: {
|
|
||||||
Referer: 'https://nexa.mov/',
|
|
||||||
Origin: 'https://nexa.mov',
|
|
||||||
},
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
key: 'pengu',
|
key: 'pengu',
|
||||||
label: 'Pengu',
|
label: 'Pengu',
|
||||||
@@ -683,6 +683,24 @@ export const SOURCES = [
|
|||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
key: 'trendimovies',
|
||||||
|
label: 'TrendiMovies',
|
||||||
|
sourceFile: 'trendimovies',
|
||||||
|
proxyParam: 'tm',
|
||||||
|
timeout: 25000,
|
||||||
|
jitter: 500,
|
||||||
|
retries: 2,
|
||||||
|
multiUrl: true,
|
||||||
|
cdnHeaders: [{
|
||||||
|
pattern: /trendimovies\.com|tgstream/i,
|
||||||
|
headers: {
|
||||||
|
Referer: 'https://trendimovies.com/',
|
||||||
|
Origin: 'https://trendimovies.com',
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
key: 'vidapi',
|
key: 'vidapi',
|
||||||
label: 'VidAPI',
|
label: 'VidAPI',
|
||||||
|
|||||||
185
src/sources/chillflix.js
Normal file
185
src/sources/chillflix.js
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import { USER_AGENT, fetchJson } from '../utils/helpers.js';
|
||||||
|
|
||||||
|
const BASE_URL = 'https://www.chillflix.pw';
|
||||||
|
const VAPLAYER_ORIGIN = 'https://nextgencloudfabric.com';
|
||||||
|
|
||||||
|
const HEADERS = {
|
||||||
|
'User-Agent': USER_AGENT,
|
||||||
|
Accept: 'application/json, text/plain, */*',
|
||||||
|
'Accept-Language': 'en-US,en;q=0.9',
|
||||||
|
Origin: BASE_URL,
|
||||||
|
};
|
||||||
|
|
||||||
|
const PLAYBACK_HEADERS = {
|
||||||
|
'User-Agent': USER_AGENT,
|
||||||
|
Referer: `${BASE_URL}/`,
|
||||||
|
Origin: BASE_URL,
|
||||||
|
};
|
||||||
|
|
||||||
|
const FAST_PROVIDERS = ['novahd', 'cinesu', 'filesun', 'notorrent'];
|
||||||
|
|
||||||
|
function qualityWeight(q) {
|
||||||
|
const s = String(q || '').toLowerCase();
|
||||||
|
if (s.includes('4k') || s.includes('2160') || s.includes('uhd')) return 2160;
|
||||||
|
if (s.includes('1440') || s.includes('2k')) return 1440;
|
||||||
|
if (s.includes('1080') || s.includes('fhd')) return 1080;
|
||||||
|
if (s.includes('720') || s.includes('hd')) return 720;
|
||||||
|
if (s.includes('480') || s.includes('sd')) return 480;
|
||||||
|
const m = s.match(/\d+/);
|
||||||
|
return m ? parseInt(m[0], 10) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDecoy(url) {
|
||||||
|
return /\/hm-decoy\/|\/promo\//i.test(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStream(args) {
|
||||||
|
const { id, s, e } = args || {};
|
||||||
|
if (!id) return null;
|
||||||
|
|
||||||
|
const isTv = s != null && e != null;
|
||||||
|
const type = isTv ? 'tv' : 'movie';
|
||||||
|
const timeout = args?.config?.timeout || 25000;
|
||||||
|
|
||||||
|
const referer = isTv
|
||||||
|
? `${BASE_URL}/embed/tv/${id}/${s}/${e}?autoplay=true&watchparty=false&title=false&parent_origin=https%3A%2F%2Fwww.chillflix.lol`
|
||||||
|
: `${BASE_URL}/embed/movie/${id}?autoplay=true&watchparty=false&title=false&parent_origin=https%3A%2F%2Fwww.chillflix.lol`;
|
||||||
|
|
||||||
|
const requestHeaders = {
|
||||||
|
...HEADERS,
|
||||||
|
Referer: referer,
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseParams = new URLSearchParams({
|
||||||
|
type,
|
||||||
|
tmdbId: String(id),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isTv) {
|
||||||
|
baseParams.set('season', String(s));
|
||||||
|
baseParams.set('episode', String(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
const tasks = [
|
||||||
|
async () => {
|
||||||
|
const data = await fetchJson(`${BASE_URL}/api/cinepro/sources?${baseParams.toString()}`, {
|
||||||
|
headers: requestHeaders,
|
||||||
|
signal: AbortSignal.timeout(timeout),
|
||||||
|
});
|
||||||
|
return { type: 'cinepro', data };
|
||||||
|
},
|
||||||
|
|
||||||
|
async () => {
|
||||||
|
const data = await fetchJson(`${BASE_URL}/api/playback/vaplayer-resolve?${baseParams.toString()}`, {
|
||||||
|
headers: requestHeaders,
|
||||||
|
signal: AbortSignal.timeout(12000),
|
||||||
|
});
|
||||||
|
return { type: 'vaplayer', data };
|
||||||
|
},
|
||||||
|
|
||||||
|
async () => {
|
||||||
|
const data = await fetchJson(`${BASE_URL}/api/4k/resolve?${baseParams.toString()}`, {
|
||||||
|
headers: requestHeaders,
|
||||||
|
signal: AbortSignal.timeout(12000),
|
||||||
|
});
|
||||||
|
return { type: '4k', data };
|
||||||
|
},
|
||||||
|
|
||||||
|
...FAST_PROVIDERS.map(prov => async () => {
|
||||||
|
const params = new URLSearchParams(baseParams);
|
||||||
|
params.set('provider', prov);
|
||||||
|
const data = await fetchJson(`${BASE_URL}/api/cinepro/sources?${params.toString()}`, {
|
||||||
|
headers: requestHeaders,
|
||||||
|
signal: AbortSignal.timeout(timeout),
|
||||||
|
});
|
||||||
|
return { type: 'cinepro', data };
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const results = await Promise.allSettled(tasks.map(t => t().catch(() => null)));
|
||||||
|
const allUrls = [];
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
|
for (const res of results) {
|
||||||
|
if (res.status !== 'fulfilled' || !res.value?.data) continue;
|
||||||
|
const { type: resType, data } = res.value;
|
||||||
|
|
||||||
|
if (resType === 'cinepro') {
|
||||||
|
const sources = Array.isArray(data.sources) ? data.sources : [];
|
||||||
|
for (const item of sources) {
|
||||||
|
if (!item?.url) continue;
|
||||||
|
|
||||||
|
let streamUrl = item.url;
|
||||||
|
if (streamUrl.startsWith('/')) {
|
||||||
|
streamUrl = `${BASE_URL}${streamUrl}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!streamUrl.startsWith('http') || seen.has(streamUrl) || isDecoy(streamUrl)) continue;
|
||||||
|
seen.add(streamUrl);
|
||||||
|
|
||||||
|
const providerName = item.provider?.name || item.provider?.id || 'Direct';
|
||||||
|
let quality = item.quality ? String(item.quality).trim() : 'Auto';
|
||||||
|
if (/^\d+$/.test(quality)) quality = `${quality}p`;
|
||||||
|
|
||||||
|
const lang = item.audioTracks?.[0]?.language || item.audioTracks?.[0]?.label;
|
||||||
|
const langLabel = lang && !/original/i.test(lang) ? ` (${lang})` : '';
|
||||||
|
|
||||||
|
allUrls.push({
|
||||||
|
url: streamUrl,
|
||||||
|
server: `ChillFlix - ${providerName} ${quality}${langLabel}`,
|
||||||
|
quality,
|
||||||
|
type: item.type || (streamUrl.includes('.m3u8') ? 'hls' : 'mp4'),
|
||||||
|
headers: PLAYBACK_HEADERS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (resType === 'vaplayer') {
|
||||||
|
const streams = Array.isArray(data.streams)
|
||||||
|
? data.streams
|
||||||
|
: (Array.isArray(data.urls) ? data.urls : []);
|
||||||
|
|
||||||
|
for (const rawUrl of streams) {
|
||||||
|
const streamUrl = String(rawUrl || '').trim();
|
||||||
|
if (!streamUrl.startsWith('http') || seen.has(streamUrl) || isDecoy(streamUrl)) continue;
|
||||||
|
seen.add(streamUrl);
|
||||||
|
|
||||||
|
allUrls.push({
|
||||||
|
url: streamUrl,
|
||||||
|
server: 'ChillFlix - VAPlayer',
|
||||||
|
quality: 'Auto',
|
||||||
|
type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||||
|
headers: {
|
||||||
|
'User-Agent': USER_AGENT,
|
||||||
|
Referer: data.referer || `${VAPLAYER_ORIGIN}/`,
|
||||||
|
Origin: VAPLAYER_ORIGIN,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (resType === '4k') {
|
||||||
|
const streams = Array.isArray(data.streams) ? data.streams : [];
|
||||||
|
for (const item of streams) {
|
||||||
|
const streamUrl = String(item?.url || '').trim();
|
||||||
|
if (!streamUrl.startsWith('http') || seen.has(streamUrl) || isDecoy(streamUrl)) continue;
|
||||||
|
seen.add(streamUrl);
|
||||||
|
|
||||||
|
const quality = item.quality || '4K';
|
||||||
|
allUrls.push({
|
||||||
|
url: streamUrl,
|
||||||
|
server: `ChillFlix - 4K (${quality})`,
|
||||||
|
quality,
|
||||||
|
type: item.type || (streamUrl.includes('.m3u8') ? 'hls' : 'mp4'),
|
||||||
|
headers: PLAYBACK_HEADERS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allUrls.length) return null;
|
||||||
|
|
||||||
|
allUrls.sort((a, b) => qualityWeight(b.quality) - qualityWeight(a.quality));
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: allUrls[0].url,
|
||||||
|
headers: allUrls[0].headers,
|
||||||
|
allUrls,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
const BASE_URL = 'https://providers.nexa.mov';
|
|
||||||
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,
|
|
||||||
Referer: 'https://nexa.mov/',
|
|
||||||
Origin: 'https://nexa.mov',
|
|
||||||
};
|
|
||||||
|
|
||||||
const PLAYBACK_HEADERS = {
|
|
||||||
'User-Agent': UA,
|
|
||||||
Referer: 'https://nexa.mov/',
|
|
||||||
Origin: 'https://nexa.mov',
|
|
||||||
};
|
|
||||||
|
|
||||||
const ANIME_SERVERS = new Set(['kyoto', 'tokyo', 'hokkaido', 'fuji']);
|
|
||||||
|
|
||||||
function extractUrls(result, serverName) {
|
|
||||||
const urls = [];
|
|
||||||
if (!result) return urls;
|
|
||||||
|
|
||||||
const data = typeof result === 'string' && (result.startsWith('{') || result.startsWith('['))
|
|
||||||
? JSON.parse(result)
|
|
||||||
: result;
|
|
||||||
|
|
||||||
if (typeof data === 'string' && /^https?:\/\//.test(data)) {
|
|
||||||
urls.push({
|
|
||||||
url: data,
|
|
||||||
server: `Nexa - ${serverName}`,
|
|
||||||
quality: 'Auto',
|
|
||||||
type: data.includes('.m3u8') ? 'hls' : (data.includes('.mpd') ? 'dash' : 'mp4'),
|
|
||||||
headers: PLAYBACK_HEADERS,
|
|
||||||
});
|
|
||||||
return urls;
|
|
||||||
}
|
|
||||||
|
|
||||||
const list = Array.isArray(data)
|
|
||||||
? data
|
|
||||||
: Array.isArray(data?.sources)
|
|
||||||
? data.sources
|
|
||||||
: Array.isArray(data?.streams)
|
|
||||||
? data.streams
|
|
||||||
: Array.isArray(data?.playlist)
|
|
||||||
? data.playlist
|
|
||||||
: [];
|
|
||||||
|
|
||||||
for (const item of list) {
|
|
||||||
const streamUrl = item?.url || item?.file || item?.src || (typeof item === 'string' ? item : null);
|
|
||||||
if (!streamUrl || typeof streamUrl !== 'string' || !streamUrl.startsWith('http')) continue;
|
|
||||||
|
|
||||||
let quality = item.quality || item.label || 'Auto';
|
|
||||||
if (/^\d+$/i.test(quality)) quality = `${quality}p`;
|
|
||||||
|
|
||||||
urls.push({
|
|
||||||
url: streamUrl,
|
|
||||||
server: `Nexa - ${item.server || item.name || serverName}`,
|
|
||||||
quality,
|
|
||||||
type: item.type || (streamUrl.includes('.m3u8') ? 'hls' : (streamUrl.includes('.mpd') ? 'dash' : 'mp4')),
|
|
||||||
headers: PLAYBACK_HEADERS,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data?.url && typeof data.url === 'string' && data.url.startsWith('http')) {
|
|
||||||
urls.push({
|
|
||||||
url: data.url,
|
|
||||||
server: `Nexa - ${data.server || serverName}`,
|
|
||||||
quality: data.quality || 'Auto',
|
|
||||||
type: data.type || (data.url.includes('.m3u8') ? 'hls' : (data.url.includes('.mpd') ? 'dash' : 'mp4')),
|
|
||||||
headers: PLAYBACK_HEADERS,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return urls;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAvailableServers() {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${BASE_URL}/v1/status`, {
|
|
||||||
headers: HEADERS,
|
|
||||||
signal: AbortSignal.timeout(6000),
|
|
||||||
});
|
|
||||||
if (!res.ok) return ['charlotte', 'albuquerque', 'denver'];
|
|
||||||
const json = await res.json();
|
|
||||||
const sources = Array.isArray(json?.sources) ? json.sources : [];
|
|
||||||
const active = sources
|
|
||||||
.map(s => s?.codename)
|
|
||||||
.filter(name => name && !ANIME_SERVERS.has(name));
|
|
||||||
return active.length > 0 ? active : ['charlotte', 'albuquerque', 'denver'];
|
|
||||||
} catch {
|
|
||||||
return ['charlotte', 'albuquerque', 'denver'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveServer(server, targetUrl) {
|
|
||||||
try {
|
|
||||||
const encRes = await fetch(`${DEC_API}/enc-nexa?url=${encodeURIComponent(targetUrl)}`, {
|
|
||||||
headers: { 'User-Agent': UA },
|
|
||||||
signal: AbortSignal.timeout(8000),
|
|
||||||
});
|
|
||||||
if (!encRes.ok) return null;
|
|
||||||
const encJson = await encRes.json();
|
|
||||||
if (encJson?.status !== 200 || !encJson.result) return null;
|
|
||||||
|
|
||||||
const payloadBuffer = Buffer.from(encJson.result, 'base64');
|
|
||||||
|
|
||||||
const playlistRes = await fetch(`${BASE_URL}/v1/playlist`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
...HEADERS,
|
|
||||||
'Content-Type': 'application/octet-stream',
|
|
||||||
},
|
|
||||||
body: payloadBuffer,
|
|
||||||
signal: AbortSignal.timeout(10000),
|
|
||||||
});
|
|
||||||
if (!playlistRes.ok) return null;
|
|
||||||
|
|
||||||
const playlistBytes = await playlistRes.arrayBuffer();
|
|
||||||
const hexData = Buffer.from(playlistBytes).toString('hex');
|
|
||||||
if (!hexData) return null;
|
|
||||||
|
|
||||||
const decRes = await fetch(`${DEC_API}/dec-nexa`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'User-Agent': UA,
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ text: hexData, server }),
|
|
||||||
signal: AbortSignal.timeout(8000),
|
|
||||||
});
|
|
||||||
if (!decRes.ok) return null;
|
|
||||||
const decJson = await decRes.json();
|
|
||||||
if (decJson?.status !== 200 || !decJson.result) return null;
|
|
||||||
|
|
||||||
return { server, result: decJson.result };
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getStream(args) {
|
|
||||||
const { id, s, e } = args || {};
|
|
||||||
if (!id) return null;
|
|
||||||
|
|
||||||
const isTv = s != null && e != null;
|
|
||||||
const servers = await getAvailableServers();
|
|
||||||
const targetServers = servers.slice(0, 3);
|
|
||||||
|
|
||||||
const responses = await Promise.allSettled(
|
|
||||||
targetServers.map(server => {
|
|
||||||
const url = isTv
|
|
||||||
? `https://providers.nexa.mov/?platform=tmdb&id=${id}&server=${server}&season=${s}&episode=${e}`
|
|
||||||
: `https://providers.nexa.mov/?platform=tmdb&id=${id}&server=${server}`;
|
|
||||||
return resolveServer(server, url);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const allUrls = [];
|
|
||||||
const seen = new Set();
|
|
||||||
|
|
||||||
for (const res of responses) {
|
|
||||||
if (res.status !== 'fulfilled' || !res.value?.result) continue;
|
|
||||||
const { server, result } = res.value;
|
|
||||||
const found = extractUrls(result, server);
|
|
||||||
for (const item of found) {
|
|
||||||
if (!item?.url || seen.has(item.url)) continue;
|
|
||||||
seen.add(item.url);
|
|
||||||
allUrls.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!allUrls.length) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
url: allUrls[0].url,
|
|
||||||
headers: allUrls[0].headers,
|
|
||||||
allUrls,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -26,8 +26,6 @@ const PLAYBACK_HEADERS = {
|
|||||||
Origin: 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 = [
|
const ACTIVE_SERVERS = [
|
||||||
{ name: 'Hemlic [Multi]', responseType: 'vidly' },
|
{ name: 'Hemlic [Multi]', responseType: 'vidly' },
|
||||||
{ name: 'Siddle', responseType: 'vidly' },
|
{ name: 'Siddle', responseType: 'vidly' },
|
||||||
@@ -177,7 +175,6 @@ function parseStreamData(data, srv) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Vidly: Hemlic [Multi] and Siddle
|
|
||||||
if (type === 'vidly' && Array.isArray(data.streams)) {
|
if (type === 'vidly' && Array.isArray(data.streams)) {
|
||||||
for (const s of data.streams) {
|
for (const s of data.streams) {
|
||||||
if (s?.url) {
|
if (s?.url) {
|
||||||
@@ -185,7 +182,6 @@ function parseStreamData(data, srv) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (type === 'vidlink') {
|
} else if (type === 'vidlink') {
|
||||||
// Tyler combined playlist
|
|
||||||
if (typeof data.combinedPlaylistUrl === 'string' && data.combinedPlaylistUrl) {
|
if (typeof data.combinedPlaylistUrl === 'string' && data.combinedPlaylistUrl) {
|
||||||
add(data.combinedPlaylistUrl, 'Auto', true);
|
add(data.combinedPlaylistUrl, 'Auto', true);
|
||||||
}
|
}
|
||||||
@@ -230,7 +226,6 @@ function parseStreamData(data, srv) {
|
|||||||
|
|
||||||
function scoreStream(item) {
|
function scoreStream(item) {
|
||||||
let score = 0;
|
let score = 0;
|
||||||
// Highest priority for the exact workers.dev / v1 stream
|
|
||||||
if (item.url?.includes('workers.dev')) score += 50000;
|
if (item.url?.includes('workers.dev')) score += 50000;
|
||||||
if (item.url?.includes('/v1.')) score += 30000;
|
if (item.url?.includes('/v1.')) score += 30000;
|
||||||
if (item.isPrimary) score += 20000;
|
if (item.isPrimary) score += 20000;
|
||||||
@@ -255,7 +250,6 @@ export async function getStream(args) {
|
|||||||
Referer: referer,
|
Referer: referer,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Race top servers: Hemlic [Multi], Siddle, Tyler, Cipher, Eternal
|
|
||||||
const candidateBatch = ACTIVE_SERVERS.slice(0, 5);
|
const candidateBatch = ACTIVE_SERVERS.slice(0, 5);
|
||||||
|
|
||||||
const settled = await Promise.allSettled(
|
const settled = await Promise.allSettled(
|
||||||
|
|||||||
187
src/sources/trendimovies.js
Normal file
187
src/sources/trendimovies.js
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import { USER_AGENT, fetchText } from '../utils/helpers.js';
|
||||||
|
|
||||||
|
const BASE = 'https://trendimovies.com';
|
||||||
|
|
||||||
|
const HEADERS = {
|
||||||
|
'User-Agent': USER_AGENT,
|
||||||
|
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'en-US,en;q=0.9',
|
||||||
|
Referer: `${BASE}/`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const PLAYBACK_HEADERS = {
|
||||||
|
'User-Agent': USER_AGENT,
|
||||||
|
Referer: `${BASE}/`,
|
||||||
|
};
|
||||||
|
|
||||||
|
function deepUnwrap(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
if (value.length === 2 && (value[0] === 0 || value[0] === 1)) {
|
||||||
|
return deepUnwrap(value[1]);
|
||||||
|
}
|
||||||
|
return value.map(deepUnwrap);
|
||||||
|
}
|
||||||
|
if (value && typeof value === 'object') {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(value).map(([k, v]) => [k, deepUnwrap(v)])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectFileType(url, item) {
|
||||||
|
const ext = item?.format || item?.extension || item?.ext || item?.type;
|
||||||
|
if (ext) return String(ext).toLowerCase().replace(/^\./, '');
|
||||||
|
const lower = String(url).toLowerCase();
|
||||||
|
if (lower.includes('.m3u8')) return 'hls';
|
||||||
|
if (lower.includes('.mpd')) return 'dash';
|
||||||
|
if (lower.includes('.mp4')) return 'mp4';
|
||||||
|
if (lower.includes('.avi')) return 'avi';
|
||||||
|
return 'mkv';
|
||||||
|
}
|
||||||
|
|
||||||
|
function qualityWeight(q) {
|
||||||
|
const s = String(q || '').toLowerCase();
|
||||||
|
if (s.includes('4k') || s.includes('2160')) return 2160;
|
||||||
|
const m = s.match(/\d+/);
|
||||||
|
return m ? parseInt(m[0], 10) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toStreamLinks(raw) {
|
||||||
|
if (!Array.isArray(raw)) return [];
|
||||||
|
const seen = new Set();
|
||||||
|
const links = [];
|
||||||
|
|
||||||
|
for (const item of raw) {
|
||||||
|
const url = item?.url ?? item?.file_url ?? item?.link;
|
||||||
|
if (!url || typeof url !== 'string' || !url.startsWith('http')) continue;
|
||||||
|
if (seen.has(url)) continue;
|
||||||
|
seen.add(url);
|
||||||
|
|
||||||
|
let quality = item.quality ?? item.resolution ?? 'Auto';
|
||||||
|
if (/^\d+$/.test(String(quality).trim())) {
|
||||||
|
quality = `${quality}p`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = item.file_size ?? item.size ?? null;
|
||||||
|
const language = item.language ?? null;
|
||||||
|
const type = detectFileType(url, item);
|
||||||
|
|
||||||
|
let serverLabel = `TrendiMovies - ${quality}`;
|
||||||
|
if (language) serverLabel += ` (${language})`;
|
||||||
|
if (size) serverLabel += ` [${size}]`;
|
||||||
|
|
||||||
|
links.push({
|
||||||
|
url,
|
||||||
|
server: serverLabel,
|
||||||
|
quality,
|
||||||
|
type,
|
||||||
|
headers: PLAYBACK_HEADERS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return links;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAstroIslands(html) {
|
||||||
|
const results = [];
|
||||||
|
if (!html) return results;
|
||||||
|
|
||||||
|
const regex = /<astro-island[^>]*props="([^"]+)"/g;
|
||||||
|
let match;
|
||||||
|
while ((match = regex.exec(html)) !== null) {
|
||||||
|
try {
|
||||||
|
const decoded = match[1]
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/'/g, "'")
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>');
|
||||||
|
|
||||||
|
const unwrapped = deepUnwrap(JSON.parse(decoded));
|
||||||
|
if (unwrapped) results.push(unwrapped);
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchMovieStreams(tmdbId, timeout) {
|
||||||
|
try {
|
||||||
|
const html = await fetchText(`${BASE}/movie/${tmdbId}`, {
|
||||||
|
headers: HEADERS,
|
||||||
|
signal: AbortSignal.timeout(timeout),
|
||||||
|
});
|
||||||
|
|
||||||
|
const islands = parseAstroIslands(html);
|
||||||
|
for (const island of islands) {
|
||||||
|
const raw = island?.links ?? island?.movie?.downloads ?? island?.downloads ?? island?.movie?.links;
|
||||||
|
const streams = toStreamLinks(raw);
|
||||||
|
if (streams.length) return streams;
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchTvStreams(tmdbId, s, e, timeout) {
|
||||||
|
let html = null;
|
||||||
|
try {
|
||||||
|
html = await fetchText(`${BASE}/tv/${tmdbId}`, {
|
||||||
|
headers: HEADERS,
|
||||||
|
signal: AbortSignal.timeout(timeout),
|
||||||
|
});
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
if (!html) {
|
||||||
|
try {
|
||||||
|
html = await fetchText(`${BASE}/show/${tmdbId}`, {
|
||||||
|
headers: HEADERS,
|
||||||
|
signal: AbortSignal.timeout(timeout),
|
||||||
|
});
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!html) return [];
|
||||||
|
|
||||||
|
const islands = parseAstroIslands(html);
|
||||||
|
let episodesBySeason = null;
|
||||||
|
let downloadsByEpisode = null;
|
||||||
|
|
||||||
|
for (const island of islands) {
|
||||||
|
episodesBySeason = island?.episodesBySeason ?? island?.show?.episodes ?? island?.episodes ?? episodesBySeason;
|
||||||
|
downloadsByEpisode = island?.downloadsByEpisode ?? island?.downloads ?? downloadsByEpisode;
|
||||||
|
if (episodesBySeason && downloadsByEpisode) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!episodesBySeason || !downloadsByEpisode) return [];
|
||||||
|
|
||||||
|
const seasonEpisodes = episodesBySeason[String(s)];
|
||||||
|
if (!Array.isArray(seasonEpisodes)) return [];
|
||||||
|
|
||||||
|
const ep = seasonEpisodes.find(item => String(item.episode_number) === String(e));
|
||||||
|
if (!ep || ep.id == null) return [];
|
||||||
|
|
||||||
|
const raw = downloadsByEpisode[String(ep.id)];
|
||||||
|
return toStreamLinks(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStream(args) {
|
||||||
|
const { id, s, e } = args || {};
|
||||||
|
if (!id) return null;
|
||||||
|
|
||||||
|
const isTv = s != null && e != null;
|
||||||
|
const timeout = args?.config?.timeout || 25000;
|
||||||
|
|
||||||
|
const allUrls = isTv
|
||||||
|
? await fetchTvStreams(id, s, e, timeout)
|
||||||
|
: await fetchMovieStreams(id, timeout);
|
||||||
|
|
||||||
|
if (!allUrls.length) return null;
|
||||||
|
|
||||||
|
allUrls.sort((a, b) => qualityWeight(b.quality) - qualityWeight(a.quality));
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: allUrls[0].url,
|
||||||
|
headers: allUrls[0].headers,
|
||||||
|
allUrls,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user