fix: failing on nuviotv due to es2016

This commit is contained in:
janisslsm committed 2026-07-20 17:17:01 +03:00
1 parent 27a87191c3
commit c7eb7b83a9
6 files changed
+390 -469

No files matched your search

+5 -3
View File
@@ -65,7 +65,9 @@ async function buildProvider(providerName, options = {}) {
outfile: outFile, outfile: outFile,
format: 'cjs', // CommonJS for module.exports compatibility format: 'cjs', // CommonJS for module.exports compatibility
platform: 'neutral', // Works in both browser and node-like environments platform: 'neutral', // Works in both browser and node-like environments
target: 'es2016', // Transpile async/await to generators for Hermes // Both Nuvio runtimes support native async/await. Keeping it avoids
// esbuild's generator helper, which older NuvioTV builds can reject.
target: 'es2017',
minify: options.minify || false, // Minify if --minify flag is set minify: options.minify || false, // Minify if --minify flag is set
sourcemap: false, sourcemap: false,
external: EXTERNAL_MODULES, external: EXTERNAL_MODULES,
@@ -107,7 +109,8 @@ async function transpileSingleFile(filename) {
try { try {
const result = await esbuild.transform(originalContent, { const result = await esbuild.transform(originalContent, {
loader: 'js', loader: 'js',
target: 'es2016', // Transpile async/await to generators // Match bundled providers and retain native async/await for both apps.
target: 'es2017',
format: 'cjs' format: 'cjs'
}); });
@@ -190,4 +193,3 @@ main().catch(err => {
console.error('Build failed:', err); console.error('Build failed:', err);
process.exit(1); process.exit(1);
}); });
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "Nuvio Providers", "name": "Nuvio Providers",
"version": "1.0.5", "version": "1.0.6",
"scrapers": [ "scrapers": [
{ {
"id": "miruro", "id": "miruro",
@@ -32,7 +32,7 @@
"id": "flixer", "id": "flixer",
"name": "Flixer", "name": "Flixer",
"description": "Movies and TV streams from Flixer.", "description": "Movies and TV streams from Flixer.",
"version": "1.0.2", "version": "1.0.3",
"author": "Jānis L", "author": "Jānis L",
"supportedTypes": [ "supportedTypes": [
"movie", "movie",
+12 -36
View File
@@ -1,6 +1,6 @@
/** /**
* _template - Built from src/_template/ * _template - Built from src/_template/
* Generated: 2026-07-19T17:21:01.885Z * Generated: 2026-07-20T14:16:44.846Z
*/ */
var __create = Object.create; var __create = Object.create;
var __defProp = Object.defineProperty; var __defProp = Object.defineProperty;
@@ -24,46 +24,22 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod mod
)); ));
var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
// src/_template/extractor.js // src/_template/extractor.js
var import_cheerio_without_node_native = __toESM(require("cheerio-without-node-native")); var import_cheerio_without_node_native = __toESM(require("cheerio-without-node-native"));
function extractStreams(tmdbId, mediaType, season, episode) { async function extractStreams(tmdbId, mediaType, season, episode) {
return __async(this, null, function* () { return [];
return [];
});
} }
// src/_template/index.js // src/_template/index.js
function getStreams(tmdbId, mediaType, season, episode) { async function getStreams(tmdbId, mediaType, season, episode) {
return __async(this, null, function* () { try {
try { console.log(`[Template] Request: ${mediaType} ${tmdbId}`);
console.log(`[Template] Request: ${mediaType} ${tmdbId}`); const streams = await extractStreams(tmdbId, mediaType, season, episode);
const streams = yield extractStreams(tmdbId, mediaType, season, episode); return streams;
return streams; } catch (error) {
} catch (error) { console.error(`[Template] Error: ${error.message}`);
console.error(`[Template] Error: ${error.message}`); return [];
return []; }
}
});
} }
module.exports = { getStreams }; module.exports = { getStreams };
+157 -176
View File
@@ -1,6 +1,6 @@
/** /**
* flixer - Built from src/flixer/ * flixer - Built from src/flixer/
* Generated: 2026-07-19T17:21:01.894Z * Generated: 2026-07-20T14:16:44.852Z
*/ */
var __defProp = Object.defineProperty; var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties; var __defProps = Object.defineProperties;
@@ -21,26 +21,6 @@ var __spreadValues = (a, b) => {
return a; return a;
}; };
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
// src/flixer/http.js // src/flixer/http.js
var SITE_URL = "https://flixer.gd"; var SITE_URL = "https://flixer.gd";
@@ -244,55 +224,49 @@ function createNonce() {
} }
var serverTimeOffset = 0; var serverTimeOffset = 0;
var serverTimeFetchedAt = 0; var serverTimeFetchedAt = 0;
function getServerTimestamp() { async function getServerTimestamp() {
return __async(this, null, function* () { const now = Date.now();
const now = Date.now(); if (serverTimeFetchedAt && now - serverTimeFetchedAt < 3e5) {
if (serverTimeFetchedAt && now - serverTimeFetchedAt < 3e5) { return Math.floor((now + serverTimeOffset) / 1e3);
return Math.floor((now + serverTimeOffset) / 1e3); }
} try {
try { const startedAt = Date.now();
const startedAt = Date.now(); const response = await fetch(`${API_URL}/api/time?t=${startedAt}`, {
const response = yield fetch(`${API_URL}/api/time?t=${startedAt}`, { headers: __spreadProps(__spreadValues({}, HEADERS), { "Cache-Control": "no-cache" })
headers: __spreadProps(__spreadValues({}, HEADERS), { "Cache-Control": "no-cache" }) });
});
if (!response.ok)
throw new Error(`HTTP ${response.status}`);
const payload = yield response.json();
if (!payload || typeof payload.timestamp !== "number")
throw new Error("invalid response");
const finishedAt = Date.now();
serverTimeOffset = payload.timestamp * 1e3 + (finishedAt - startedAt) / 2 - finishedAt;
serverTimeFetchedAt = finishedAt;
return Math.floor((finishedAt + serverTimeOffset) / 1e3);
} catch (_) {
return Math.floor(Date.now() / 1e3);
}
});
}
function createSecureHeaders(apiKey, path) {
return __async(this, null, function* () {
const timestamp = yield getServerTimestamp();
const nonce = createNonce();
const message = `${apiKey}:${timestamp}:${nonce}:${path}`;
return {
"X-Api-Key": apiKey,
"X-Request-Timestamp": String(timestamp),
"X-Request-Nonce": nonce,
"X-Request-Signature": encodeBase64(hmacSha256(apiKey, message)),
"X-Client-Fingerprint": "zerlgt",
"X-Fingerprint-Lite": "e9136c41504646444"
};
});
}
function fetchText(_0) {
return __async(this, arguments, function* (url, options = {}) {
const response = yield fetch(url, __spreadProps(__spreadValues({}, options), {
headers: __spreadValues(__spreadValues({}, HEADERS), options.headers || {})
}));
if (!response.ok) if (!response.ok)
throw new Error(`HTTP ${response.status} for ${url}`); throw new Error(`HTTP ${response.status}`);
return yield response.text(); const payload = await response.json();
}); if (!payload || typeof payload.timestamp !== "number")
throw new Error("invalid response");
const finishedAt = Date.now();
serverTimeOffset = payload.timestamp * 1e3 + (finishedAt - startedAt) / 2 - finishedAt;
serverTimeFetchedAt = finishedAt;
return Math.floor((finishedAt + serverTimeOffset) / 1e3);
} catch (_) {
return Math.floor(Date.now() / 1e3);
}
}
async function createSecureHeaders(apiKey, path) {
const timestamp = await getServerTimestamp();
const nonce = createNonce();
const message = `${apiKey}:${timestamp}:${nonce}:${path}`;
return {
"X-Api-Key": apiKey,
"X-Request-Timestamp": String(timestamp),
"X-Request-Nonce": nonce,
"X-Request-Signature": encodeBase64(hmacSha256(apiKey, message)),
"X-Client-Fingerprint": "zerlgt",
"X-Fingerprint-Lite": "e9136c41504646444"
};
}
async function fetchText(url, options = {}) {
const response = await fetch(url, __spreadProps(__spreadValues({}, options), {
headers: __spreadValues(__spreadValues({}, HEADERS), options.headers || {})
}));
if (!response.ok)
throw new Error(`HTTP ${response.status} for ${url}`);
return await response.text();
} }
// src/flixer/decoder.js // src/flixer/decoder.js
@@ -591,54 +565,67 @@ var NATO_ORDER = [
"zulu" "zulu"
]; ];
var SERVER_LABELS = { var SERVER_LABELS = {
alpha: "Ares",
bravo: "Balder", bravo: "Balder",
charlie: "Circe",
delta: "Dionysus", delta: "Dionysus",
echo: "Eros", echo: "Eros",
foxtrot: "Freya",
golf: "Gaia", golf: "Gaia",
hotel: "Hades", hotel: "Hades",
india: "Isis", india: "Isis",
juliet: "Juno", juliet: "Juno",
kilo: "Kronos", kilo: "Kronos",
lima: "Loki" lima: "Loki",
mike: "Medusa",
november: "Nyx",
oscar: "Odin",
papa: "Persephone",
quebec: "Quirinus",
romeo: "Ra",
sierra: "Selene",
tango: "Thor",
uniform: "Uranus",
victor: "Vulcan",
whiskey: "Woden",
xray: "Xolotl",
yankee: "Ymir",
zulu: "Zeus"
}; };
var TMDB_ID_CACHE = {}; var TMDB_ID_CACHE = {};
function resolveTmdbId(id, mediaType) { async function resolveTmdbId(id, mediaType) {
return __async(this, null, function* () { if (/^\d+$/.test(id))
if (/^\d+$/.test(id)) return id;
return id; if (!/^tt\d+$/i.test(id))
if (!/^tt\d+$/i.test(id)) return null;
const cacheKey = `${mediaType}:${id.toLowerCase()}`;
if (TMDB_ID_CACHE[cacheKey])
return TMDB_ID_CACHE[cacheKey];
const catalogType = mediaType === "movie" ? "movie" : "series";
try {
const raw = await fetchText(`https://v3-cinemeta.strem.io/meta/${catalogType}/${encodeURIComponent(id)}.json`, {
headers: { "Accept": "application/json" }
});
const payload = JSON.parse(raw);
const resolved = payload && payload.meta && payload.meta.moviedb_id;
if (!resolved || !/^\d+$/.test(String(resolved)))
return null; return null;
const cacheKey = `${mediaType}:${id.toLowerCase()}`; TMDB_ID_CACHE[cacheKey] = String(resolved);
if (TMDB_ID_CACHE[cacheKey]) console.log(`[Flixer] Resolved ${id} to TMDB ${resolved}`);
return TMDB_ID_CACHE[cacheKey]; return TMDB_ID_CACHE[cacheKey];
const catalogType = mediaType === "movie" ? "movie" : "series"; } catch (error) {
try { console.warn(`[Flixer] Could not resolve IMDb ID ${id}: ${error && error.message ? error.message : String(error)}`);
const raw = yield fetchText(`https://v3-cinemeta.strem.io/meta/${catalogType}/${encodeURIComponent(id)}.json`, { return null;
headers: { "Accept": "application/json" } }
});
const payload = JSON.parse(raw);
const resolved = payload && payload.meta && payload.meta.moviedb_id;
if (!resolved || !/^\d+$/.test(String(resolved)))
return null;
TMDB_ID_CACHE[cacheKey] = String(resolved);
console.log(`[Flixer] Resolved ${id} to TMDB ${resolved}`);
return TMDB_ID_CACHE[cacheKey];
} catch (error) {
console.warn(`[Flixer] Could not resolve IMDb ID ${id}: ${error && error.message ? error.message : String(error)}`);
return null;
}
});
} }
function buildApiPath(tmdbId, mediaType, season, episode) { function buildApiPath(tmdbId, mediaType, season, episode) {
if (mediaType === "movie") if (mediaType === "movie")
return `/api/tmdb/movie/${encodeURIComponent(tmdbId)}/images`; return `/api/tmdb/movie/${encodeURIComponent(tmdbId)}/images`;
return `/api/tmdb/tv/${encodeURIComponent(tmdbId)}/season/${season}/episode/${episode}/images`; return `/api/tmdb/tv/${encodeURIComponent(tmdbId)}/season/${season}/episode/${episode}/images`;
} }
function decryptPayload(encrypted, apiKey) { async function decryptPayload(encrypted, apiKey) {
return __async(this, null, function* () { const clearText = await process_img_data(encrypted, apiKey);
const clearText = yield process_img_data(encrypted, apiKey); return JSON.parse(clearText);
return JSON.parse(clearText);
});
} }
function getServers(payload) { function getServers(payload) {
const values = []; const values = [];
@@ -674,90 +661,84 @@ function getQuality(url) {
const match = String(url).match(/(?:^|[^0-9])(2160|1440|1080|720|480|360)p?(?:[^0-9]|$)/i); const match = String(url).match(/(?:^|[^0-9])(2160|1440|1080|720|480|360)p?(?:[^0-9]|$)/i);
return match ? `${match[1]}p` : "Auto"; return match ? `${match[1]}p` : "Auto";
} }
function fetchServerPayload(apiUrl, apiPath, apiKey, server) { async function fetchServerPayload(apiUrl, apiPath, apiKey, server) {
return __async(this, null, function* () { const secureHeaders = await createSecureHeaders(apiKey, apiPath);
const secureHeaders = yield createSecureHeaders(apiKey, apiPath); return await fetchText(apiUrl, {
return yield fetchText(apiUrl, { headers: __spreadProps(__spreadValues({}, secureHeaders), {
headers: __spreadProps(__spreadValues({}, secureHeaders), { "X-Only-Sources": "1",
"X-Only-Sources": "1", "X-Server": server
"X-Server": server })
})
});
}); });
} }
function extractStreams(tmdbId, mediaType, season, episode) { async function extractStreams(tmdbId, mediaType, season, episode) {
return __async(this, null, function* () { const id = String(tmdbId || "").trim();
const id = String(tmdbId || "").trim(); if (mediaType !== "movie" && mediaType !== "tv")
if (mediaType !== "movie" && mediaType !== "tv") return [];
return []; if (mediaType === "tv" && (!Number.isInteger(Number(season)) || !Number.isInteger(Number(episode)) || Number(season) < 1 || Number(episode) < 1))
if (mediaType === "tv" && (!Number.isInteger(Number(season)) || !Number.isInteger(Number(episode)) || Number(season) < 1 || Number(episode) < 1)) return [];
return []; const resolvedId = await resolveTmdbId(id, mediaType);
const resolvedId = yield resolveTmdbId(id, mediaType); if (!resolvedId)
if (!resolvedId) return [];
return []; initDecoder();
initDecoder(); const apiKey = get_img_key();
const apiKey = get_img_key(); const apiPath = buildApiPath(resolvedId, mediaType, Number(season), Number(episode));
const apiPath = buildApiPath(resolvedId, mediaType, Number(season), Number(episode)); const apiUrl = `${API_URL}${apiPath}`;
const apiUrl = `${API_URL}${apiPath}`; const secureHeaders = await createSecureHeaders(apiKey, apiPath);
const secureHeaders = yield createSecureHeaders(apiKey, apiPath); const serverPayload = await fetchText(apiUrl, {
const serverPayload = yield fetchText(apiUrl, { headers: __spreadProps(__spreadValues({}, secureHeaders), { bW90aGFmYWth: "1" })
headers: __spreadProps(__spreadValues({}, secureHeaders), { bW90aGFmYWth: "1" }) });
}); const servers = getServers(await decryptPayload(serverPayload, apiKey));
const servers = getServers(yield decryptPayload(serverPayload, apiKey)); const encryptedResponses = await Promise.all(servers.map(async (server) => {
const encryptedResponses = yield Promise.all(servers.map((server) => __async(this, null, function* () { try {
try { return { server, encrypted: await fetchServerPayload(apiUrl, apiPath, apiKey, server) };
return { server, encrypted: yield fetchServerPayload(apiUrl, apiPath, apiKey, server) }; } catch (error) {
} catch (error) { console.warn(`[Flixer] ${server} request failed: ${error && error.message ? error.message : String(error)}`);
console.warn(`[Flixer] ${server} request failed: ${error && error.message ? error.message : String(error)}`); return { server, encrypted: "" };
return { server, encrypted: "" }; }
} }));
}))); const streams = [];
const streams = []; const seenUrls = {};
const seenUrls = {}; for (const response of encryptedResponses) {
for (const response of encryptedResponses) { if (!response.encrypted)
if (!response.encrypted) continue;
try {
const url = findUrl(await decryptPayload(response.encrypted, apiKey), response.server);
if (!url || seenUrls[url])
continue; continue;
try { seenUrls[url] = true;
const url = findUrl(yield decryptPayload(response.encrypted, apiKey), response.server); const label = SERVER_LABELS[response.server] || response.server.charAt(0).toUpperCase() + response.server.slice(1);
if (!url || seenUrls[url]) const quality = getQuality(url);
continue; streams.push({
seenUrls[url] = true; name: `Flixer \xB7 ${label}`,
const label = SERVER_LABELS[response.server] || response.server.charAt(0).toUpperCase() + response.server.slice(1); title: `${label} \xB7 ${quality}`,
const quality = getQuality(url); url,
streams.push({ quality,
name: `Flixer \xB7 ${label}`, headers: {
title: `${label} \xB7 ${quality}`, "Referer": `${SITE_URL}/`,
url, "Origin": SITE_URL,
quality, "User-Agent": HEADERS["User-Agent"]
headers: {
"Referer": `${SITE_URL}/`,
"Origin": SITE_URL,
"User-Agent": HEADERS["User-Agent"]
}
});
} catch (error) {
const message = error && error.message ? error.message : String(error);
if (!/^E5[678]$/.test(message)) {
console.warn(`[Flixer] ${response.server} decode failed: ${message}`);
} }
});
} catch (error) {
const message = error && error.message ? error.message : String(error);
if (!/^E5[678]$/.test(message)) {
console.warn(`[Flixer] ${response.server} decode failed: ${message}`);
} }
} }
return streams; }
}); return streams;
} }
// src/flixer/index.js // src/flixer/index.js
function getStreams(tmdbId, mediaType, season, episode) { async function getStreams(tmdbId, mediaType, season, episode) {
return __async(this, null, function* () { try {
try { console.log(`[Flixer] Request: ${mediaType} ${tmdbId}`);
console.log(`[Flixer] Request: ${mediaType} ${tmdbId}`); const streams = await extractStreams(tmdbId, mediaType, season, episode);
const streams = yield extractStreams(tmdbId, mediaType, season, episode); console.log(`[Flixer] Found ${streams.length} stream(s)`);
console.log(`[Flixer] Found ${streams.length} stream(s)`); return streams;
return streams; } catch (error) {
} catch (error) { console.error(`[Flixer] Error: ${error.message}`);
console.error(`[Flixer] Error: ${error.message}`); return [];
return []; }
}
});
} }
module.exports = { getStreams }; module.exports = { getStreams };
+208 -250
View File
@@ -1,6 +1,6 @@
/** /**
* miruro - Built from src/miruro/ * miruro - Built from src/miruro/
* Generated: 2026-07-19T17:21:26.205Z * Generated: 2026-07-20T14:16:44.855Z
*/ */
var __defProp = Object.defineProperty; var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties; var __defProps = Object.defineProperties;
@@ -21,26 +21,6 @@ var __spreadValues = (a, b) => {
return a; return a;
}; };
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
// node_modules/fflate/esm/browser.js // node_modules/fflate/esm/browser.js
var u8 = Uint8Array; var u8 = Uint8Array;
@@ -528,46 +508,40 @@ function decodeSecurePayload(raw, obfuscationVersion) {
function mergeHeaders(headers) { function mergeHeaders(headers) {
return __spreadValues(__spreadValues({}, HEADERS), headers || {}); return __spreadValues(__spreadValues({}, HEADERS), headers || {});
} }
function fetchText(_0) { async function fetchText(url, options = {}) {
return __async(this, arguments, function* (url, options = {}) { const response = await fetch(url, __spreadProps(__spreadValues({}, options), {
const response = yield fetch(url, __spreadProps(__spreadValues({}, options), { headers: mergeHeaders(options.headers)
headers: mergeHeaders(options.headers) }));
})); if (!response.ok) {
if (!response.ok) { throw new Error(`HTTP ${response.status} for ${url}`);
throw new Error(`HTTP ${response.status} for ${url}`); }
} return await response.text();
return yield response.text();
});
} }
function fetchJson(_0) { async function fetchJson(url, options = {}) {
return __async(this, arguments, function* (url, options = {}) { const raw = await fetchText(url, options);
const raw = yield fetchText(url, options); return JSON.parse(raw);
return JSON.parse(raw);
});
} }
function secureGet(_0) { async function secureGet(path, query = {}) {
return __async(this, arguments, function* (path, query = {}) { const envelope = encodeBase64Url(JSON.stringify({
const envelope = encodeBase64Url(JSON.stringify({ path,
path, method: "GET",
method: "GET", query,
query, body: null
body: null }));
})); const url = `${BASE_URL}/api/secure/pipe?e=${envelope}`;
const url = `${BASE_URL}/api/secure/pipe?e=${envelope}`; let response;
let response; for (let attempt = 0; attempt < 2; attempt++) {
for (let attempt = 0; attempt < 2; attempt++) { response = await fetch(url, { headers: HEADERS });
response = yield fetch(url, { headers: HEADERS }); if (response.ok || response.status !== 403 && response.status !== 429 && response.status < 500) {
if (response.ok || response.status !== 403 && response.status !== 429 && response.status < 500) { break;
break;
}
} }
if (!response || !response.ok) { }
throw new Error(`Miruro API returned HTTP ${response ? response.status : "unknown"}`); if (!response || !response.ok) {
} throw new Error(`Miruro API returned HTTP ${response ? response.status : "unknown"}`);
const raw = yield response.text(); }
const version = response.headers && response.headers.get ? response.headers.get("x-obfuscated") : null; const raw = await response.text();
return decodeSecurePayload(raw.trim(), version); const version = response.headers && response.headers.get ? response.headers.get("x-obfuscated") : null;
}); return decodeSecurePayload(raw.trim(), version);
} }
// src/miruro/extractor.js // src/miruro/extractor.js
@@ -647,16 +621,15 @@ function getLocalEpisode(mappings, mediaType, season, episode) {
} }
return requestedEpisode - offset; return requestedEpisode - offset;
} }
function getAniListSequel(anilistId) { async function getAniListSequel(anilistId) {
return __async(this, null, function* () { const response = await fetchJson(ANILIST_URL, {
const response = yield fetchJson(ANILIST_URL, { method: "POST",
method: "POST", headers: {
headers: { "Accept": "application/json",
"Accept": "application/json", "Content-Type": "application/json"
"Content-Type": "application/json" },
}, body: JSON.stringify({
body: JSON.stringify({ query: `
query: `
query ($id: Int!) { query ($id: Int!) {
Media(id: $id, type: ANIME) { Media(id: $id, type: ANIME) {
relations { relations {
@@ -672,85 +645,80 @@ function getAniListSequel(anilistId) {
} }
} }
`, `,
variables: { id: anilistId } variables: { id: anilistId }
}) })
});
const edges = response && response.data && response.data.Media && response.data.Media.relations ? response.data.Media.relations.edges : [];
const sequel = (edges || []).find((edge) => edge.relationType === "SEQUEL" && edge.node && edge.node.type === "ANIME" && ["TV", "TV_SHORT", "ONA"].includes(edge.node.format));
return sequel ? asNumber(sequel.node.id) : null;
}); });
const edges = response && response.data && response.data.Media && response.data.Media.relations ? response.data.Media.relations.edges : [];
const sequel = (edges || []).find((edge) => edge.relationType === "SEQUEL" && edge.node && edge.node.type === "ANIME" && ["TV", "TV_SHORT", "ONA"].includes(edge.node.format));
return sequel ? asNumber(sequel.node.id) : null;
} }
function getSeasonAniListId(anilistId, season) { async function getSeasonAniListId(anilistId, season) {
return __async(this, null, function* () { const requestedSeason = asNumber(season) || 1;
const requestedSeason = asNumber(season) || 1; let currentId = anilistId;
let currentId = anilistId; for (let currentSeason = 1; currentSeason < requestedSeason; currentSeason++) {
for (let currentSeason = 1; currentSeason < requestedSeason; currentSeason++) { const sequelId = await getAniListSequel(currentId);
const sequelId = yield getAniListSequel(currentId); if (!sequelId || sequelId === currentId) {
if (!sequelId || sequelId === currentId) { return null;
return null;
}
currentId = sequelId;
} }
return currentId; currentId = sequelId;
}); }
return currentId;
} }
function getSequelEpisode(payload, mediaType, season, episode) { function getSequelEpisode(payload, mediaType, season, episode) {
return getLocalEpisode(payload.mappings, mediaType, season, episode) || getLocalEpisode(payload.mappings, mediaType, 1, episode); return getLocalEpisode(payload.mappings, mediaType, season, episode) || getLocalEpisode(payload.mappings, mediaType, 1, episode);
} }
function resolveEpisodePayload(mediaId, mediaType, season, episode) { async function resolveEpisodePayload(mediaId, mediaType, season, episode) {
return __async(this, null, function* () { const identifier = parseIdentifier(mediaId);
const identifier = parseIdentifier(mediaId); if (!identifier) {
if (!identifier) { return null;
return null; }
} let mappingRecords;
let mappingRecords; try {
try { mappingRecords = await fetchJson(
mappingRecords = yield fetchJson( `${ANIMAP_URL}/${identifier.service}/${encodeURIComponent(identifier.value)}`,
`${ANIMAP_URL}/${identifier.service}/${encodeURIComponent(identifier.value)}`, { headers: { "Accept": "application/json" } }
{ headers: { "Accept": "application/json" } } );
); } catch (error) {
} catch (error) { console.warn(`[Miruro] No anime mapping for ${mediaId}: ${error.message}`);
console.warn(`[Miruro] No anime mapping for ${mediaId}: ${error.message}`); return null;
return null; }
} const aniListIds = getAniListIds(mappingRecords, identifier, mediaType);
const aniListIds = getAniListIds(mappingRecords, identifier, mediaType); if (aniListIds.length === 0) {
if (aniListIds.length === 0) { return null;
return null; }
} if (aniListIds.length === 1) {
if (aniListIds.length === 1) { const anilistId = aniListIds[0];
const anilistId = aniListIds[0]; if (mediaType === "tv" && (asNumber(season) || 1) > 1) {
if (mediaType === "tv" && (asNumber(season) || 1) > 1) {
try {
const seasonAniListId = yield getSeasonAniListId(anilistId, season);
if (seasonAniListId && seasonAniListId !== anilistId) {
const payload2 = yield secureGet("episodes", { anilistId: seasonAniListId });
const localEpisode2 = getSequelEpisode(payload2, mediaType, season, episode);
if (localEpisode2) {
return { anilistId: seasonAniListId, localEpisode: localEpisode2, payload: payload2 };
}
}
} catch (error) {
console.warn(`[Miruro] Season ${season} mapping fallback failed: ${error.message}`);
}
}
const payload = yield secureGet("episodes", { anilistId });
const localEpisode = getLocalEpisode(payload.mappings, mediaType, season, episode);
return localEpisode ? { anilistId, localEpisode, payload } : null;
}
for (const anilistId of aniListIds) {
try { try {
const info = yield secureGet(`info/${anilistId}`); const seasonAniListId = await getSeasonAniListId(anilistId, season);
const localEpisode = getLocalEpisode(info.mappings, mediaType, season, episode); if (seasonAniListId && seasonAniListId !== anilistId) {
if (localEpisode) { const payload2 = await secureGet("episodes", { anilistId: seasonAniListId });
const payload = yield secureGet("episodes", { anilistId }); const localEpisode2 = getSequelEpisode(payload2, mediaType, season, episode);
return { anilistId, localEpisode, payload }; if (localEpisode2) {
return { anilistId: seasonAniListId, localEpisode: localEpisode2, payload: payload2 };
}
} }
} catch (error) { } catch (error) {
console.warn(`[Miruro] Skipping AniList ${anilistId}: ${error.message}`); console.warn(`[Miruro] Season ${season} mapping fallback failed: ${error.message}`);
} }
} }
return null; const payload = await secureGet("episodes", { anilistId });
}); const localEpisode = getLocalEpisode(payload.mappings, mediaType, season, episode);
return localEpisode ? { anilistId, localEpisode, payload } : null;
}
for (const anilistId of aniListIds) {
try {
const info = await secureGet(`info/${anilistId}`);
const localEpisode = getLocalEpisode(info.mappings, mediaType, season, episode);
if (localEpisode) {
const payload = await secureGet("episodes", { anilistId });
return { anilistId, localEpisode, payload };
}
} catch (error) {
console.warn(`[Miruro] Skipping AniList ${anilistId}: ${error.message}`);
}
}
return null;
} }
function findEpisode(providerPayload, variant, episodeNumber) { function findEpisode(providerPayload, variant, episodeNumber) {
const episodes = providerPayload && providerPayload.episodes ? providerPayload.episodes[variant] : null; const episodes = providerPayload && providerPayload.episodes ? providerPayload.episodes[variant] : null;
@@ -787,132 +755,122 @@ function isHlsStream(stream) {
const type = String(stream && stream.type || "").toLowerCase(); const type = String(stream && stream.type || "").toLowerCase();
return type === "hls" || type === "m3u8" || type === "application/x-mpegurl"; return type === "hls" || type === "m3u8" || type === "application/x-mpegurl";
} }
function resolveQuality(stream, headers) { async function resolveQuality(stream, headers) {
return __async(this, null, function* () { const declaredQuality = getDeclaredQuality(stream);
const declaredQuality = getDeclaredQuality(stream); if (declaredQuality) {
if (declaredQuality) { return declaredQuality;
return declaredQuality; }
} if (isHlsStream(stream)) {
if (isHlsStream(stream)) { let timeoutId;
let timeoutId; try {
try { const manifest = await Promise.race([
const manifest = yield Promise.race([ fetchText(stream.url, { headers }),
fetchText(stream.url, { headers }), new Promise((resolve) => {
new Promise((resolve) => { timeoutId = setTimeout(() => resolve(null), HLS_QUALITY_TIMEOUT_MS);
timeoutId = setTimeout(() => resolve(null), HLS_QUALITY_TIMEOUT_MS); })
}) ]);
]); const hlsQuality = typeof manifest === "string" ? getHlsQuality(manifest) : null;
const hlsQuality = typeof manifest === "string" ? getHlsQuality(manifest) : null; if (hlsQuality) {
if (hlsQuality) { return hlsQuality;
return hlsQuality; }
} } catch (_) {
} catch (_) { } finally {
} finally { if (timeoutId != null) {
if (timeoutId != null) { clearTimeout(timeoutId);
clearTimeout(timeoutId);
}
} }
} }
return "HD"; }
}); return "HD";
} }
function isPlayableStream(stream) { function isPlayableStream(stream) {
const type = String(stream && stream.type || "").toLowerCase(); const type = String(stream && stream.type || "").toLowerCase();
return !!(stream && stream.url) && (type === "hls" || type === "m3u8" || type === "mp4" || type === "application/x-mpegurl"); return !!(stream && stream.url) && (type === "hls" || type === "m3u8" || type === "mp4" || type === "application/x-mpegurl");
} }
function normalizeStreams(sourcePayload, provider, variant) { async function normalizeStreams(sourcePayload, provider, variant) {
return __async(this, null, function* () { const subtitleCount = Array.isArray(sourcePayload.subtitles) ? sourcePayload.subtitles.length : 0;
const subtitleCount = Array.isArray(sourcePayload.subtitles) ? sourcePayload.subtitles.length : 0; const language = variant === "dub" ? "DUB" : "SUB";
const language = variant === "dub" ? "DUB" : "SUB"; return Promise.all((sourcePayload.streams || []).filter(isPlayableStream).map(async (stream) => {
return Promise.all((sourcePayload.streams || []).filter(isPlayableStream).map((stream) => __async(this, null, function* () { const streamHeaders = stream.headers && typeof stream.headers === "object" ? stream.headers : {};
const streamHeaders = stream.headers && typeof stream.headers === "object" ? stream.headers : {}; const referer = stream.referer || streamHeaders.Referer || streamHeaders.referer || BASE_URL;
const referer = stream.referer || streamHeaders.Referer || streamHeaders.referer || BASE_URL; const subtitleLabel = subtitleCount > 0 && variant !== "dub" ? " \xB7 CC" : "";
const subtitleLabel = subtitleCount > 0 && variant !== "dub" ? " \xB7 CC" : ""; const headers = __spreadProps(__spreadValues({}, streamHeaders), {
const headers = __spreadProps(__spreadValues({}, streamHeaders), { "Referer": referer,
"Referer": referer, "Origin": getOrigin(referer),
"Origin": getOrigin(referer), "User-Agent": HEADERS["User-Agent"]
"User-Agent": HEADERS["User-Agent"]
});
const quality = yield resolveQuality(stream, headers);
const server = !stream.server || /^(?:HD|auto)$/i.test(stream.server) ? quality : stream.server;
return {
// Nuvio's source picker prominently displays `name`, so include the
// Miruro server and audio variant here instead of only in `title`.
name: `Miruro \xB7 ${provider.label} \xB7 ${language}`,
title: `[${language}] ${provider.label} \xB7 ${server}${subtitleLabel}`,
url: stream.url,
quality,
headers
};
})));
});
}
function getProviderStreams(provider, providerPayload, anilistId, episodeNumber) {
return __async(this, null, function* () {
const requests = [];
const subEpisode = findEpisode(providerPayload, "sub", episodeNumber);
const dubEpisode = findEpisode(providerPayload, "dub", episodeNumber);
if (subEpisode) {
requests.push({ episode: subEpisode, variant: "sub", category: provider.subCategory });
}
if (dubEpisode) {
requests.push({ episode: dubEpisode, variant: "dub", category: "dub" });
}
const results = yield Promise.all(requests.map((request) => __async(this, null, function* () {
try {
const sources = yield secureGet("sources", {
episodeId: request.episode.id,
provider: provider.id,
category: request.category,
anilistId
});
return normalizeStreams(sources, provider, request.variant);
} catch (error) {
console.warn(`[Miruro] ${provider.label} ${request.variant} failed: ${error.message}`);
return [];
}
})));
return results.flat();
});
}
function extractStreams(tmdbId, mediaType, season, episode) {
return __async(this, null, function* () {
if (!tmdbId || mediaType !== "movie" && mediaType !== "tv") {
return [];
}
const resolved = yield resolveEpisodePayload(tmdbId, mediaType, season, episode);
if (!resolved || !resolved.payload || !resolved.payload.providers) {
return [];
}
const providerResults = yield Promise.all(PROVIDERS.map((provider) => {
const payload = resolved.payload.providers[provider.id];
return payload ? getProviderStreams(provider, payload, resolved.anilistId, resolved.localEpisode) : [];
}));
const streams = providerResults.flat();
const seen = /* @__PURE__ */ new Set();
return streams.filter((stream) => {
if (seen.has(stream.url)) {
return false;
}
seen.add(stream.url);
return true;
}); });
const quality = await resolveQuality(stream, headers);
const server = !stream.server || /^(?:HD|auto)$/i.test(stream.server) ? quality : stream.server;
return {
// Nuvio's source picker prominently displays `name`, so include the
// Miruro server and audio variant here instead of only in `title`.
name: `Miruro \xB7 ${provider.label} \xB7 ${language}`,
title: `[${language}] ${provider.label} \xB7 ${server}${subtitleLabel}`,
url: stream.url,
quality,
headers
};
}));
}
async function getProviderStreams(provider, providerPayload, anilistId, episodeNumber) {
const requests = [];
const subEpisode = findEpisode(providerPayload, "sub", episodeNumber);
const dubEpisode = findEpisode(providerPayload, "dub", episodeNumber);
if (subEpisode) {
requests.push({ episode: subEpisode, variant: "sub", category: provider.subCategory });
}
if (dubEpisode) {
requests.push({ episode: dubEpisode, variant: "dub", category: "dub" });
}
const results = await Promise.all(requests.map(async (request) => {
try {
const sources = await secureGet("sources", {
episodeId: request.episode.id,
provider: provider.id,
category: request.category,
anilistId
});
return normalizeStreams(sources, provider, request.variant);
} catch (error) {
console.warn(`[Miruro] ${provider.label} ${request.variant} failed: ${error.message}`);
return [];
}
}));
return results.flat();
}
async function extractStreams(tmdbId, mediaType, season, episode) {
if (!tmdbId || mediaType !== "movie" && mediaType !== "tv") {
return [];
}
const resolved = await resolveEpisodePayload(tmdbId, mediaType, season, episode);
if (!resolved || !resolved.payload || !resolved.payload.providers) {
return [];
}
const providerResults = await Promise.all(PROVIDERS.map((provider) => {
const payload = resolved.payload.providers[provider.id];
return payload ? getProviderStreams(provider, payload, resolved.anilistId, resolved.localEpisode) : [];
}));
const streams = providerResults.flat();
const seen = /* @__PURE__ */ new Set();
return streams.filter((stream) => {
if (seen.has(stream.url)) {
return false;
}
seen.add(stream.url);
return true;
}); });
} }
// src/miruro/index.js // src/miruro/index.js
function getStreams(tmdbId, mediaType, season, episode) { async function getStreams(tmdbId, mediaType, season, episode) {
return __async(this, null, function* () { try {
try { const episodeLabel = mediaType === "tv" ? ` S${season}E${episode}` : "";
const episodeLabel = mediaType === "tv" ? ` S${season}E${episode}` : ""; console.log(`[Miruro] Request: ${mediaType} ${tmdbId}${episodeLabel}`);
console.log(`[Miruro] Request: ${mediaType} ${tmdbId}${episodeLabel}`); const streams = await extractStreams(tmdbId, mediaType, season, episode);
const streams = yield extractStreams(tmdbId, mediaType, season, episode); console.log(`[Miruro] Found ${streams.length} stream(s)`);
console.log(`[Miruro] Found ${streams.length} stream(s)`); return streams;
return streams; } catch (error) {
} catch (error) { console.error(`[Miruro] Error: ${error.message}`);
console.error(`[Miruro] Error: ${error.message}`); return [];
return []; }
}
});
} }
module.exports = { getStreams }; module.exports = { getStreams };
+6 -2
View File
@@ -10,8 +10,12 @@ const NATO_ORDER = [
]; ];
const SERVER_LABELS = { const SERVER_LABELS = {
bravo: 'Balder', delta: 'Dionysus', echo: 'Eros', golf: 'Gaia', hotel: 'Hades', alpha: 'Ares', bravo: 'Balder', charlie: 'Circe', delta: 'Dionysus', echo: 'Eros',
india: 'Isis', juliet: 'Juno', kilo: 'Kronos', lima: 'Loki' foxtrot: 'Freya', golf: 'Gaia', hotel: 'Hades', india: 'Isis', juliet: 'Juno',
kilo: 'Kronos', lima: 'Loki', mike: 'Medusa', november: 'Nyx', oscar: 'Odin',
papa: 'Persephone', quebec: 'Quirinus', romeo: 'Ra', sierra: 'Selene', tango: 'Thor',
uniform: 'Uranus', victor: 'Vulcan', whiskey: 'Woden', xray: 'Xolotl', yankee: 'Ymir',
zulu: 'Zeus'
}; };
const TMDB_ID_CACHE = {}; const TMDB_ID_CACHE = {};