From 4eda9819ce851728912a07e48b88269c13d4aa0f Mon Sep 17 00:00:00 2001 From: alwaysmesmerizingyou Date: Sat, 7 Nov 2026 20:43:08 -0800 Subject: [PATCH] added allaboutmovies, added cinebros, added movienerds, added netplayz, added taroflix, added vidspark --- src/config.js | 102 +++++++++++- src/sources/allaboutmovies.js | 81 ++++++++++ src/sources/cinebros.js | 159 ++++++++++++++++++ src/sources/movienerds.js | 111 +++++++++++++ src/sources/netplayz.js | 149 +++++++++++++++++ src/sources/taroflix.js | 297 ++++++++++++++++++++++++++++++++++ src/sources/vidspark.js | 139 ++++++++++++++++ 7 files changed, 1037 insertions(+), 1 deletion(-) create mode 100644 src/sources/allaboutmovies.js create mode 100644 src/sources/cinebros.js create mode 100644 src/sources/movienerds.js create mode 100644 src/sources/netplayz.js create mode 100644 src/sources/taroflix.js create mode 100644 src/sources/vidspark.js diff --git a/src/config.js b/src/config.js index 03a5979..1ce50e0 100644 --- a/src/config.js +++ b/src/config.js @@ -89,6 +89,24 @@ export const SOURCES = [ }], }, + { + key: 'allaboutmovies', + label: 'AllAboutMovies', + sourceFile: 'allaboutmovies', + proxyParam: 'aam', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + cdnHeaders: [{ + pattern: /lorenzo-rapattoni-pc\.workers\.dev/i, + headers: { + Referer: 'https://allaboutmovies.pages.dev/', + Origin: 'https://allaboutmovies.pages.dev', + }, + }], + }, + { key: 'anizone', label: 'AniZone', @@ -154,6 +172,24 @@ export const SOURCES = [ multiUrl: true, }, + { + key: 'cinebros', + label: 'CineBros', + sourceFile: 'cinebros', + proxyParam: 'cb', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + cdnHeaders: [{ + pattern: /cinebros\.net|medialink\.[a-z0-9-]+\.workers\.dev/i, + headers: { + Referer: 'https://cinebros.net/', + Origin: 'https://cinebros.net', + }, + }], + }, + { key: 'cinemaarmy', label: 'CinemaArmy', @@ -437,6 +473,17 @@ export const SOURCES = [ }], }, + { + key: 'movienerds', + label: 'MovieNerds', + sourceFile: 'movienerds', + proxyParam: 'mvi', + timeout: 35000, + jitter: 500, + retries: 2, + multiUrl: true, + }, + { key: 'movy', label: 'Movy', @@ -466,6 +513,24 @@ export const SOURCES = [ multiUrl: true, }, + { + key: 'netplayz', + label: 'NetPlayz', + sourceFile: 'netplayz', + proxyParam: 'npz', + timeout: 30000, + jitter: 500, + retries: 2, + multiUrl: true, + cdnHeaders: [{ + pattern: /fcxmb\.com|hscow\.com/i, + headers: { + Referer: 'https://netplayz.icu/', + Origin: 'https://netplayz.icu', + }, + }], + }, + { key: 'nexa', label: 'Nexa', @@ -515,7 +580,6 @@ export const SOURCES = [ timeout: 20000, jitter: 500, retries: 2, - skipProxy: true }, { @@ -547,6 +611,24 @@ export const SOURCES = [ }], }, + { + key: 'taroflix', + label: 'TaroFlix', + sourceFile: 'taroflix', + proxyParam: 'tf', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + cdnHeaders: [{ + pattern: /starkio\.cyou|taroflix\.net|workers\.dev/i, + headers: { + Referer: 'https://taroflix.net/', + Origin: 'https://taroflix.net', + }, + }], + }, + { key: 'streamaggregator', label: 'StreamAggregator', @@ -706,6 +788,24 @@ export const SOURCES = [ }], }, + { + key: 'vidspark', + label: 'VidSpark', + sourceFile: 'vidspark', + proxyParam: 'vsp', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + cdnHeaders: [{ + pattern: /vidspark\.to|paretocdn\.site|netrocdn\.site/i, + headers: { + Referer: 'https://vidspark.to/', + Origin: 'https://vidspark.to', + }, + }], + }, + { key: 'vidup', label: 'VidUp', diff --git a/src/sources/allaboutmovies.js b/src/sources/allaboutmovies.js new file mode 100644 index 0000000..8776018 --- /dev/null +++ b/src/sources/allaboutmovies.js @@ -0,0 +1,81 @@ +import { USER_AGENT } from '../utils/helpers.js'; + +const BASE_URL = 'https://allaboutmovies.pages.dev'; + +const API_HEADERS = { + Accept: 'application/json', + 'User-Agent': USER_AGENT, + Referer: `${BASE_URL}/`, +}; + +const PLAYBACK_HEADERS = { + 'User-Agent': USER_AGENT, +}; + +function buildApiUrl(id, s, e) { + if (s != null && e != null) { + return `${BASE_URL}/api/scrape/tv/${id}/${s}/${e}`; + } + return `${BASE_URL}/api/scrape/movie/${id}`; +} + +function normalizeQuality(q) { + if (!q || q === 'Auto') return 'Auto'; + const str = String(q).trim(); + return /^\d+$/.test(str) ? `${str}p` : str; +} + +export async function getStream(args) { + const { id, s, e, config } = args || {}; + if (!id) return null; + + const url = buildApiUrl(id, s, e); + + try { + const res = await fetch(url, { + headers: API_HEADERS, + signal: AbortSignal.timeout(config?.timeout || 25000), + }); + + if (!res.ok) return null; + + const data = await res.json(); + if (!data || !Array.isArray(data.servers)) return null; + + const readyServers = data.servers.filter( + srv => srv.status === 'ready' && typeof srv.url === 'string' && srv.url.startsWith('http') + ); + + if (!readyServers.length) return null; + + const allUrls = []; + const seen = new Set(); + + for (const srv of readyServers) { + if (seen.has(srv.url)) continue; + seen.add(srv.url); + + const quality = normalizeQuality(srv.quality); + const type = srv.type || (srv.url.includes('.m3u8') ? 'hls' : srv.url.includes('.mpd') ? 'dash' : 'mp4'); + const serverName = srv.name || srv.key || 'Server'; + + allUrls.push({ + url: srv.url, + server: `AllAboutMovies - ${serverName}`, + quality, + type, + headers: PLAYBACK_HEADERS, + }); + } + + 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/cinebros.js b/src/sources/cinebros.js new file mode 100644 index 0000000..1b54d24 --- /dev/null +++ b/src/sources/cinebros.js @@ -0,0 +1,159 @@ +import { USER_AGENT, getTmdbMetadata, resolveTmdbKey } from '../utils/helpers.js'; + +const BASE_URL = 'https://cinebros.net'; + +const HEADERS = { + 'User-Agent': USER_AGENT, + Accept: '*/*', + 'Accept-Language': 'en-US,en;q=0.9', + Origin: BASE_URL, + Referer: `${BASE_URL}/`, +}; + +const PLAYBACK_HEADERS = { + 'User-Agent': USER_AGENT, + Referer: `${BASE_URL}/`, + Origin: BASE_URL, +}; + +function parseJsonStream(text) { + if (!text) return []; + const items = []; + let depth = 0; + let inString = false; + let escape = false; + let startIndex = -1; + + for (let i = 0; i < text.length; i++) { + const char = text[i]; + if (escape) { + escape = false; + continue; + } + if (char === '\\') { + escape = true; + continue; + } + if (char === '"') { + inString = !inString; + continue; + } + if (!inString) { + if (char === '{') { + if (depth === 0) startIndex = i; + depth++; + } else if (char === '}') { + depth--; + if (depth === 0 && startIndex !== -1) { + const chunk = text.slice(startIndex, i + 1); + try { + items.push(JSON.parse(chunk)); + } catch { } + startIndex = -1; + } + } + } + } + return items; +} + +function getStreamScore(item) { + let score = 0; + const qMatch = String(item.quality || '').match(/\d+/); + if (qMatch) score += parseInt(qMatch[0], 10); + if (item.url && item.url.includes('/stream/')) score += 10000; + if (item.url && item.url.includes('blob=')) score -= 5000; + return score; +} + +export async function getStream(args) { + const { id, s, e, sdk } = args || {}; + if (!id) return null; + + const isTv = s != null && e != null; + const apiKey = resolveTmdbKey(args, sdk); + const meta = await getTmdbMetadata(id, isTv, apiKey).catch(() => null); + + const title = args?.title || meta?.title || ''; + const year = args?.year || meta?.year || ''; + const runtime = args?.runtime || meta?.runtime || (isTv ? '24' : '90'); + + const params = new URLSearchParams({ + tmdbId: String(id), + type: isTv ? 'tv' : 'movie', + title: title || String(id), + }); + + if (isTv) { + params.set('seasonId', String(s || 1)); + params.set('episodeId', String(e || 1)); + } + + if (year) params.set('year', String(year)); + if (runtime) params.set('runtime', String(runtime)); + + const referer = isTv + ? `${BASE_URL}/watch/tv/${id}/${s || 1}/${e || 1}` + : `${BASE_URL}/watch/movie/${id}`; + + const targetUrl = `${BASE_URL}/api/streams?${params.toString()}`; + + try { + const res = await fetch(targetUrl, { + headers: { + ...HEADERS, + Referer: referer, + }, + signal: AbortSignal.timeout(args?.config?.timeout || 25000), + }); + + if (!res.ok) return null; + const text = await res.text(); + const items = parseJsonStream(text); + + const allUrls = []; + const seen = new Set(); + + for (const item of items) { + const streamUrl = item?.url; + if (!streamUrl || typeof streamUrl !== 'string' || !streamUrl.startsWith('http')) { + continue; + } + if (seen.has(streamUrl)) continue; + seen.add(streamUrl); + + let quality = 'Auto'; + if (item.quality) { + const qStr = String(item.quality).trim(); + quality = /^\d+$/.test(qStr) ? `${qStr}p` : qStr; + } + + const serverLabel = item.name ? `CineBros - ${item.name}` : `CineBros (${quality})`; + const type = item.type || (streamUrl.includes('.m3u8') || streamUrl.includes('/stream/') + ? 'hls' + : streamUrl.includes('.mpd') + ? 'dash' + : 'mp4'); + + allUrls.push({ + url: streamUrl, + server: serverLabel, + quality, + type, + headers: PLAYBACK_HEADERS, + }); + } + + if (!allUrls.length) return null; + + allUrls.sort((a, b) => getStreamScore(b) - getStreamScore(a)); + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; + } catch { + return null; + } +} \ No newline at end of file diff --git a/src/sources/movienerds.js b/src/sources/movienerds.js new file mode 100644 index 0000000..54830c9 --- /dev/null +++ b/src/sources/movienerds.js @@ -0,0 +1,111 @@ +import { USER_AGENT } from '../utils/helpers.js'; + +const API_BASE = 'https://api.movienerds.online'; +const STREAM_ORIGIN = 'https://vidrock.net'; + +const API_HEADERS = { + Accept: 'application/json', + 'x-client-app': 'movienerds', + 'x-app-key': 'mn_sec_2026_stream_auth', +}; + +const PLAYBACK_HEADERS = { + 'User-Agent': USER_AGENT, + Referer: `${STREAM_ORIGIN}/`, + Origin: STREAM_ORIGIN, +}; + +const BAD_PATTERNS = [ + 'apopheniaatlas.site', 'lifestylefreedomlab.site', 'aoneroom.com/other', + '/other/202', 'mb=1', 'b164fbfb4347792950bdfbfb563d39d9', 'dummy', + 'teaser', 'trailer', 'promo', 'sample', 'fastcloud.live', 'bigtits', + 'tiktokcdn.com', 'tiktokcdn', 'tos-alisg', 'tos-maliva', 'byteoversea', + 'ibytedtos', 'ibyteimg', 'bytedance', 'staticreverie.site', 'staticreverie', +]; + +function isBadUrl(url) { + if (!url) return true; + const lower = String(url).toLowerCase(); + let decoded = ''; + try { decoded = decodeURIComponent(lower); } catch { } + return BAD_PATTERNS.some(p => lower.includes(p) || decoded.includes(p)); +} + +function isValidSource(src) { + if (!src || !src.url) return false; + const url = String(src.url).toLowerCase(); + const rawUrl = String(src.rawUrl || '').toLowerCase(); + if (url.includes('unavailable') || rawUrl.includes('unavailable')) return false; + if (!url.startsWith('http://') && !url.startsWith('https://')) return false; + return !isBadUrl(url) && !isBadUrl(rawUrl); +} + +export async function getStream(args) { + const { id, s, e, config } = args || {}; + if (!id) return null; + + const isTv = s != null && e != null; + const params = new URLSearchParams({ + tmdbId: String(id), + type: isTv ? 'tv' : 'movie', + lang: 'en', + }); + + if (isTv) { + params.set('season', String(s)); + params.set('episode', String(e)); + } + + try { + const res = await fetch(`${API_BASE}/api/stream?${params.toString()}`, { + headers: API_HEADERS, + signal: AbortSignal.timeout(config?.timeout || 35000), + }); + + if (!res.ok) return null; + + const data = await res.json(); + const sources = Array.isArray(data?.sources) ? data.sources.filter(isValidSource) : []; + + const direct = sources.filter(src => src.type !== 'embed' && src.isDirect !== false); + const embed = sources.filter(src => src.type === 'embed' || src.isDirect === false); + const sorted = direct.length ? [...direct, ...embed] : embed; + + if (!sorted.length) return null; + + const allUrls = []; + const seen = new Set(); + + for (const src of sorted) { + if (seen.has(src.url)) continue; + seen.add(src.url); + + const rawUrl = String(src.url); + const type = src.type === 'embed' + ? 'hls' + : (rawUrl.includes('.m3u8') ? 'hls' : (rawUrl.includes('.mpd') ? 'dash' : 'mp4')); + const quality = src.quality + ? (/^\d+$/.test(String(src.quality)) ? `${src.quality}p` : String(src.quality)) + : 'Auto'; + const serverName = src.server || src.name || 'Direct'; + + allUrls.push({ + url: rawUrl, + server: `MovieNerds - ${serverName}`, + quality, + type, + headers: PLAYBACK_HEADERS, + }); + } + + 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/netplayz.js b/src/sources/netplayz.js new file mode 100644 index 0000000..fd5babd --- /dev/null +++ b/src/sources/netplayz.js @@ -0,0 +1,149 @@ +import crypto from 'node:crypto'; +import { USER_AGENT, getTmdbMetadata, resolveTmdbKey } from '../utils/helpers.js'; + +const API_BASE = 'https://api.hwnou.com'; +const KEY = Buffer.from('fJATm*kgfJT!BgJB', 'utf8'); +const OKHTTP_UA = 'okhttp/3.2.0'; +const APK_SIGN_KEY = 'ED0955EB04E67A1D9F3305B95454FED485261475'; + +const STREAM_HEADERS = { + 'User-Agent': USER_AGENT, + Referer: 'https://thugtv.online/', + Origin: 'https://thugtv.online', +}; + +function decrypt(raw) { + const decipher = crypto.createDecipheriv('aes-128-cbc', KEY, KEY); + const decoded = Buffer.from(raw, 'base64'); + return Buffer.concat([decipher.update(decoded), decipher.final()]).toString('utf8'); +} + +function parseDecrypted(raw) { + const text = decrypt(raw).trim(); + const quoted = text.replace(/(?<=[:\[,]\s*)(\d{16,})(?=\s*[,\]}])/g, '"$1"'); + return JSON.parse(quoted); +} + +async function hwnouFetch(url, { method = 'GET', body = null, signal } = {}) { + try { + const headers = { + Accept: '*/*', + 'Accept-Language': 'en-US,en;q=0.9', + 'User-Agent': OKHTTP_UA, + }; + if (body) headers['Content-Type'] = 'application/json;charset=utf-8'; + const res = await fetch(url, { method, body, headers, signal }); + if (!res.ok) return null; + const text = await res.text(); + if (!text) return null; + return parseDecrypted(text); + } catch { + return null; + } +} + +function pickBestVideo(ep) { + const videos = (ep?.videos || []).filter(v => v && v.premiumProPermission === false); + if (!videos.length) return (ep?.videos || [])[0] || null; + videos.sort((a, b) => (b.resolution || 0) - (a.resolution || 0)); + return videos[0]; +} + +function pickLanguageId(ep) { + const tracks = ep?.tracks || []; + const pref = tracks.find(t => t && t.languageId === 1003); + return (pref || tracks[0] || {}).languageId || null; +} + +function normalizeTitle(title) { + return String(title).toLowerCase().replace(/ /g, '+'); +} + +export async function getStream(args) { + const { id, s, e, sdk, signal } = args || {}; + if (!id) return null; + + const isTv = s != null && e != null; + const episodeNum = isTv ? (parseInt(e, 10) || 1) : 1; + + let title = args?.title; + if (!title) { + try { + const meta = await getTmdbMetadata(id, isTv, resolveTmdbKey(args, sdk)); + title = meta?.title; + } catch { } + } + if (!title) return null; + + try { + const searchUrl = `${API_BASE}/film-api/v2.1.2/movie/searchByKeyword?keyword=${encodeURIComponent(normalizeTitle(title))}&page=1&size=20&mode=1&secure=0&channel=IndiaA&appMarket=GuanWang&packageName=com.external.castle&clientType=1`; + const searchData = await hwnouFetch(searchUrl, { signal }); + const rows = searchData?.data?.rows || []; + if (!rows.length) return null; + + const wantMovie = !isTv; + const candidates = rows.filter(r => (r.movieType === 2) === wantMovie); + if (!candidates.length) return null; + + const exact = candidates.find(r => String(r.title || '').toLowerCase() === String(title).toLowerCase()); + const match = exact || candidates[0]; + const movieId = match.id; + if (!movieId) return null; + + const detailsUrl = `${API_BASE}/film-api/v2.1.2/movie?movieId=${movieId}&packageName=com.external.castle&clientType=1`; + const detailsData = await hwnouFetch(detailsUrl, { signal }); + const episodes = detailsData?.data?.episodes || []; + if (!episodes.length) return null; + + const ep = isTv + ? (episodes.find(x => Number(x.number) === episodeNum) || episodes[0]) + : episodes[0]; + + const video = pickBestVideo(ep); + if (!video) return null; + + const languageId = pickLanguageId(ep); + + const streamApiUrl = `${API_BASE}/film-api/v2.0.7/movie/getVideo2?clientType=1&packageName=PKG&channel=CHANNEL&lang=en-US`; + const streamBody = JSON.stringify({ + appMarket: 'GuanWang', + woolUser: 'false', + isp: 'Android', + useVipCdn: 'false', + movieId, + episodeId: ep.id, + isNewUser: 'true', + resolution: video.resolution, + mode: '1', + clientType: '1', + apkSignKey: APK_SIGN_KEY, + androidVersion: '11', + networkType: 'NETWORK_WIFI', + packageName: 'com.external.castle', + languageId, + }); + + const streamData = await hwnouFetch(streamApiUrl, { method: 'POST', body: streamBody, signal }); + const videoUrl = streamData?.data?.videoUrl; + if (!videoUrl) return null; + + const quality = video.resolutionDescription || (video.resolution ? `${video.resolution}p` : 'Auto'); + const type = videoUrl.includes('.m3u8') ? 'hls' : (videoUrl.includes('.mpd') ? 'dash' : 'mp4'); + + const allUrls = [{ + url: videoUrl, + server: `NetPlayz - ${quality}`, + quality, + type, + headers: STREAM_HEADERS, + }]; + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; + } catch { + return null; + } +} \ No newline at end of file diff --git a/src/sources/taroflix.js b/src/sources/taroflix.js new file mode 100644 index 0000000..2129cef --- /dev/null +++ b/src/sources/taroflix.js @@ -0,0 +1,297 @@ +import crypto from 'node:crypto'; +import { USER_AGENT } from '../utils/helpers.js'; + +const TARO_ORIGIN = 'https://taroflix.net'; +const BACKEND_BASE = 'https://starkio.cyou'; + +const FALLBACK_SCHEME = { + v: 1, + salt: 'c1n3t4r-0bf5c4t10n-s4lt-v1-2024-09', + xk: 'c1n3t4r-0uter-xk-v1', + transform: 'none', + mode: 'on', +}; + +const HEADERS = { + 'User-Agent': USER_AGENT, + Accept: '*/*', + 'Accept-Language': 'en-US,en;q=0.9', + Referer: `${BACKEND_BASE}/`, + Origin: BACKEND_BASE, +}; + +const PLAYBACK_HEADERS = { + 'User-Agent': USER_AGENT, + Referer: `${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 = [ + { name: 'Hemlic [Multi]', responseType: 'vidly' }, + { name: 'Siddle', responseType: 'vidly' }, + { name: 'Tyler', responseType: 'vidlink' }, + { name: 'Cipher [Multi]', responseType: 'cipher' }, + { name: 'Eternal', responseType: 'xpass' }, + { name: 'Posiden', responseType: 'qualities', quality: '1080p' }, + { name: 'Helix', responseType: 'sources', quality: '1080p' }, + { name: 'Rimora', responseType: 'vidnest', quality: '1080p' }, +]; + +let wasmExports = null; +let schemeCache = null; +let schemeAt = 0; + +async function getScheme(force = false) { + if (!force && schemeCache && Date.now() - schemeAt < 120000) { + return schemeCache; + } + try { + const res = await fetch(`${BACKEND_BASE}/api/scheme`, { + headers: HEADERS, + signal: AbortSignal.timeout(5000), + }); + if (res.ok) { + const data = await res.json(); + if (data && typeof data.v === 'number') { + schemeCache = data; + schemeAt = Date.now(); + return schemeCache; + } + } + } catch { } + schemeCache = FALLBACK_SCHEME; + schemeAt = Date.now(); + return schemeCache; +} + +async function loadWasm() { + if (wasmExports) return wasmExports; + const res = await fetch(`${BACKEND_BASE}/decrypt.wasm`, { + headers: HEADERS, + signal: AbortSignal.timeout(8000), + }); + if (!res.ok) throw new Error(`Failed to load decrypt.wasm: ${res.status}`); + const bytes = await res.arrayBuffer(); + const env = { abort: () => { } }; + const { instance } = await WebAssembly.instantiate(bytes, { env }); + wasmExports = instance.exports; + return wasmExports; +} + +function deriveKey(pub, sch) { + const basis = (sch.v >= 2) ? `${pub}|${sch.v}|${sch.salt}` : `${pub}${sch.salt}`; + return new Uint8Array(crypto.createHash('sha256').update(basis, 'utf8').digest()); +} + +function untransform(out, pub, sch) { + if (sch.transform === 'reverse') { + return out.slice().reverse(); + } + if (sch.transform === 'xor2') { + const basis = `${sch.xk}|${sch.v}|${pub}`; + const k2 = new Uint8Array(crypto.createHash('sha256').update(basis, 'utf8').digest()); + const copy = new Uint8Array(out); + for (let i = 0; i < copy.length; i++) { + copy[i] ^= k2[i % 32]; + } + return copy; + } + return out; +} + +async function decWith(raw, pub, sch) { + const [key, exports] = await Promise.all([ + deriveKey(pub, sch), + loadWasm(), + ]); + + const { memory, allocBuffer, freeBuffer, normalizeBuffer } = exports; + const binary = Buffer.from(raw.enc, 'base64'); + const encBytes = new Uint8Array(binary); + + const encPtr = allocBuffer(encBytes.length); + const keyPtr = allocBuffer(32); + const outPtr = allocBuffer(encBytes.length); + + try { + let view = new Uint8Array(memory.buffer); + view.set(encBytes, encPtr); + view.set(key, keyPtr); + + const len = normalizeBuffer(encPtr, encBytes.length, keyPtr, outPtr); + view = new Uint8Array(memory.buffer); + const out = new Uint8Array(len); + for (let i = 0; i < len; i++) { + out[i] = view[outPtr + i]; + } + + const untransformed = untransform(out, pub, sch); + return JSON.parse(new TextDecoder().decode(untransformed)); + } finally { + freeBuffer(encPtr); + freeBuffer(keyPtr); + freeBuffer(outPtr); + } +} + +async function decryptResponse(raw, pub) { + if (!raw || !raw.enc) return raw; + try { + const sch = await getScheme(); + if (sch.mode === 'kill') return null; + try { + return await decWith(raw, pub, sch); + } catch { + const fresh = await getScheme(true); + if (fresh && fresh.mode !== 'kill' && fresh.v !== sch.v) { + return await decWith(raw, pub, fresh); + } + return null; + } + } catch { + return null; + } +} + +function parseStreamData(data, srv) { + if (!data) return []; + const type = srv.responseType || 'stream'; + const serverName = srv.name || 'Default'; + const urls = []; + + const add = (url, quality = 'Auto', isPrimary = false) => { + if (!url || typeof url !== 'string' || !url.startsWith('http')) return; + const cleanUrl = url.replace(/#\.mp4$/i, ''); + let qStr = String(quality || 'Auto').trim(); + if (/^\d+$/.test(qStr)) qStr = `${qStr}p`; + + urls.push({ + url: cleanUrl, + server: `TaroFlix - ${serverName}`, + quality: qStr, + type: cleanUrl.includes('.m3u8') || cleanUrl.includes('workers.dev') ? 'hls' : (cleanUrl.includes('.mpd') ? 'dash' : 'mp4'), + headers: PLAYBACK_HEADERS, + isPrimary, + }); + }; + + // Vidly: Hemlic [Multi] and Siddle + if (type === 'vidly' && Array.isArray(data.streams)) { + for (const s of data.streams) { + if (s?.url) { + add(s.url, s.quality || s.label || 'Auto', true); + } + } + } else if (type === 'vidlink') { + // Tyler combined playlist + if (typeof data.combinedPlaylistUrl === 'string' && data.combinedPlaylistUrl) { + add(data.combinedPlaylistUrl, 'Auto', true); + } + if (data.stream?.playlist) { + add(data.stream.playlist, 'Auto', false); + } + } else if (type === 'cipher') { + const bigbang = (data.servers || []).find(s => s && s.name === 'BigBang'); + add(bigbang?.url || data.url); + } else if (type === 'xpass' && Array.isArray(data.sources)) { + const sel = data.sources.find(s => s.server === data.selectedServer || s.label === data.selectedServer) || data.sources[0]; + add(sel?.file); + } else if (type === 'qualities' && Array.isArray(data.qualities)) { + for (const q of data.qualities) { + add(q.url, q.quality); + } + } else if (type === 'sources' && Array.isArray(data.sources)) { + for (const s of data.sources) { + add(s.url, s.name || (s.resolution ? `${s.resolution}p` : 'Auto')); + } + } else if (type === 'lordflix') { + if (typeof data.playlist === 'string') add(data.playlist); + else if (Array.isArray(data.stream) && data.stream.length) { + const prim = data.stream.find(s => s.id === 'primary') || data.stream[0]; + add(prim?.playlist); + } + } else if (type === 'vidnest') { + const vd = data.data; + if (vd?.url) add(vd.url); + else if (Array.isArray(vd?.streams)) { + for (const s of vd.streams) { + add(s.url, s.resolution || s.language || 'Auto'); + } + } + } else { + const fallback = data.url || data.stream || data.streamUrl; + if (fallback) add(fallback); + } + + return urls; +} + +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; + if (item.server?.includes('Hemlic') || item.server?.includes('Siddle')) score += 15000; + if (item.server?.includes('Tyler')) score += 10000; + if (item.type === 'hls') score += 1000; + return score; +} + +export async function getStream(args) { + const { id, s, e } = args || {}; + if (!id) return null; + + const isTv = s != null && e != null; + const queryType = isTv ? `series&season=${s}&episode=${e}` : 'movie'; + const referer = isTv + ? `${BACKEND_BASE}/api/cinero/series/${id}/${s}/${e}` + : `${BACKEND_BASE}/api/cinero/movie/${id}`; + + const requestHeaders = { + ...HEADERS, + Referer: referer, + }; + + // Race top servers: Hemlic [Multi], Siddle, Tyler, Cipher, Eternal + const candidateBatch = ACTIVE_SERVERS.slice(0, 5); + + const settled = await Promise.allSettled( + candidateBatch.map(async srv => { + const apiUrl = `${BACKEND_BASE}/api/proxy?server=${encodeURIComponent(srv.name)}&tmdb=${id}&type=${queryType}`; + const res = await fetch(apiUrl, { + headers: requestHeaders, + signal: AbortSignal.timeout(10000), + }); + if (!res.ok) return null; + const raw = await res.json().catch(() => null); + if (!raw) return null; + const decrypted = await decryptResponse(raw, String(id)); + return parseStreamData(decrypted, srv); + }) + ); + + const allUrls = []; + const seen = new Set(); + + for (const res of settled) { + if (res.status !== 'fulfilled' || !Array.isArray(res.value)) continue; + for (const item of res.value) { + if (!item?.url || seen.has(item.url)) continue; + seen.add(item.url); + allUrls.push(item); + } + } + + if (!allUrls.length) return null; + + allUrls.sort((a, b) => scoreStream(b) - scoreStream(a)); + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; +} \ No newline at end of file diff --git a/src/sources/vidspark.js b/src/sources/vidspark.js new file mode 100644 index 0000000..c57ea9d --- /dev/null +++ b/src/sources/vidspark.js @@ -0,0 +1,139 @@ +import { USER_AGENT, fetchJson, fetchText } from '../utils/helpers.js'; + +const BASE_URL = 'https://vidspark.to'; +const PLAYER_KEY = '3a67e8866ae1d2bb9e81fe7f73315a56eb3bdf5e3e755c7554c8be6910aa6b13'; + +const PLAYBACK_HEADERS = { + 'User-Agent': USER_AGENT, + Referer: `${BASE_URL}/`, + Origin: BASE_URL, +}; + +async function resolveHlsVariants(masterUrl, headers) { + try { + const text = await fetchText(masterUrl, { + headers, + signal: AbortSignal.timeout(5000), + }); + if (!text || !text.includes('#EXTM3U') || !text.includes('#EXT-X-STREAM-INF')) { + return null; + } + + const baseUrl = masterUrl.substring(0, masterUrl.lastIndexOf('/') + 1); + const lines = text.split('\n').map(l => l.trim()); + const variants = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.startsWith('#EXT-X-STREAM-INF:')) { + let quality = 'Auto'; + const resMatch = line.match(/RESOLUTION=\d+x(\d+)/i); + if (resMatch) quality = `${resMatch[1]}p`; + + const nextLine = lines[i + 1]; + if (nextLine && !nextLine.startsWith('#')) { + const fullUrl = nextLine.startsWith('http') ? nextLine : `${baseUrl}${nextLine}`; + variants.push({ url: fullUrl, quality }); + } + } + } + + return variants.length ? variants : null; + } catch { + return null; + } +} + +function parseQualityNumber(q) { + const m = String(q).match(/\d+/); + return m ? parseInt(m[0], 10) : 0; +} + +export async function getStream(args) { + const { id, s, e } = args || {}; + if (!id) return null; + + const isTv = s != null && e != null; + const targetUrl = isTv + ? `${BASE_URL}/api/vidora/v1/tv/${id}/${s}/${e}` + : `${BASE_URL}/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: 'application/json, text/plain, */*', + 'Accept-Language': 'en-US,en;q=0.9', + Referer: referer, + Origin: BASE_URL, + 'x-player-key': PLAYER_KEY, + }; + + try { + const data = await fetchJson(targetUrl, { + headers, + signal: AbortSignal.timeout(args?.config?.timeout || 25000), + }); + + if (!data?.result || !Array.isArray(data.sources) || !data.sources.length) { + return null; + } + + const allUrls = []; + const seen = new Set(); + + for (const sourceItem of data.sources) { + const streamUrl = sourceItem?.url; + if (!streamUrl || typeof streamUrl !== 'string' || !streamUrl.startsWith('http')) { + continue; + } + + const serverLabel = sourceItem.source + ? sourceItem.source.charAt(0).toUpperCase() + sourceItem.source.slice(1) + : 'Direct'; + + if (streamUrl.includes('.m3u8')) { + const variants = await resolveHlsVariants(streamUrl, PLAYBACK_HEADERS); + if (variants && variants.length) { + for (const v of variants) { + if (seen.has(v.url)) continue; + seen.add(v.url); + + allUrls.push({ + url: v.url, + server: `VidSpark - ${serverLabel} (${v.quality})`, + quality: v.quality, + type: 'hls', + headers: PLAYBACK_HEADERS, + }); + } + } + } + + if (!seen.has(streamUrl)) { + seen.add(streamUrl); + allUrls.push({ + url: streamUrl, + server: `VidSpark - ${serverLabel}`, + quality: 'Auto', + type: streamUrl.includes('.m3u8') ? 'hls' : (streamUrl.includes('.mpd') ? 'dash' : 'mp4'), + headers: PLAYBACK_HEADERS, + }); + } + } + + if (!allUrls.length) return null; + + allUrls.sort((a, b) => parseQualityNumber(b.quality) - parseQualityNumber(a.quality)); + + return { + url: allUrls[0].url, + headers: allUrls[0].headers, + allUrls, + }; + } catch { + return null; + } +} \ No newline at end of file