diff --git a/src/config.js b/src/config.js index e61de1d..2ebc7da 100644 --- a/src/config.js +++ b/src/config.js @@ -8,7 +8,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: false, - skipProxy: true, cdnHeaders: [{ pattern: /3eyedraven\.watch/i, headers: { @@ -27,7 +26,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /4movie\.in|aapanel\.devcorp\.me/i, headers: { @@ -46,7 +44,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: false, - skipProxy: true, cdnHeaders: [{ pattern: /hlsx\d+cdn\.|burntburst\d+\.store|echovideo\.ru/i, headers: { @@ -65,7 +62,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /111477\.xyz/i, headers: { @@ -84,7 +80,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /raccoonunbutteredbagel\.zip|1x2\.space|kaxate\.fun|mechanicalserenity\.xyz|reelvault\.click|visionaryfounderslab\.site|goldenkite\.top|aether\.ist/i, headers: { @@ -103,7 +98,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /braflix\.dev/i, headers: { @@ -122,7 +116,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, }, { @@ -134,7 +127,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /cinema\.army/i, headers: { @@ -153,7 +145,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /cinesrc\.st/i, headers: { @@ -172,7 +163,6 @@ export const SOURCES = [ jitter: 300, retries: 1, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /cno\.to/i, headers: { @@ -191,7 +181,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /themoviebox\.uk|vidrock\.|orbitingdreams\.xyz|fastcloud\.live|deton444eik\.com/i, headers: { @@ -210,7 +199,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /fmfau\.com/i, headers: { @@ -229,7 +217,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /framextv\.cc|streamos\.top|workers\.dev|films365\.org/i, headers: { @@ -248,7 +235,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, }, { @@ -260,7 +246,6 @@ export const SOURCES = [ retries: 1, jitter: 0, multiUrl: true, - skipProxy: true, }, { @@ -272,7 +257,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, }, { @@ -284,7 +268,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /hexa\.su|flixer\.su/i, headers: { @@ -303,7 +286,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /krussdomi\.com/i, headers: { @@ -375,7 +357,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /meowtv\.ru/i, headers: { @@ -394,7 +375,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /movenerds\.space/i, headers: { @@ -413,7 +393,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /wecollege\.net|movy\.sx|goldenkite\.top/i, headers: { @@ -434,6 +413,24 @@ export const SOURCES = [ multiUrl: true, }, + { + 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', @@ -443,7 +440,6 @@ export const SOURCES = [ jitter: 500, retries: 1, multiUrl: true, - skipProxy: true, disabled: false, }, @@ -507,7 +503,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /streamaggregator\.in/i, headers: { @@ -526,7 +521,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /thugtv\.online|fcxmb\.com|hscow\.com/i, headers: { @@ -545,7 +539,6 @@ export const SOURCES = [ jitter: 500, retries: 1, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /torrentio\.to|fcxmb\.com/i, headers: { @@ -575,7 +568,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /rousav\.tech|vidbolt\.xyz/i, headers: { @@ -594,7 +586,6 @@ export const SOURCES = [ jitter: 500, retries: 1, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /vidcore\.io/i, headers: { @@ -635,7 +626,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /vidoraplayer\.duckdns\.org/i, headers: { @@ -676,7 +666,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /wecollege\.net|vidy\.st/i, headers: { @@ -712,7 +701,6 @@ export const SOURCES = [ jitter: 0, retries: 2, multiUrl: false, - skipProxy: true, verifyHeaders: { Accept: 'application/json, text/javascript, /; q=0.01', 'Accept-Language': 'en-US,en;q=0.9', @@ -730,7 +718,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, }, { @@ -742,7 +729,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /hscow\.com|downloadwella\.com|willowmovies\.com/i, headers: { @@ -761,7 +747,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /films365\.org/i, headers: { @@ -780,7 +765,6 @@ export const SOURCES = [ jitter: 500, retries: 2, multiUrl: true, - skipProxy: true, cdnHeaders: [{ pattern: /yapgrid\.com|embedwave\.cc/i, headers: { diff --git a/src/sources/hexa.js b/src/sources/hexa.js index 8da193d..41522ea 100644 --- a/src/sources/hexa.js +++ b/src/sources/hexa.js @@ -1,61 +1,205 @@ -import { fetchJson, fetchText, getRandomHex } from '../utils/helpers.js'; +import crypto from 'node:crypto'; -const API_BASE = 'https://enc-dec.app/api'; -const BASE_URL = 'https://hexa.su'; -const TMDB_PROXY_BASE = 'https://theemoviedb.hexa.su'; +const BASE_URL = 'https://theemoviedb.hexa.su'; +const CAP_URL = 'https://cap.hexa.su/15d2cf0395'; +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 CHALLENGE_HEADERS = { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'Accept-Language': 'en-US,en;q=0.9,ru;q=0.8,fr;q=0.7,bg;q=0.6,pl;q=0.5,id;q=0.4,zh-CN;q=0.3,zh;q=0.2,ja;q=0.1,es;q=0.1', + Origin: 'https://hexa.su', + Referer: 'https://hexa.su/', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 OPR/129.0.0.0 (Edition std-2)', +}; + +const PLAYBACK_HEADERS = { + 'User-Agent': UA, + Referer: 'https://hexa.su/', + Origin: 'https://hexa.su', +}; + +let cachedCapToken = null; +let tokenUseCount = 0; + +async function fetchCapToken() { + if (cachedCapToken && tokenUseCount < 18) { + tokenUseCount++; + return cachedCapToken; + } + + try { + const chalRes = await fetch(`${CAP_URL}/challenge`, { + method: 'POST', + headers: CHALLENGE_HEADERS, + signal: AbortSignal.timeout(8000), + }); + if (!chalRes.ok) return null; + const challengeData = await chalRes.json(); + + const encRes = await fetch(`${DEC_API}/enc-hexa`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': UA, + }, + body: JSON.stringify({ challenge_data: challengeData }), + signal: AbortSignal.timeout(8000), + }); + if (!encRes.ok) return null; + const encJson = await encRes.json(); + if (encJson?.status !== 200 || !encJson.result) return null; + + const redeemRes = await fetch(`${CAP_URL}/redeem`, { + method: 'POST', + headers: CHALLENGE_HEADERS, + body: JSON.stringify(encJson.result), + signal: AbortSignal.timeout(8000), + }); + if (!redeemRes.ok) return null; + const redeemJson = await redeemRes.json(); + + if (redeemJson?.token) { + cachedCapToken = redeemJson.token; + tokenUseCount = 1; + return cachedCapToken; + } + } catch { + cachedCapToken = null; + } + return null; +} + +function extractStreams(result) { + const list = []; + if (!result) return list; + + const data = typeof result === 'string' && (result.startsWith('{') || result.startsWith('[')) + ? JSON.parse(result) + : result; + + if (typeof data === 'string' && /^https?:\/\//.test(data)) { + list.push({ + url: data, + server: 'Hexa - Direct', + quality: 'Auto', + type: data.includes('.m3u8') ? 'hls' : (data.includes('.mpd') ? 'dash' : 'mp4'), + headers: PLAYBACK_HEADERS, + }); + return list; + } + + const rawSources = Array.isArray(data) + ? data + : Array.isArray(data?.sources) + ? data.sources + : Array.isArray(data?.streams) + ? data.streams + : []; + + for (const item of rawSources) { + 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`; + + list.push({ + url: streamUrl, + server: `Hexa - ${item.server || item.name || 'Direct'}`, + 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')) { + list.push({ + url: data.url, + server: `Hexa - ${data.server || 'Direct'}`, + quality: data.quality || 'Auto', + type: data.type || (data.url.includes('.m3u8') ? 'hls' : (data.url.includes('.mpd') ? 'dash' : 'mp4')), + headers: PLAYBACK_HEADERS, + }); + } + + return list; +} export async function getStream(args) { const { id, s, e } = args || {}; + const idNum = parseInt(id, 10); + if (!idNum) return null; + const isTv = s != null && e != null; - const key = getRandomHex(32); + const path = isTv + ? `/api/tmdb/tv/${idNum}/season/${s}/episode/${e}/images` + : `/api/tmdb/movie/${idNum}/images`; + + const capToken = await fetchCapToken(); + if (!capToken) return null; + + const key = crypto.randomBytes(32).toString('hex'); const headers = { - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', - Referer: `${BASE_URL}/`, + 'User-Agent': UA, + Referer: 'https://hexa.su/', + Origin: 'https://hexa.su', Accept: 'text/plain', 'X-Fingerprint-Lite': 'e9136c41504646444', + 'X-Cap-Token': capToken, 'X-Api-Key': key, }; - const encHexaRes = await fetchJson(`${API_BASE}/enc-hexa`); - if (!encHexaRes || encHexaRes.status !== 200) return null; - headers['X-Cap-Token'] = encHexaRes.result.token; + try { + const res = await fetch(`${BASE_URL}${path}`, { + headers, + signal: AbortSignal.timeout(12000), + }); - const url = isTv - ? `${TMDB_PROXY_BASE}/api/tmdb/tv/${id}/season/${s}/episode/${e}/images` - : `${TMDB_PROXY_BASE}/api/tmdb/movie/${id}/images`; + if (!res.ok) { + cachedCapToken = null; + return null; + } - const encrypted = await fetchText(url, { headers }); - if (!encrypted) return null; + const encryptedText = (await res.text()).trim(); + if (!encryptedText) return null; - const decRes = await fetchJson(`${API_BASE}/dec-hexa`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ text: encrypted, key }), - }); + const decRes = await fetch(`${DEC_API}/dec-hexa`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': UA, + }, + body: JSON.stringify({ text: encryptedText, key }), + signal: AbortSignal.timeout(10000), + }); - if (!decRes || decRes.status !== 200 || !decRes.result) return null; + if (!decRes.ok) return null; + const decJson = await decRes.json(); + if (decJson?.status !== 200 || !decJson.result) return null; - const data = decRes.result; - const streamUrl = data.url || data.file || (Array.isArray(data.sources) ? data.sources[0]?.url : null); - if (!streamUrl) return null; + const streams = extractStreams(decJson.result); + if (!streams.length) return null; - const streamHeaders = { - Referer: `${BASE_URL}/`, - Origin: BASE_URL, - }; + const allUrls = []; + const seen = new Set(); - const allUrls = [{ - url: streamUrl, - server: 'Hexa', - quality: 'Auto', - type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', - headers: streamHeaders, - }]; + for (const item of streams) { + if (!item?.url || seen.has(item.url)) continue; + seen.add(item.url); + allUrls.push(item); + } - return { - url: streamUrl, - headers: streamHeaders, - allUrls, - }; + if (!allUrls.length) return null; + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; + } catch { + return null; + } } \ No newline at end of file diff --git a/src/sources/nexa.js b/src/sources/nexa.js new file mode 100644 index 0000000..714a820 --- /dev/null +++ b/src/sources/nexa.js @@ -0,0 +1,179 @@ +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/test/index.js b/test/index.js index 601a6db..39e9a05 100644 --- a/test/index.js +++ b/test/index.js @@ -13,7 +13,7 @@ const CONFIG = { TV_ID: "37854", // TV TMDB ID to test, for anime I recommend using a random ID like 37854 TV_SEASON: "1", // TV Season to test TV_EPISODE: "1", // TV Episode to test - STREAM_SOURCE: "", // Source key to test + STREAM_SOURCE: "hexa", // Source key to test TEST_SUBTITLE: false, TEST_DOWNLOAD: false,