diff --git a/build.js b/build.js index a155522..8a296bd 100644 --- a/build.js +++ b/build.js @@ -65,7 +65,9 @@ async function buildProvider(providerName, options = {}) { outfile: outFile, format: 'cjs', // CommonJS for module.exports compatibility 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 sourcemap: false, external: EXTERNAL_MODULES, @@ -107,7 +109,8 @@ async function transpileSingleFile(filename) { try { const result = await esbuild.transform(originalContent, { loader: 'js', - target: 'es2016', // Transpile async/await to generators + // Match bundled providers and retain native async/await for both apps. + target: 'es2017', format: 'cjs' }); @@ -190,4 +193,3 @@ main().catch(err => { console.error('Build failed:', err); process.exit(1); }); - diff --git a/manifest.json b/manifest.json index 68ed857..e26dd38 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { "name": "Nuvio Providers", - "version": "1.0.5", + "version": "1.0.6", "scrapers": [ { "id": "miruro", @@ -32,7 +32,7 @@ "id": "flixer", "name": "Flixer", "description": "Movies and TV streams from Flixer.", - "version": "1.0.2", + "version": "1.0.3", "author": "Jānis L", "supportedTypes": [ "movie", diff --git a/providers/_template.js b/providers/_template.js index 46ecd83..e140b35 100644 --- a/providers/_template.js +++ b/providers/_template.js @@ -1,6 +1,6 @@ /** * _template - Built from src/_template/ - * Generated: 2026-07-19T17:21:01.885Z + * Generated: 2026-07-20T14:16:44.846Z */ var __create = Object.create; 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, 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 var import_cheerio_without_node_native = __toESM(require("cheerio-without-node-native")); -function extractStreams(tmdbId, mediaType, season, episode) { - return __async(this, null, function* () { - return []; - }); +async function extractStreams(tmdbId, mediaType, season, episode) { + return []; } // src/_template/index.js -function getStreams(tmdbId, mediaType, season, episode) { - return __async(this, null, function* () { - try { - console.log(`[Template] Request: ${mediaType} ${tmdbId}`); - const streams = yield extractStreams(tmdbId, mediaType, season, episode); - return streams; - } catch (error) { - console.error(`[Template] Error: ${error.message}`); - return []; - } - }); +async function getStreams(tmdbId, mediaType, season, episode) { + try { + console.log(`[Template] Request: ${mediaType} ${tmdbId}`); + const streams = await extractStreams(tmdbId, mediaType, season, episode); + return streams; + } catch (error) { + console.error(`[Template] Error: ${error.message}`); + return []; + } } module.exports = { getStreams }; diff --git a/providers/flixer.js b/providers/flixer.js index aa36483..b2bfc52 100644 --- a/providers/flixer.js +++ b/providers/flixer.js @@ -1,6 +1,6 @@ /** * flixer - Built from src/flixer/ - * Generated: 2026-07-19T17:21:01.894Z + * Generated: 2026-07-20T14:16:44.852Z */ var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; @@ -21,26 +21,6 @@ var __spreadValues = (a, b) => { return a; }; 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 var SITE_URL = "https://flixer.gd"; @@ -244,55 +224,49 @@ function createNonce() { } var serverTimeOffset = 0; var serverTimeFetchedAt = 0; -function getServerTimestamp() { - return __async(this, null, function* () { - const now = Date.now(); - if (serverTimeFetchedAt && now - serverTimeFetchedAt < 3e5) { - return Math.floor((now + serverTimeOffset) / 1e3); - } - try { - const startedAt = Date.now(); - const response = yield fetch(`${API_URL}/api/time?t=${startedAt}`, { - 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 || {}) - })); +async function getServerTimestamp() { + const now = Date.now(); + if (serverTimeFetchedAt && now - serverTimeFetchedAt < 3e5) { + return Math.floor((now + serverTimeOffset) / 1e3); + } + try { + const startedAt = Date.now(); + const response = await fetch(`${API_URL}/api/time?t=${startedAt}`, { + headers: __spreadProps(__spreadValues({}, HEADERS), { "Cache-Control": "no-cache" }) + }); if (!response.ok) - throw new Error(`HTTP ${response.status} for ${url}`); - return yield response.text(); - }); + throw new Error(`HTTP ${response.status}`); + 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 @@ -591,54 +565,67 @@ var NATO_ORDER = [ "zulu" ]; var SERVER_LABELS = { + alpha: "Ares", bravo: "Balder", + charlie: "Circe", delta: "Dionysus", echo: "Eros", + foxtrot: "Freya", golf: "Gaia", hotel: "Hades", india: "Isis", juliet: "Juno", 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 = {}; -function resolveTmdbId(id, mediaType) { - return __async(this, null, function* () { - if (/^\d+$/.test(id)) - return id; - if (!/^tt\d+$/i.test(id)) +async function resolveTmdbId(id, mediaType) { + if (/^\d+$/.test(id)) + return 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; - const cacheKey = `${mediaType}:${id.toLowerCase()}`; - if (TMDB_ID_CACHE[cacheKey]) - return TMDB_ID_CACHE[cacheKey]; - const catalogType = mediaType === "movie" ? "movie" : "series"; - try { - const raw = yield 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; - 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; - } - }); + 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) { if (mediaType === "movie") return `/api/tmdb/movie/${encodeURIComponent(tmdbId)}/images`; return `/api/tmdb/tv/${encodeURIComponent(tmdbId)}/season/${season}/episode/${episode}/images`; } -function decryptPayload(encrypted, apiKey) { - return __async(this, null, function* () { - const clearText = yield process_img_data(encrypted, apiKey); - return JSON.parse(clearText); - }); +async function decryptPayload(encrypted, apiKey) { + const clearText = await process_img_data(encrypted, apiKey); + return JSON.parse(clearText); } function getServers(payload) { 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); return match ? `${match[1]}p` : "Auto"; } -function fetchServerPayload(apiUrl, apiPath, apiKey, server) { - return __async(this, null, function* () { - const secureHeaders = yield createSecureHeaders(apiKey, apiPath); - return yield fetchText(apiUrl, { - headers: __spreadProps(__spreadValues({}, secureHeaders), { - "X-Only-Sources": "1", - "X-Server": server - }) - }); +async function fetchServerPayload(apiUrl, apiPath, apiKey, server) { + const secureHeaders = await createSecureHeaders(apiKey, apiPath); + return await fetchText(apiUrl, { + headers: __spreadProps(__spreadValues({}, secureHeaders), { + "X-Only-Sources": "1", + "X-Server": server + }) }); } -function extractStreams(tmdbId, mediaType, season, episode) { - return __async(this, null, function* () { - const id = String(tmdbId || "").trim(); - if (mediaType !== "movie" && mediaType !== "tv") - return []; - if (mediaType === "tv" && (!Number.isInteger(Number(season)) || !Number.isInteger(Number(episode)) || Number(season) < 1 || Number(episode) < 1)) - return []; - const resolvedId = yield resolveTmdbId(id, mediaType); - if (!resolvedId) - return []; - initDecoder(); - const apiKey = get_img_key(); - const apiPath = buildApiPath(resolvedId, mediaType, Number(season), Number(episode)); - const apiUrl = `${API_URL}${apiPath}`; - const secureHeaders = yield createSecureHeaders(apiKey, apiPath); - const serverPayload = yield fetchText(apiUrl, { - headers: __spreadProps(__spreadValues({}, secureHeaders), { bW90aGFmYWth: "1" }) - }); - const servers = getServers(yield decryptPayload(serverPayload, apiKey)); - const encryptedResponses = yield Promise.all(servers.map((server) => __async(this, null, function* () { - try { - return { server, encrypted: yield fetchServerPayload(apiUrl, apiPath, apiKey, server) }; - } catch (error) { - console.warn(`[Flixer] ${server} request failed: ${error && error.message ? error.message : String(error)}`); - return { server, encrypted: "" }; - } - }))); - const streams = []; - const seenUrls = {}; - for (const response of encryptedResponses) { - if (!response.encrypted) +async function extractStreams(tmdbId, mediaType, season, episode) { + const id = String(tmdbId || "").trim(); + if (mediaType !== "movie" && mediaType !== "tv") + return []; + if (mediaType === "tv" && (!Number.isInteger(Number(season)) || !Number.isInteger(Number(episode)) || Number(season) < 1 || Number(episode) < 1)) + return []; + const resolvedId = await resolveTmdbId(id, mediaType); + if (!resolvedId) + return []; + initDecoder(); + const apiKey = get_img_key(); + const apiPath = buildApiPath(resolvedId, mediaType, Number(season), Number(episode)); + const apiUrl = `${API_URL}${apiPath}`; + const secureHeaders = await createSecureHeaders(apiKey, apiPath); + const serverPayload = await fetchText(apiUrl, { + headers: __spreadProps(__spreadValues({}, secureHeaders), { bW90aGFmYWth: "1" }) + }); + const servers = getServers(await decryptPayload(serverPayload, apiKey)); + const encryptedResponses = await Promise.all(servers.map(async (server) => { + try { + return { server, encrypted: await fetchServerPayload(apiUrl, apiPath, apiKey, server) }; + } catch (error) { + console.warn(`[Flixer] ${server} request failed: ${error && error.message ? error.message : String(error)}`); + return { server, encrypted: "" }; + } + })); + const streams = []; + const seenUrls = {}; + for (const response of encryptedResponses) { + if (!response.encrypted) + continue; + try { + const url = findUrl(await decryptPayload(response.encrypted, apiKey), response.server); + if (!url || seenUrls[url]) continue; - try { - const url = findUrl(yield decryptPayload(response.encrypted, apiKey), response.server); - if (!url || seenUrls[url]) - continue; - seenUrls[url] = true; - const label = SERVER_LABELS[response.server] || response.server.charAt(0).toUpperCase() + response.server.slice(1); - const quality = getQuality(url); - streams.push({ - name: `Flixer \xB7 ${label}`, - title: `${label} \xB7 ${quality}`, - url, - quality, - 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}`); + seenUrls[url] = true; + const label = SERVER_LABELS[response.server] || response.server.charAt(0).toUpperCase() + response.server.slice(1); + const quality = getQuality(url); + streams.push({ + name: `Flixer \xB7 ${label}`, + title: `${label} \xB7 ${quality}`, + url, + quality, + 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}`); } } - return streams; - }); + } + return streams; } // src/flixer/index.js -function getStreams(tmdbId, mediaType, season, episode) { - return __async(this, null, function* () { - try { - console.log(`[Flixer] Request: ${mediaType} ${tmdbId}`); - const streams = yield extractStreams(tmdbId, mediaType, season, episode); - console.log(`[Flixer] Found ${streams.length} stream(s)`); - return streams; - } catch (error) { - console.error(`[Flixer] Error: ${error.message}`); - return []; - } - }); +async function getStreams(tmdbId, mediaType, season, episode) { + try { + console.log(`[Flixer] Request: ${mediaType} ${tmdbId}`); + const streams = await extractStreams(tmdbId, mediaType, season, episode); + console.log(`[Flixer] Found ${streams.length} stream(s)`); + return streams; + } catch (error) { + console.error(`[Flixer] Error: ${error.message}`); + return []; + } } module.exports = { getStreams }; diff --git a/providers/miruro.js b/providers/miruro.js index 0edb841..9dc6e30 100644 --- a/providers/miruro.js +++ b/providers/miruro.js @@ -1,6 +1,6 @@ /** * miruro - Built from src/miruro/ - * Generated: 2026-07-19T17:21:26.205Z + * Generated: 2026-07-20T14:16:44.855Z */ var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; @@ -21,26 +21,6 @@ var __spreadValues = (a, b) => { return a; }; 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 var u8 = Uint8Array; @@ -528,46 +508,40 @@ function decodeSecurePayload(raw, obfuscationVersion) { function mergeHeaders(headers) { return __spreadValues(__spreadValues({}, HEADERS), headers || {}); } -function fetchText(_0) { - return __async(this, arguments, function* (url, options = {}) { - const response = yield fetch(url, __spreadProps(__spreadValues({}, options), { - headers: mergeHeaders(options.headers) - })); - if (!response.ok) { - throw new Error(`HTTP ${response.status} for ${url}`); - } - return yield response.text(); - }); +async function fetchText(url, options = {}) { + const response = await fetch(url, __spreadProps(__spreadValues({}, options), { + headers: mergeHeaders(options.headers) + })); + if (!response.ok) { + throw new Error(`HTTP ${response.status} for ${url}`); + } + return await response.text(); } -function fetchJson(_0) { - return __async(this, arguments, function* (url, options = {}) { - const raw = yield fetchText(url, options); - return JSON.parse(raw); - }); +async function fetchJson(url, options = {}) { + const raw = await fetchText(url, options); + return JSON.parse(raw); } -function secureGet(_0) { - return __async(this, arguments, function* (path, query = {}) { - const envelope = encodeBase64Url(JSON.stringify({ - path, - method: "GET", - query, - body: null - })); - const url = `${BASE_URL}/api/secure/pipe?e=${envelope}`; - let response; - for (let attempt = 0; attempt < 2; attempt++) { - response = yield fetch(url, { headers: HEADERS }); - if (response.ok || response.status !== 403 && response.status !== 429 && response.status < 500) { - break; - } +async function secureGet(path, query = {}) { + const envelope = encodeBase64Url(JSON.stringify({ + path, + method: "GET", + query, + body: null + })); + const url = `${BASE_URL}/api/secure/pipe?e=${envelope}`; + let response; + for (let attempt = 0; attempt < 2; attempt++) { + response = await fetch(url, { headers: HEADERS }); + if (response.ok || response.status !== 403 && response.status !== 429 && response.status < 500) { + break; } - 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; - return decodeSecurePayload(raw.trim(), version); - }); + } + if (!response || !response.ok) { + throw new Error(`Miruro API returned HTTP ${response ? response.status : "unknown"}`); + } + const raw = await response.text(); + const version = response.headers && response.headers.get ? response.headers.get("x-obfuscated") : null; + return decodeSecurePayload(raw.trim(), version); } // src/miruro/extractor.js @@ -647,16 +621,15 @@ function getLocalEpisode(mappings, mediaType, season, episode) { } return requestedEpisode - offset; } -function getAniListSequel(anilistId) { - return __async(this, null, function* () { - const response = yield fetchJson(ANILIST_URL, { - method: "POST", - headers: { - "Accept": "application/json", - "Content-Type": "application/json" - }, - body: JSON.stringify({ - query: ` +async function getAniListSequel(anilistId) { + const response = await fetchJson(ANILIST_URL, { + method: "POST", + headers: { + "Accept": "application/json", + "Content-Type": "application/json" + }, + body: JSON.stringify({ + query: ` query ($id: Int!) { Media(id: $id, type: ANIME) { relations { @@ -672,85 +645,80 @@ function getAniListSequel(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; + 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; } -function getSeasonAniListId(anilistId, season) { - return __async(this, null, function* () { - const requestedSeason = asNumber(season) || 1; - let currentId = anilistId; - for (let currentSeason = 1; currentSeason < requestedSeason; currentSeason++) { - const sequelId = yield getAniListSequel(currentId); - if (!sequelId || sequelId === currentId) { - return null; - } - currentId = sequelId; +async function getSeasonAniListId(anilistId, season) { + const requestedSeason = asNumber(season) || 1; + let currentId = anilistId; + for (let currentSeason = 1; currentSeason < requestedSeason; currentSeason++) { + const sequelId = await getAniListSequel(currentId); + if (!sequelId || sequelId === currentId) { + return null; } - return currentId; - }); + currentId = sequelId; + } + return currentId; } function getSequelEpisode(payload, mediaType, season, episode) { return getLocalEpisode(payload.mappings, mediaType, season, episode) || getLocalEpisode(payload.mappings, mediaType, 1, episode); } -function resolveEpisodePayload(mediaId, mediaType, season, episode) { - return __async(this, null, function* () { - const identifier = parseIdentifier(mediaId); - if (!identifier) { - return null; - } - let mappingRecords; - try { - mappingRecords = yield fetchJson( - `${ANIMAP_URL}/${identifier.service}/${encodeURIComponent(identifier.value)}`, - { headers: { "Accept": "application/json" } } - ); - } catch (error) { - console.warn(`[Miruro] No anime mapping for ${mediaId}: ${error.message}`); - return null; - } - const aniListIds = getAniListIds(mappingRecords, identifier, mediaType); - if (aniListIds.length === 0) { - return null; - } - if (aniListIds.length === 1) { - const anilistId = aniListIds[0]; - 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) { +async function resolveEpisodePayload(mediaId, mediaType, season, episode) { + const identifier = parseIdentifier(mediaId); + if (!identifier) { + return null; + } + let mappingRecords; + try { + mappingRecords = await fetchJson( + `${ANIMAP_URL}/${identifier.service}/${encodeURIComponent(identifier.value)}`, + { headers: { "Accept": "application/json" } } + ); + } catch (error) { + console.warn(`[Miruro] No anime mapping for ${mediaId}: ${error.message}`); + return null; + } + const aniListIds = getAniListIds(mappingRecords, identifier, mediaType); + if (aniListIds.length === 0) { + return null; + } + if (aniListIds.length === 1) { + const anilistId = aniListIds[0]; + if (mediaType === "tv" && (asNumber(season) || 1) > 1) { try { - const info = yield secureGet(`info/${anilistId}`); - const localEpisode = getLocalEpisode(info.mappings, mediaType, season, episode); - if (localEpisode) { - const payload = yield secureGet("episodes", { anilistId }); - return { anilistId, localEpisode, payload }; + const seasonAniListId = await getSeasonAniListId(anilistId, season); + if (seasonAniListId && seasonAniListId !== anilistId) { + const payload2 = await 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] 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) { const episodes = providerPayload && providerPayload.episodes ? providerPayload.episodes[variant] : null; @@ -787,132 +755,122 @@ function isHlsStream(stream) { const type = String(stream && stream.type || "").toLowerCase(); return type === "hls" || type === "m3u8" || type === "application/x-mpegurl"; } -function resolveQuality(stream, headers) { - return __async(this, null, function* () { - const declaredQuality = getDeclaredQuality(stream); - if (declaredQuality) { - return declaredQuality; - } - if (isHlsStream(stream)) { - let timeoutId; - try { - const manifest = yield Promise.race([ - fetchText(stream.url, { headers }), - new Promise((resolve) => { - timeoutId = setTimeout(() => resolve(null), HLS_QUALITY_TIMEOUT_MS); - }) - ]); - const hlsQuality = typeof manifest === "string" ? getHlsQuality(manifest) : null; - if (hlsQuality) { - return hlsQuality; - } - } catch (_) { - } finally { - if (timeoutId != null) { - clearTimeout(timeoutId); - } +async function resolveQuality(stream, headers) { + const declaredQuality = getDeclaredQuality(stream); + if (declaredQuality) { + return declaredQuality; + } + if (isHlsStream(stream)) { + let timeoutId; + try { + const manifest = await Promise.race([ + fetchText(stream.url, { headers }), + new Promise((resolve) => { + timeoutId = setTimeout(() => resolve(null), HLS_QUALITY_TIMEOUT_MS); + }) + ]); + const hlsQuality = typeof manifest === "string" ? getHlsQuality(manifest) : null; + if (hlsQuality) { + return hlsQuality; + } + } catch (_) { + } finally { + if (timeoutId != null) { + clearTimeout(timeoutId); } } - return "HD"; - }); + } + return "HD"; } function isPlayableStream(stream) { const type = String(stream && stream.type || "").toLowerCase(); return !!(stream && stream.url) && (type === "hls" || type === "m3u8" || type === "mp4" || type === "application/x-mpegurl"); } -function normalizeStreams(sourcePayload, provider, variant) { - return __async(this, null, function* () { - const subtitleCount = Array.isArray(sourcePayload.subtitles) ? sourcePayload.subtitles.length : 0; - const language = variant === "dub" ? "DUB" : "SUB"; - return Promise.all((sourcePayload.streams || []).filter(isPlayableStream).map((stream) => __async(this, null, function* () { - const streamHeaders = stream.headers && typeof stream.headers === "object" ? stream.headers : {}; - const referer = stream.referer || streamHeaders.Referer || streamHeaders.referer || BASE_URL; - const subtitleLabel = subtitleCount > 0 && variant !== "dub" ? " \xB7 CC" : ""; - const headers = __spreadProps(__spreadValues({}, streamHeaders), { - "Referer": referer, - "Origin": getOrigin(referer), - "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; +async function normalizeStreams(sourcePayload, provider, variant) { + const subtitleCount = Array.isArray(sourcePayload.subtitles) ? sourcePayload.subtitles.length : 0; + const language = variant === "dub" ? "DUB" : "SUB"; + return Promise.all((sourcePayload.streams || []).filter(isPlayableStream).map(async (stream) => { + const streamHeaders = stream.headers && typeof stream.headers === "object" ? stream.headers : {}; + const referer = stream.referer || streamHeaders.Referer || streamHeaders.referer || BASE_URL; + const subtitleLabel = subtitleCount > 0 && variant !== "dub" ? " \xB7 CC" : ""; + const headers = __spreadProps(__spreadValues({}, streamHeaders), { + "Referer": referer, + "Origin": getOrigin(referer), + "User-Agent": HEADERS["User-Agent"] }); + 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 -function getStreams(tmdbId, mediaType, season, episode) { - return __async(this, null, function* () { - try { - const episodeLabel = mediaType === "tv" ? ` S${season}E${episode}` : ""; - console.log(`[Miruro] Request: ${mediaType} ${tmdbId}${episodeLabel}`); - const streams = yield extractStreams(tmdbId, mediaType, season, episode); - console.log(`[Miruro] Found ${streams.length} stream(s)`); - return streams; - } catch (error) { - console.error(`[Miruro] Error: ${error.message}`); - return []; - } - }); +async function getStreams(tmdbId, mediaType, season, episode) { + try { + const episodeLabel = mediaType === "tv" ? ` S${season}E${episode}` : ""; + console.log(`[Miruro] Request: ${mediaType} ${tmdbId}${episodeLabel}`); + const streams = await extractStreams(tmdbId, mediaType, season, episode); + console.log(`[Miruro] Found ${streams.length} stream(s)`); + return streams; + } catch (error) { + console.error(`[Miruro] Error: ${error.message}`); + return []; + } } module.exports = { getStreams }; diff --git a/src/flixer/extractor.js b/src/flixer/extractor.js index 69c80ce..912fb8a 100644 --- a/src/flixer/extractor.js +++ b/src/flixer/extractor.js @@ -10,8 +10,12 @@ const NATO_ORDER = [ ]; const SERVER_LABELS = { - bravo: 'Balder', delta: 'Dionysus', echo: 'Eros', golf: 'Gaia', hotel: 'Hades', - india: 'Isis', juliet: 'Juno', kilo: 'Kronos', lima: 'Loki' + alpha: 'Ares', bravo: 'Balder', charlie: 'Circe', delta: 'Dionysus', echo: 'Eros', + 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 = {};