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, }; }