added chillflix, and trendimovies, removed nexa

This commit is contained in:
2026-11-08 12:01:44 -08:00
parent ea3dcace08
commit 5ba145e1ef
5 changed files with 408 additions and 203 deletions

View File

@@ -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
View 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,
};
}

View File

@@ -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,
};
}

View File

@@ -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
View 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(/&quot;/g, '"')
.replace(/&amp;/g, '&')
.replace(/&#39;/g, "'")
.replace(/&lt;/g, '<')
.replace(/&gt;/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,
};
}