diff --git a/src/config.js b/src/config.js index 38343a9..85e4901 100644 --- a/src/config.js +++ b/src/config.js @@ -759,6 +759,24 @@ export const SOURCES = [ multiUrl: true, }, + { + key: 'vidflix', + label: 'VidFlix', + sourceFile: 'vidflix', + proxyParam: 'vfl', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + cdnHeaders: [{ + pattern: /vidflix\.club|followguchimany\.workers\.dev|hakunaymatata\.com|claritybusinessacademy\.site|fcxmb\.com/i, + headers: { + Referer: 'https://player.vidflix.club/', + Origin: 'https://player.vidflix.club', + }, + }], + }, + { key: 'vidlink', label: 'Vidlink', @@ -770,6 +788,24 @@ export const SOURCES = [ multiUrl: true, }, + { + key: 'vidlux', + label: 'VidLux', + sourceFile: 'vidlux', + proxyParam: 'vlx', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + cdnHeaders: [{ + pattern: /vidlux\.xyz/i, + headers: { + Referer: 'https://vidlux.xyz/', + Origin: 'https://vidlux.xyz', + }, + }], + }, + { key: 'vidly', label: 'Vidly', diff --git a/src/sources/vidflix.js b/src/sources/vidflix.js new file mode 100644 index 0000000..7f7eece --- /dev/null +++ b/src/sources/vidflix.js @@ -0,0 +1,96 @@ +import { fetchJson, USER_AGENT } from '../utils/helpers.js'; + +const BASE_URL = 'https://player.vidflix.club'; +const PLAYER_KEY = 'f3b72e73c80c9a996574379798703796a1936efa3516a7105cb0e43048b46b5a'; + +const PLAYBACK_HEADERS = { + 'User-Agent': USER_AGENT, + Referer: `${BASE_URL}/`, + Origin: BASE_URL, +}; + +const SERVERS = [ + { key: 'p9fx', label: 'Vertex' }, + { key: 'b4tz', label: 'Beta' }, + { key: 'w7dn', label: 'Nebula' }, + { key: 'vjej', label: 'Vega' }, +]; + +export async function getStream(args) { + const { id, s, e } = args || {}; + if (!id) return null; + + const isTv = s != null && e != null; + const path = isTv + ? `/api/vidora/v1/tv/${id}/${s}/${e}` + : `/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: '*/*', + 'Accept-Language': 'en-US,en;q=0.9', + Referer: referer, + Origin: BASE_URL, + 'x-player-key': PLAYER_KEY, + }; + + const responses = await Promise.allSettled( + SERVERS.map(async (server) => { + const data = await fetchJson(`${BASE_URL}${path}?source=${server.key}`, { + headers, + signal: AbortSignal.timeout(args?.config?.timeout || 25000), + }); + return { server, data }; + }) + ); + + const allUrls = []; + const seen = new Set(); + + for (const res of responses) { + if (res.status !== 'fulfilled' || !res.value?.data?.result) continue; + const { server, data } = res.value; + const sources = Array.isArray(data.sources) ? data.sources : []; + + for (const sourceItem of sources) { + if (Array.isArray(sourceItem.qualities) && sourceItem.qualities.length > 0) { + for (const q of sourceItem.qualities) { + if (!q?.url || seen.has(q.url)) continue; + seen.add(q.url); + const qLabel = q.height ? `${q.height}p` : 'Auto'; + allUrls.push({ + url: q.url, + server: `VidFlix - ${server.label} (${qLabel})`, + quality: qLabel, + type: q.url.includes('.mpd') ? 'dash' : (q.url.includes('.m3u8') ? 'hls' : 'mp4'), + headers: PLAYBACK_HEADERS, + }); + } + } + + const streamUrl = sourceItem.url; + if (streamUrl && typeof streamUrl === 'string' && streamUrl.startsWith('http') && !seen.has(streamUrl)) { + seen.add(streamUrl); + allUrls.push({ + url: streamUrl, + server: `VidFlix - ${server.label}`, + quality: 'Auto', + type: streamUrl.includes('.mpd') ? 'dash' : (streamUrl.includes('.m3u8') ? 'hls' : 'mp4'), + headers: PLAYBACK_HEADERS, + }); + } + } + } + + 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/vidlux.js b/src/sources/vidlux.js new file mode 100644 index 0000000..d46d15e --- /dev/null +++ b/src/sources/vidlux.js @@ -0,0 +1,156 @@ +import crypto from 'node:crypto'; +import { fetchText, USER_AGENT } from '../utils/helpers.js'; + +const BASE_URL = 'https://vidlux.xyz'; +const ENCRYPTION_KEY = 'vidlux-stream-encryption-2026-secure-key'; + +const PLAYBACK_HEADERS = { + 'User-Agent': USER_AGENT, + Referer: `${BASE_URL}/`, + Origin: BASE_URL, +}; + +const SERVERS = [ + { id: 'robot', name: 'Robot' }, + { id: 'titan', name: 'Titan' }, + { id: 'spider', name: 'Spider' }, + { id: 'dragon', name: 'Dragon' }, + { id: 'quasar', name: 'Quasar' }, + { id: 'vidfast', name: 'Flux' }, + { id: 'rocket', name: 'Aether' }, + { id: 'star', name: 'Star' }, + { id: 'bolt', name: 'Bolt' }, + { id: 'quilox', name: 'Quilox' }, + { id: 'vidrock', name: 'Nova' }, + { id: 'dubai', name: 'Dubai' }, + { id: 'magic', name: 'Magic' }, + { id: 'vixsrc', name: 'Aura' }, + { id: 'astra', name: 'Astra' }, +]; + +function decryptPayload(ciphertextB64) { + try { + const key = crypto.createHash('sha256').update(ENCRYPTION_KEY).digest(); + const raw = Buffer.from(ciphertextB64, 'base64'); + const iv = raw.subarray(0, 12); + const data = raw.subarray(12); + const authTag = data.subarray(data.length - 16); + const encrypted = data.subarray(0, data.length - 16); + const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv); + decipher.setAuthTag(authTag); + const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]); + return JSON.parse(decrypted.toString('utf8')); + } catch { + return null; + } +} + +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 embedUrl = isTv + ? `${BASE_URL}/embed/tv/${id}/${s}/${e}` + : `${BASE_URL}/embed/movie/${id}`; + + let token = null; + try { + const html = await fetchText(embedUrl, { + headers: { + 'User-Agent': USER_AGENT, + Referer: `${BASE_URL}/`, + }, + signal: AbortSignal.timeout(8000), + }); + const match = html.match(/(\d{13}\.[a-f0-9]{8}\.[a-f0-9]{64})/i) + || html.match(/requestToken["\\'\s:]+([a-zA-Z0-9._-]+)/i); + if (match) token = match[1]; + } catch { } + + if (!token) return null; + + const queryParams = new URLSearchParams({ + id: String(id), + type, + _t: token, + }); + + if (isTv) { + queryParams.set('season', String(s)); + queryParams.set('episode', String(e)); + } + + const allUrls = []; + const seen = new Set(); + + for (const server of SERVERS) { + try { + const extractUrl = `${BASE_URL}/api/extract/${server.id}?${queryParams.toString()}`; + const res = await fetch(extractUrl, { + headers: { + Accept: '*/*', + 'Accept-Language': 'en-US,en;q=0.9', + 'User-Agent': USER_AGENT, + Referer: embedUrl, + Origin: BASE_URL, + }, + signal: AbortSignal.timeout(10000), + }); + + if (!res.ok) continue; + + const json = await res.json().catch(() => null); + if (!json) continue; + + let data = json; + if (json.encrypted && json.data) { + data = decryptPayload(json.data); + } + + if (!data) continue; + + const streams = Array.isArray(data.streams) + ? data.streams + : (Array.isArray(data.sources) ? data.sources : []); + + for (const item of streams) { + let streamUrl = item?.file || item?.url; + if (!streamUrl || typeof streamUrl !== 'string') continue; + + if (streamUrl.startsWith('/')) { + streamUrl = `${BASE_URL}${streamUrl}`; + } + + if (!streamUrl.startsWith('http') || seen.has(streamUrl)) continue; + seen.add(streamUrl); + + let quality = item.quality || 'Auto'; + if (/^\d+$/.test(String(quality).trim())) { + quality = `${quality}p`; + } + + const streamType = item.type || (streamUrl.includes('.mpd') ? 'dash' : (streamUrl.includes('.m3u8') ? 'hls' : 'mp4')); + + allUrls.push({ + url: streamUrl, + server: `VidLux - ${server.name}`, + quality, + type: streamType, + headers: PLAYBACK_HEADERS, + }); + } + + if (allUrls.length > 0) break; + } catch { } + } + + if (!allUrls.length) return null; + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; +} \ No newline at end of file