diff --git a/README.md b/README.md index 2eec7c7..0372f48 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# [DMCA](https://docs.vyla.cc/misc/dmca) +# [DMCA](https://vyla.cc/misc/project/DMCA) If your source is listed here, I sincerely apologize. I understand that its inclusion may be costing you a lot of money, and that was never my intention. Please contact me as soon as possible, and I'll remove it ASAP. @@ -10,7 +10,7 @@ Original work and core authorship: [@alwaysmesmerizingyou](https://gitlab.com/alwaysmesmerizingyou) -[@GavinGoGaming](https://gitlab.com/GavinGoGaming) +[@GavinGoGaming](https://github.com/GavinGoGaming) Full credits and acknowledgements: -https://docs.vyla.cc/misc/credit \ No newline at end of file +https://vyla.cc/misc/project/credit \ No newline at end of file diff --git a/package.json b/package.json index 815f133..e7bde37 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "description": "Stream API — multi-provider stream scraper", "type": "module", "dependencies": { - "@vyla-entertainment/sdk": "^1.12.0", + "@vyla-entertainment/sdk": "latest", "dotenv": "^17.4.2", "latest": "^0.2.0" } diff --git a/server.js b/server.js index 69bab42..620cd7a 100644 --- a/server.js +++ b/server.js @@ -587,50 +587,104 @@ function applyCdnHeaders(cleanUrl, extraHeaders, sourceKey) { } } +async function getSdkStreams(sourceKey, id, s, e, clientIP) { + if (typeof sdk.getStreams === 'function') { + try { + const res = await sdk.getStreams(sourceKey, id, s, e, clientIP); + if (res && (Array.isArray(res) ? res.length > 0 : Object.keys(res).length > 0)) { + return res; + } + } catch { } + } + return sdk.getStream(sourceKey, id, s, e, clientIP); +} + function fetchSource(cfg, cacheKey, id, s, e, clientIP, absoluteBase) { function fixSource(url) { - if (url.includes('/api?url=')) { + if (typeof url === 'string' && url.includes('/api?url=')) { return `${absoluteBase}/api?url=${encodeURIComponent(url)}`; } return url; } function applyFix(result) { - if (result && Array.isArray(result.allUrls)) { - result.allUrls = result.allUrls.map(entry => ({ - ...entry, - url: fixSource(entry.url) - })); - } + if (!result) return result; + const fixEntry = item => { + if (typeof item === 'string') return fixSource(item); + if (item && typeof item === 'object') { + const target = item.url || item.file || item.streamUrl || item.link; + if (target) return { ...item, url: fixSource(target) }; + } + return item; + }; + + if (Array.isArray(result)) return result.map(fixEntry); + if (Array.isArray(result.streams)) result.streams = result.streams.map(fixEntry); + if (Array.isArray(result.allUrls)) result.allUrls = result.allUrls.map(fixEntry); + if (Array.isArray(result.sources)) result.sources = result.sources.map(fixEntry); + if (Array.isArray(result.urls)) result.urls = result.urls.map(fixEntry); + if (typeof result.url === 'string') result.url = fixSource(result.url); return result; } if (cfg.skipCache) { return withTimeout( - jitter(cfg.jitter).then(() => withRetry(() => sdk.getStream(cfg.key, id, s, e, clientIP), cfg.retries, 300)), + jitter(cfg.jitter).then(() => withRetry(() => getSdkStreams(cfg.key, id, s, e, clientIP), cfg.retries, 300)), cfg.timeout ).then(applyFix); } return withTimeout( jitter(cfg.jitter).then(() => - getSharedCached(`${cfg.key}-${cacheKey}`, () => withRetry(() => sdk.getStream(cfg.key, id, s, e, clientIP), cfg.retries, 300)) + getSharedCached(`${cfg.key}-${cacheKey}`, () => withRetry(() => getSdkStreams(cfg.key, id, s, e, clientIP), cfg.retries, 300)) ), cfg.timeout ).then(applyFix); } function normalizeCandidates(rawResult) { - let candidates = []; - if (rawResult?.allUrls?.length) { - candidates = rawResult.allUrls.map(u => typeof u === 'object' ? u : { url: u }); - } else if (rawResult?.streams?.length) { - candidates = rawResult.streams.map(u => typeof u === 'object' ? u : { url: u }); - } else if (Array.isArray(rawResult)) { - candidates = rawResult.map(u => typeof u === 'object' ? u : { url: u }); - } else if (rawResult) { - candidates = [{ url: typeof rawResult === 'object' ? rawResult.url : rawResult, headers: rawResult?.headers, skipProxy: rawResult?.skipProxy, skipHlsCheck: rawResult?.skipHlsCheck }]; + if (!rawResult) return []; + const pool = []; + + if (Array.isArray(rawResult)) { + pool.push(...rawResult); + } else if (typeof rawResult === 'object') { + if (Array.isArray(rawResult.streams)) pool.push(...rawResult.streams); + if (Array.isArray(rawResult.allUrls)) pool.push(...rawResult.allUrls); + if (Array.isArray(rawResult.sources)) pool.push(...rawResult.sources); + if (Array.isArray(rawResult.urls)) pool.push(...rawResult.urls); + if (rawResult.url) { + pool.push({ + url: rawResult.url, + headers: rawResult.headers, + quality: rawResult.quality, + type: rawResult.type, + name: rawResult.name, + label: rawResult.label, + language: rawResult.language + }); + } + } else if (typeof rawResult === 'string') { + pool.push({ url: rawResult }); } + + const seen = new Set(); + const candidates = []; + + for (const item of pool) { + if (!item) continue; + const candidate = typeof item === 'object' + ? { ...item, url: item.url || item.file || item.streamUrl || item.link } + : { url: item }; + + if (typeof candidate.url !== 'string') continue; + const trimmed = candidate.url.trim(); + if (!trimmed || seen.has(trimmed)) continue; + seen.add(trimmed); + candidate.url = trimmed; + candidates.push(candidate); + } + return candidates; } @@ -772,8 +826,15 @@ async function verifyServed(finalUrl, headers, absoluteBase, trace = []) { async function verifyCandidate(candidate, sourceKey, absoluteBase, trace = []) { if (!candidate?.url) return null; - const wrapped = wrapUrl(candidate, sourceKey, absoluteBase, sdk); - if (!wrapped) { trace.push('wrapUrl returned null'); return null; } + let wrapped = wrapUrl(candidate, sourceKey, absoluteBase, sdk); + if (!wrapped || (!wrapped.includes('/api?url=') && !wrapped.includes('/api/?url='))) { + const cfg = sdk.getSources().find(c => c.key === sourceKey); + const extraParam = cfg?.proxyParam ? `&${cfg.proxyParam}=1` : '&vn=1'; + const headersParam = candidate.headers && Object.keys(candidate.headers).length + ? `&proxyHeaders=${encodeURIComponent(JSON.stringify(candidate.headers))}` + : ''; + wrapped = `${absoluteBase}/api?url=${encodeURIComponent(candidate.url)}${extraParam}${headersParam}`; + } const viaProxy = wrapped !== candidate.url || toLoopback(wrapped, absoluteBase) !== wrapped; trace.push(`wrapped=${wrapped} viaProxy=${viaProxy}`); @@ -783,9 +844,9 @@ async function verifyCandidate(candidate, sourceKey, absoluteBase, trace = []) { ); if (!ok) { trace.push('verify failed or timed out'); return null; } - const out = { url: wrapped, raw_url: candidate.url, proxied: viaProxy }; + const out = { url: wrapped, raw_url: candidate.url, proxied: true }; if (!viaProxy && candidate.headers && Object.keys(candidate.headers).length) out.headers = candidate.headers; - for (const k of ['quality', 'type', 'name', 'language']) { + for (const k of ['quality', 'type', 'name', 'label', 'language', 'resolution']) { if (candidate[k] !== undefined) out[k] = candidate[k]; } return out; @@ -846,7 +907,7 @@ async function handleTestSource(sdk, sourceKey, id, s, e, clientIP, host) { try { rawResult = await fetchSource(cfg, `${id}-${s ?? ''}-${e ?? ''}`, id, s, e, clientIP, absoluteBase); - if (!rawResult) rawResult = await withTimeout(sdk.getStream(sourceKey, id, s, e), 20_000); + if (!rawResult) rawResult = await withTimeout(getSdkStreams(sourceKey, id, s, e, clientIP), 20_000); } catch (err) { fetchError = err.message; } const seen = new Set(); @@ -932,13 +993,14 @@ async function streamSources(sources, id, s, e, clientIP, absoluteBase, res) { sent.add(stream.url); n++; const { raw_url, ...publicStream } = stream; + const streamLabel = publicStream.label || publicStream.name || (publicStream.quality ? `${label} (${publicStream.quality})` : (n > 1 ? `${label} ${n}` : label)); safeWrite(`data: ${JSON.stringify({ type: 'source', source: { source: cfg.key, - label: n > 1 ? `${label} ${n}` : label, index: n - 1, ...publicStream, + label: streamLabel, }, })}\n\n`); } @@ -975,7 +1037,7 @@ async function handleRequest(req, res) { if (pathname === '/' || pathname === '') { return { status: 200, - body: `${LOGO_TEXT}\n\ndeveloped_by: @vyla-entertainment\ngitlab: https://gitlab.com/vyla-entertainment\ndocs: https://docs.vyla.cc\ndmca: https://docs.vyla.cc/misc/dmca`, + body: `${LOGO_TEXT}\n\ndeveloped_by: @vyla-entertainment\ngitlab: https://gitlab.com/vyla-entertainment\ndocs: https://vyla.cc/stream-api/overview\ndmca: https://vyla.cc/misc/project/dmca`, headers: { 'Content-Type': 'text/plain; charset=utf-8', ...CORS_HEADERS }, }; }