diff --git a/src/config.js b/src/config.js index 4a7c483..e61de1d 100644 --- a/src/config.js +++ b/src/config.js @@ -182,25 +182,6 @@ export const SOURCES = [ }], }, - { - key: 'dulo', - label: 'Dulo', - sourceFile: 'dulo', - proxyParam: 'dl', - timeout: 25000, - jitter: 500, - retries: 2, - multiUrl: true, - skipProxy: true, - cdnHeaders: [{ - pattern: /dulo\.mov/i, - headers: { - Referer: 'https://dulo.mov/', - Origin: 'https://dulo.mov', - }, - }], - }, - { key: 'flaxmovies', label: 'FlaxMovies', @@ -220,6 +201,25 @@ export const SOURCES = [ }], }, + { + key: 'fmovies4u', + label: 'Fmovies4U', + sourceFile: 'fmovies4u', + proxyParam: 'fm4u', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + skipProxy: true, + cdnHeaders: [{ + pattern: /fmfau\.com/i, + headers: { + Referer: 'https://fmfau.com/', + Origin: 'https://fmfau.com', + }, + }], + }, + { key: 'framextv', label: 'FrameXTV', @@ -536,6 +536,25 @@ export const SOURCES = [ }], }, + { + key: 'torrentio', + label: 'Torrentio', + sourceFile: 'torrentio', + proxyParam: 'tio', + timeout: 30000, + jitter: 500, + retries: 1, + multiUrl: true, + skipProxy: true, + cdnHeaders: [{ + pattern: /torrentio\.to|fcxmb\.com/i, + headers: { + Referer: 'https://stream.vidplay.to/', + Origin: 'https://stream.vidplay.to', + }, + }], + }, + { key: 'vidapi', label: 'VidAPI', @@ -547,6 +566,25 @@ export const SOURCES = [ multiUrl: true }, + { + key: 'vidbolt', + label: 'VidBolt', + sourceFile: 'vidbolt', + proxyParam: 'vb', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + skipProxy: true, + cdnHeaders: [{ + pattern: /rousav\.tech|vidbolt\.xyz/i, + headers: { + Referer: 'https://vidbolt.xyz/', + Origin: 'https://vidbolt.xyz', + }, + }], + }, + { key: 'vidcore', label: 'VidCore', @@ -695,6 +733,25 @@ export const SOURCES = [ skipProxy: true, }, + { + key: 'willow', + label: 'Willow', + sourceFile: 'willow', + proxyParam: 'wl', + timeout: 25000, + jitter: 500, + retries: 2, + multiUrl: true, + skipProxy: true, + cdnHeaders: [{ + pattern: /hscow\.com|downloadwella\.com|willowmovies\.com/i, + headers: { + Referer: 'https://willowmovies.com/', + Origin: 'https://willowmovies.com', + }, + }], + }, + { key: 'xdownloader', label: 'XDownloader', @@ -744,25 +801,6 @@ export const SOURCES = [ multiUrl: true, }, - { - key: 'zenox', - label: 'Zenox', - sourceFile: 'zenox', - proxyParam: 'zx', - timeout: 25000, - jitter: 500, - retries: 2, - multiUrl: true, - skipProxy: true, - cdnHeaders: [{ - pattern: /zenox\.lol|hls-m3u8\.lol/i, - headers: { - Referer: 'https://zenox.lol/', - Origin: 'https://zenox.lol', - }, - }], - }, - ]; export const HEALTH_PROBE_ID = '155'; diff --git a/src/sources/dulo.js b/src/sources/dulo.js deleted file mode 100644 index 57fa86e..0000000 --- a/src/sources/dulo.js +++ /dev/null @@ -1,135 +0,0 @@ -import { getTmdbMetadata, getUA, resolveTmdbKey } from '../utils/helpers.js'; - -const BASE_URL = 'https://dulo.mov'; - -const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); - -async function fetchSource(url, headers) { - try { - const res = await fetch(url, { - headers, - signal: AbortSignal.timeout(8000), - }); - if (!res.ok) return null; - return await res.json(); - } catch { - return null; - } -} - -export async function getStream(args) { - const { id, s, e, sdk } = args || {}; - const isTv = s != null && e != null; - const mediaType = isTv ? 'tv' : 'movie'; - const apiKey = resolveTmdbKey(args, sdk); - const ua = getUA(); - - const meta = await getTmdbMetadata(id, isTv, apiKey); - const title = meta?.title || ''; - const year = meta?.year || ''; - const runtime = meta?.runtime || ''; - - const referer = isTv - ? `${BASE_URL}/watch/tv/${id}/${s}/${e}` - : `${BASE_URL}/watch/movie/${id}`; - - const defaultHeaders = { - 'User-Agent': ua, - Referer: referer, - Origin: BASE_URL, - Accept: '*/*', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'same-origin', - }; - - const baseParams = new URLSearchParams({ - tmdbId: String(id), - type: mediaType, - }); - - const additionalParams = new URLSearchParams({ - tmdbId: String(id), - type: mediaType, - progressive: 'true', - }); - - if (title) additionalParams.set('title', title); - if (year) additionalParams.set('year', String(year)); - if (runtime) additionalParams.set('runtime', String(runtime)); - - if (isTv) { - baseParams.set('season', String(s)); - baseParams.set('episode', String(e)); - additionalParams.set('season', String(s)); - additionalParams.set('episode', String(e)); - } - - const endpoints = [ - `${BASE_URL}/api/sources?${baseParams.toString()}`, - `${BASE_URL}/api/sources/willow?${baseParams.toString()}`, - `${BASE_URL}/api/sources/additional?${additionalParams.toString()}`, - ]; - - const seenUrls = new Set(); - const allUrls = []; - - let pendingUrls = [...endpoints]; - const maxPollTime = Date.now() + 16000; - - while (pendingUrls.length > 0 && Date.now() < maxPollTime) { - const results = await Promise.all( - pendingUrls.map(url => fetchSource(url, defaultHeaders)) - ); - - const nextPending = []; - - results.forEach((data, index) => { - if (!data) return; - - if (Array.isArray(data.sources) && data.sources.length > 0) { - for (const src of data.sources) { - let streamUrl = String(src?.url || '').trim(); - if (!streamUrl) continue; - - if (streamUrl.startsWith('/')) { - streamUrl = `${BASE_URL}${streamUrl}`; - } - - if (!streamUrl.startsWith('http') || seenUrls.has(streamUrl)) continue; - seenUrls.add(streamUrl); - - allUrls.push({ - url: streamUrl, - server: src.label || 'Dulo', - label: src.label || 'Dulo', - quality: 'Auto', - type: streamUrl.includes('.m3u8') ? 'hls' : 'mp4', - headers: { - 'User-Agent': ua, - Referer: referer, - }, - captions: Array.isArray(src.captions) ? src.captions : [], - }); - } - } - - if (data.pending) { - nextPending.push(pendingUrls[index]); - } - }); - - pendingUrls = nextPending; - if (pendingUrls.length > 0) { - await sleep(1500); - } - } - - if (allUrls.length === 0) return null; - - return { - url: allUrls[0].url, - headers: allUrls[0].headers, - allUrls, - }; -} \ No newline at end of file diff --git a/src/sources/fmovies4u.js b/src/sources/fmovies4u.js new file mode 100644 index 0000000..44a4b2d --- /dev/null +++ b/src/sources/fmovies4u.js @@ -0,0 +1,143 @@ +const BASE_URL = 'https://fmfau.com'; +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: `${BASE_URL}/`, + Origin: BASE_URL, + Accept: 'application/json, text/plain, */*', + 'Accept-Language': 'en-US,en;q=0.9', +}; + +const PLAYBACK_HEADERS = { + 'User-Agent': UA, + Referer: `${BASE_URL}/`, + Origin: BASE_URL, +}; + +const PROVIDERS = [ + 'cinetaro', + 'filmu', + 'streamingunity', + 'vaultplayer', + 'ernax', + 'phantomflix', + 'moovie', + 'watchflux', +]; + +function extractStreams(data, providerName) { + const list = []; + if (!data) return list; + + const rawList = Array.isArray(data) + ? data + : Array.isArray(data.sources) + ? data.sources + : Array.isArray(data.streams) + ? data.streams + : Array.isArray(data.data?.sources) + ? data.data.sources + : Array.isArray(data.result?.sources) + ? data.result.sources + : []; + + for (const item of rawList) { + 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`; + + const type = item.type || item.format || (streamUrl.includes('.m3u8') + ? 'hls' + : streamUrl.includes('.mpd') + ? 'dash' + : 'mp4'); + + const server = item.name || item.server || providerName; + + list.push({ + url: streamUrl, + server: `Fmovies4U - ${server}`, + quality, + type, + headers: PLAYBACK_HEADERS, + }); + } + + const singleUrl = data.url || data.file || data.src || data.stream?.url || data.stream; + if (typeof singleUrl === 'string' && singleUrl.startsWith('http')) { + const type = data.type || (singleUrl.includes('.m3u8') + ? 'hls' + : singleUrl.includes('.mpd') + ? 'dash' + : 'mp4'); + + list.push({ + url: singleUrl, + server: `Fmovies4U - ${providerName}`, + quality: data.quality || 'Auto', + type, + headers: PLAYBACK_HEADERS, + }); + } + + return list; +} + +export async function getStream(args) { + const { id, s, e, title, year, imdbId, audio } = args || {}; + if (!id) return null; + + const isTv = s != null && e != null; + const baseParams = new URLSearchParams({ + tmdbId: String(id), + mediaType: isTv ? 'tv' : 'movie', + title: String(title || ''), + year: String(year || ''), + imdbId: String(imdbId || ''), + audioPreference: audio || 'sub', + }); + + if (isTv) { + baseParams.set('season', String(s)); + baseParams.set('episode', String(e)); + baseParams.set('seasonTitle', `Season ${s}`); + } + + const responses = await Promise.allSettled( + PROVIDERS.map(async provider => { + const res = await fetch(`${BASE_URL}/api/sources/${provider}?${baseParams.toString()}`, { + headers: HEADERS, + signal: AbortSignal.timeout(10000), + }); + if (!res.ok) return null; + const data = await res.json().catch(() => null); + return { provider, data }; + }) + ); + + const allUrls = []; + const seen = new Set(); + + for (const r of responses) { + if (r.status !== 'fulfilled' || !r.value?.data) continue; + const provName = r.value.provider.charAt(0).toUpperCase() + r.value.provider.slice(1); + const extracted = extractStreams(r.value.data, provName); + + for (const item of extracted) { + 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/torrentio.js b/src/sources/torrentio.js new file mode 100644 index 0000000..5dea949 --- /dev/null +++ b/src/sources/torrentio.js @@ -0,0 +1,128 @@ +const BASE_URL = 'https://s.torrentio.to'; +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://stream.vidplay.to/', + Origin: 'https://stream.vidplay.to', + Accept: 'text/event-stream, */*', + 'Accept-Language': 'en-US,en;q=0.9', +}; + +const PLAYBACK_HEADERS = { + 'User-Agent': UA, + Referer: 'https://stream.vidplay.to/', + Origin: 'https://stream.vidplay.to', +}; + +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}/v1/sources/stream?type=tv&id=${id}&s=${s}&e=${e}` + : `${BASE_URL}/v1/sources/stream?type=movie&id=${id}`; + + try { + const res = await fetch(targetUrl, { + headers: HEADERS, + signal: AbortSignal.timeout(args?.config?.timeout || 25000), + }); + + if (!res.ok || !res.body) return null; + + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + const allUrls = []; + const seen = new Set(); + + outer: while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed === 'event: done') { + try { await reader.cancel(); } catch { } + break outer; + } + + if (trimmed.startsWith('data: ')) { + try { + const data = JSON.parse(trimmed.slice(6)); + if (Array.isArray(data?.sources)) { + for (const src of data.sources) { + if (!src?.url || seen.has(src.url)) continue; + seen.add(src.url); + + const provName = src.provider?.name || data.provider?.name || 'Server'; + const rawQ = src.quality ? String(src.quality).trim() : 'Auto'; + const quality = /^\d+$/i.test(rawQ) + ? `${rawQ}p` + : /^\d+p$/i.test(rawQ) + ? rawQ.toLowerCase() + : rawQ; + + const type = src.type || (src.url.includes('.m3u8') ? 'hls' : (src.url.includes('.mpd') ? 'dash' : 'mp4')); + + allUrls.push({ + url: src.url, + server: `Torrentio - ${provName}`, + quality, + type, + headers: PLAYBACK_HEADERS, + }); + } + } + } catch { } + } + } + } + + if (buffer.trim().startsWith('data: ')) { + try { + const data = JSON.parse(buffer.trim().slice(6)); + if (Array.isArray(data?.sources)) { + for (const src of data.sources) { + if (!src?.url || seen.has(src.url)) continue; + seen.add(src.url); + + const provName = src.provider?.name || data.provider?.name || 'Server'; + const rawQ = src.quality ? String(src.quality).trim() : 'Auto'; + const quality = /^\d+$/i.test(rawQ) + ? `${rawQ}p` + : /^\d+p$/i.test(rawQ) + ? rawQ.toLowerCase() + : rawQ; + + const type = src.type || (src.url.includes('.m3u8') ? 'hls' : (src.url.includes('.mpd') ? 'dash' : 'mp4')); + + allUrls.push({ + url: src.url, + server: `Torrentio - ${provName}`, + quality, + type, + headers: PLAYBACK_HEADERS, + }); + } + } + } catch { } + } + + 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/vidbolt.js b/src/sources/vidbolt.js new file mode 100644 index 0000000..04061ca --- /dev/null +++ b/src/sources/vidbolt.js @@ -0,0 +1,95 @@ +const BASE_URL = 'https://vidbolt.xyz'; +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: `${BASE_URL}/`, + Origin: BASE_URL, + Accept: 'application/json, text/plain, */*', + 'Accept-Language': 'en-US,en;q=0.9', +}; + +const PLAYBACK_HEADERS = { + 'User-Agent': UA, + Referer: `${BASE_URL}/`, + Origin: BASE_URL, +}; + +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/resolve?type=tv&id=${encodeURIComponent(id)}&season=${encodeURIComponent(s)}&episode=${encodeURIComponent(e)}` + : `${BASE_URL}/api/resolve?type=movie&id=${encodeURIComponent(id)}`; + + try { + const res = await fetch(targetUrl, { + headers: HEADERS, + signal: AbortSignal.timeout(args?.config?.timeout || 25000), + }); + + if (!res.ok) return null; + const data = await res.json().catch(() => null); + if (!data?.stream) return null; + + const serverName = data.stream.server + ? data.stream.server.charAt(0).toUpperCase() + data.stream.server.slice(1) + : 'Direct'; + + const allUrls = []; + const seen = new Set(); + + if (Array.isArray(data.stream.sources)) { + for (const item of data.stream.sources) { + if (!item?.url || seen.has(item.url)) continue; + seen.add(item.url); + + let quality = item.quality ? String(item.quality).trim() : 'Auto'; + if (/^\d+$/i.test(quality)) quality = `${quality}p`; + + const type = item.kind || (item.url.includes('.m3u8') + ? 'hls' + : item.url.includes('.mpd') + ? 'dash' + : 'mp4'); + + allUrls.push({ + url: item.url, + server: `VidBolt - ${serverName}`, + quality, + type, + headers: PLAYBACK_HEADERS, + }); + } + } + + if (data.stream.url && !seen.has(data.stream.url)) { + seen.add(data.stream.url); + const type = data.stream.kind || (data.stream.url.includes('.m3u8') + ? 'hls' + : data.stream.url.includes('.mpd') + ? 'dash' + : 'mp4'); + + allUrls.push({ + url: data.stream.url, + server: `VidBolt - ${serverName}`, + quality: data.stream.quality || 'Auto', + 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/willow.js b/src/sources/willow.js new file mode 100644 index 0000000..3848b7a --- /dev/null +++ b/src/sources/willow.js @@ -0,0 +1,139 @@ +const BASE_URL = 'https://willowmovies.com'; +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: `${BASE_URL}/`, + Origin: BASE_URL, + Accept: 'application/json, text/plain, */*', + 'Accept-Language': 'en-US,en;q=0.9', +}; + +const PLAYBACK_HEADERS = { + 'User-Agent': UA, + Referer: `${BASE_URL}/`, + Origin: BASE_URL, +}; + +function extractStreams(data, fallbackName) { + const list = []; + if (!data) return list; + + const serverName = data.source?.name || data.source?.id || fallbackName || 'Default'; + + if (Array.isArray(data.qualities) && data.qualities.length > 0) { + for (const item of data.qualities) { + if (!item?.url) continue; + + let quality = item.quality ? String(item.quality).trim() : 'Auto'; + if (quality === '0p') quality = 'Auto'; + else if (/^\d+$/i.test(quality)) quality = `${quality}p`; + + const type = item.type || (item.url.includes('.m3u8') + ? 'hls' + : item.url.includes('.mpd') + ? 'dash' + : item.url.includes('.mkv') + ? 'mkv' + : 'mp4'); + + list.push({ + url: item.url, + server: `Willow - ${serverName}`, + quality, + type, + headers: PLAYBACK_HEADERS, + }); + } + } + + if (data.src) { + const type = data.type || (data.src.includes('.m3u8') + ? 'hls' + : data.src.includes('.mpd') + ? 'dash' + : data.src.includes('.mkv') + ? 'mkv' + : 'mp4'); + + list.push({ + url: data.src, + server: `Willow - ${serverName}`, + quality: 'Auto', + type, + headers: PLAYBACK_HEADERS, + }); + } + + return list; +} + +export async function getStream(args) { + const { id, s, e } = args || {}; + if (!id) return null; + + const isTv = s != null && e != null; + const baseQuery = isTv + ? `id=${encodeURIComponent(id)}&type=tv&s=${encodeURIComponent(s)}&e=${encodeURIComponent(e)}` + : `id=${encodeURIComponent(id)}&type=movie`; + + try { + const initialRes = await fetch(`${BASE_URL}/api/stream?${baseQuery}`, { + headers: HEADERS, + signal: AbortSignal.timeout(10000), + }); + + if (!initialRes.ok) return null; + const initialData = await initialRes.json().catch(() => null); + if (!initialData) return null; + + const allUrls = []; + const seen = new Set(); + + const initialStreams = extractStreams(initialData); + for (const item of initialStreams) { + if (!item?.url || seen.has(item.url)) continue; + seen.add(item.url); + allUrls.push(item); + } + + const currentSourceId = initialData.source?.id; + const extraSources = Array.isArray(initialData.sources) + ? initialData.sources.filter(src => src?.id && src.id !== currentSourceId) + : []; + + if (extraSources.length > 0) { + const extraResults = await Promise.allSettled( + extraSources.map(async src => { + const res = await fetch(`${BASE_URL}/api/stream?${baseQuery}&source=${encodeURIComponent(src.id)}`, { + headers: HEADERS, + signal: AbortSignal.timeout(8000), + }); + if (!res.ok) return null; + const data = await res.json().catch(() => null); + return { src, data }; + }) + ); + + for (const r of extraResults) { + if (r.status !== 'fulfilled' || !r.value?.data) continue; + const streams = extractStreams(r.value.data, r.value.src.name || r.value.src.id); + for (const item of streams) { + 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, + }; + } catch { + return null; + } +} \ No newline at end of file diff --git a/src/sources/zenox.js b/src/sources/zenox.js deleted file mode 100644 index 7094432..0000000 --- a/src/sources/zenox.js +++ /dev/null @@ -1,108 +0,0 @@ -const BASE_URL = 'https://zenox.lol'; -const API_URL = `${BASE_URL}/api/v1/playback/session`; -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 = { - 'Content-Type': 'application/json', - Origin: BASE_URL, - Referer: `${BASE_URL}/`, - 'User-Agent': UA, -}; - -const PLAYBACK_HEADERS = { - Origin: BASE_URL, - Referer: `${BASE_URL}/`, -}; - -async function fetchServerStream(payload, srv) { - try { - const res = await fetch(API_URL, { - method: 'POST', - headers: HEADERS, - body: JSON.stringify({ - ...payload, - serverIndex: srv.index, - serverName: srv.name, - }), - signal: AbortSignal.timeout(6000), - }); - if (!res.ok) return null; - const data = await res.json(); - if ( - !data?.success || - !data?.streamUrl || - data.streamUrl.includes('/primary/') || - data.serverName?.toLowerCase() === 'primary' - ) { - return null; - } - - const streamUrl = data.streamUrl.startsWith('http') - ? data.streamUrl - : `${BASE_URL}${data.streamUrl}`; - - return { - url: streamUrl, - server: `Zenox - ${data.serverName || srv.name}`, - quality: 'Auto', - type: streamUrl.includes('.m3u8') ? 'hls' : (streamUrl.includes('.mpd') ? 'dash' : 'mp4'), - headers: PLAYBACK_HEADERS, - }; - } catch { - return null; - } -} - -export async function getStream(args) { - const { id, s, e, title, year, imdbId } = args || {}; - const idNum = parseInt(id, 10); - if (!idNum) return null; - - const isTv = s != null && e != null; - const payload = { - mediaId: idNum, - mediaType: isTv ? 'tv' : 'movie', - season: isTv ? (parseInt(s, 10) || 1) : 1, - episode: isTv ? (parseInt(e, 10) || 1) : 1, - title: title || '', - year: String(year || ''), - imdbId: imdbId || '', - autoUseCommunity: false, - isManualSwitch: true, - }; - - const activeServers = [ - { name: 'Vbot', index: 1 }, - { name: 'Temporary', index: 1 }, - ]; - - const results = await Promise.allSettled( - activeServers.map(srv => fetchServerStream(payload, srv)) - ); - - const allUrls = []; - const seen = new Set(); - - for (const r of results) { - if (r.status === 'fulfilled' && r.value?.url && !seen.has(r.value.url)) { - seen.add(r.value.url); - allUrls.push(r.value); - } - } - - if (!allUrls.length) { - const delta = await fetchServerStream(payload, { name: 'Delta', index: 0 }); - if (delta?.url && !seen.has(delta.url)) { - seen.add(delta.url); - allUrls.push(delta); - } - } - - if (!allUrls.length) return null; - - return { - url: allUrls[0].url, - headers: allUrls[0].headers, - allUrls, - }; -} \ No newline at end of file