diff --git a/src/config.js b/src/config.js index 10fac1a..38343a9 100644 --- a/src/config.js +++ b/src/config.js @@ -172,6 +172,24 @@ export const SOURCES = [ 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', 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', 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', label: 'VidAPI', diff --git a/src/sources/chillflix.js b/src/sources/chillflix.js new file mode 100644 index 0000000..c4e0f8f --- /dev/null +++ b/src/sources/chillflix.js @@ -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, + }; +} \ No newline at end of file diff --git a/src/sources/nexa.js b/src/sources/nexa.js deleted file mode 100644 index 714a820..0000000 --- a/src/sources/nexa.js +++ /dev/null @@ -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, - }; -} \ No newline at end of file diff --git a/src/sources/taroflix.js b/src/sources/taroflix.js index 2129cef..de9d6d8 100644 --- a/src/sources/taroflix.js +++ b/src/sources/taroflix.js @@ -26,8 +26,6 @@ const PLAYBACK_HEADERS = { 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' }, @@ -177,7 +175,6 @@ function parseStreamData(data, srv) { }); }; - // Vidly: Hemlic [Multi] and Siddle if (type === 'vidly' && Array.isArray(data.streams)) { for (const s of data.streams) { if (s?.url) { @@ -185,7 +182,6 @@ function parseStreamData(data, srv) { } } } else if (type === 'vidlink') { - // Tyler combined playlist if (typeof data.combinedPlaylistUrl === 'string' && data.combinedPlaylistUrl) { add(data.combinedPlaylistUrl, 'Auto', true); } @@ -230,7 +226,6 @@ function parseStreamData(data, srv) { 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; @@ -255,7 +250,6 @@ export async function getStream(args) { Referer: referer, }; - // Race top servers: Hemlic [Multi], Siddle, Tyler, Cipher, Eternal const candidateBatch = ACTIVE_SERVERS.slice(0, 5); const settled = await Promise.allSettled( diff --git a/src/sources/trendimovies.js b/src/sources/trendimovies.js new file mode 100644 index 0000000..5866cba --- /dev/null +++ b/src/sources/trendimovies.js @@ -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 = /]*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, + }; +} \ No newline at end of file