This commit is contained in:
2026-10-27 21:12:45 -07:00
parent 5b7b085c02
commit 295c0efb66
3 changed files with 91 additions and 29 deletions

View File

@@ -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
https://vyla.cc/misc/project/credit

View File

@@ -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"
}

112
server.js
View File

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