1002 lines
30 KiB
JavaScript
1002 lines
30 KiB
JavaScript
// miruro - bundled from src/miruro/
|
|
var __defProp = Object.defineProperty;
|
|
var __defProps = Object.defineProperties;
|
|
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
var __spreadValues = (a, b) => {
|
|
for (var prop in b || (b = {}))
|
|
if (__hasOwnProp.call(b, prop))
|
|
__defNormalProp(a, prop, b[prop]);
|
|
if (__getOwnPropSymbols)
|
|
for (var prop of __getOwnPropSymbols(b)) {
|
|
if (__propIsEnum.call(b, prop))
|
|
__defNormalProp(a, prop, b[prop]);
|
|
}
|
|
return a;
|
|
};
|
|
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
|
|
// node_modules/fflate/esm/browser.js
|
|
var u8 = Uint8Array;
|
|
var u16 = Uint16Array;
|
|
var i32 = Int32Array;
|
|
var fleb = new u8([
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
1,
|
|
1,
|
|
1,
|
|
1,
|
|
2,
|
|
2,
|
|
2,
|
|
2,
|
|
3,
|
|
3,
|
|
3,
|
|
3,
|
|
4,
|
|
4,
|
|
4,
|
|
4,
|
|
5,
|
|
5,
|
|
5,
|
|
5,
|
|
0,
|
|
/* unused */
|
|
0,
|
|
0,
|
|
/* impossible */
|
|
0
|
|
]);
|
|
var fdeb = new u8([
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
1,
|
|
1,
|
|
2,
|
|
2,
|
|
3,
|
|
3,
|
|
4,
|
|
4,
|
|
5,
|
|
5,
|
|
6,
|
|
6,
|
|
7,
|
|
7,
|
|
8,
|
|
8,
|
|
9,
|
|
9,
|
|
10,
|
|
10,
|
|
11,
|
|
11,
|
|
12,
|
|
12,
|
|
13,
|
|
13,
|
|
/* unused */
|
|
0,
|
|
0
|
|
]);
|
|
var clim = new u8([16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15]);
|
|
var freb = function(eb, start) {
|
|
var b = new u16(31);
|
|
for (var i = 0; i < 31; ++i) {
|
|
b[i] = start += 1 << eb[i - 1];
|
|
}
|
|
var r = new i32(b[30]);
|
|
for (var i = 1; i < 30; ++i) {
|
|
for (var j = b[i]; j < b[i + 1]; ++j) {
|
|
r[j] = j - b[i] << 5 | i;
|
|
}
|
|
}
|
|
return { b, r };
|
|
};
|
|
var _a = freb(fleb, 2);
|
|
var fl = _a.b;
|
|
var revfl = _a.r;
|
|
fl[28] = 258, revfl[258] = 28;
|
|
var _b = freb(fdeb, 0);
|
|
var fd = _b.b;
|
|
var revfd = _b.r;
|
|
var rev = new u16(32768);
|
|
for (i = 0; i < 32768; ++i) {
|
|
x = (i & 43690) >> 1 | (i & 21845) << 1;
|
|
x = (x & 52428) >> 2 | (x & 13107) << 2;
|
|
x = (x & 61680) >> 4 | (x & 3855) << 4;
|
|
rev[i] = ((x & 65280) >> 8 | (x & 255) << 8) >> 1;
|
|
}
|
|
var x;
|
|
var i;
|
|
var hMap = function(cd, mb, r) {
|
|
var s = cd.length;
|
|
var i = 0;
|
|
var l = new u16(mb);
|
|
for (; i < s; ++i) {
|
|
if (cd[i])
|
|
++l[cd[i] - 1];
|
|
}
|
|
var le = new u16(mb);
|
|
for (i = 1; i < mb; ++i) {
|
|
le[i] = le[i - 1] + l[i - 1] << 1;
|
|
}
|
|
var co;
|
|
if (r) {
|
|
co = new u16(1 << mb);
|
|
var rvb = 15 - mb;
|
|
for (i = 0; i < s; ++i) {
|
|
if (cd[i]) {
|
|
var sv = i << 4 | cd[i];
|
|
var r_1 = mb - cd[i];
|
|
var v = le[cd[i] - 1]++ << r_1;
|
|
for (var m = v | (1 << r_1) - 1; v <= m; ++v) {
|
|
co[rev[v] >> rvb] = sv;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
co = new u16(s);
|
|
for (i = 0; i < s; ++i) {
|
|
if (cd[i]) {
|
|
co[i] = rev[le[cd[i] - 1]++] >> 15 - cd[i];
|
|
}
|
|
}
|
|
}
|
|
return co;
|
|
};
|
|
var flt = new u8(288);
|
|
for (i = 0; i < 144; ++i)
|
|
flt[i] = 8;
|
|
var i;
|
|
for (i = 144; i < 256; ++i)
|
|
flt[i] = 9;
|
|
var i;
|
|
for (i = 256; i < 280; ++i)
|
|
flt[i] = 7;
|
|
var i;
|
|
for (i = 280; i < 288; ++i)
|
|
flt[i] = 8;
|
|
var i;
|
|
var fdt = new u8(32);
|
|
for (i = 0; i < 32; ++i)
|
|
fdt[i] = 5;
|
|
var i;
|
|
var flrm = /* @__PURE__ */ hMap(flt, 9, 1);
|
|
var fdrm = /* @__PURE__ */ hMap(fdt, 5, 1);
|
|
var max = function(a) {
|
|
var m = a[0];
|
|
for (var i = 1; i < a.length; ++i) {
|
|
if (a[i] > m)
|
|
m = a[i];
|
|
}
|
|
return m;
|
|
};
|
|
var bits = function(d, p, m) {
|
|
var o = p / 8 | 0;
|
|
return (d[o] | d[o + 1] << 8) >> (p & 7) & m;
|
|
};
|
|
var bits16 = function(d, p) {
|
|
var o = p / 8 | 0;
|
|
return (d[o] | d[o + 1] << 8 | d[o + 2] << 16) >> (p & 7);
|
|
};
|
|
var shft = function(p) {
|
|
return (p + 7) / 8 | 0;
|
|
};
|
|
var slc = function(v, s, e) {
|
|
if (s == null || s < 0)
|
|
s = 0;
|
|
if (e == null || e > v.length)
|
|
e = v.length;
|
|
return new u8(v.subarray(s, e));
|
|
};
|
|
var ec = [
|
|
"unexpected EOF",
|
|
"invalid block type",
|
|
"invalid length/literal",
|
|
"invalid distance",
|
|
"stream finished",
|
|
"no stream handler",
|
|
,
|
|
// determined by compression function
|
|
"no callback",
|
|
"invalid UTF-8 data",
|
|
"extra field too long",
|
|
"date not in range 1980-2099",
|
|
"filename too long",
|
|
"stream finishing",
|
|
"invalid zip data"
|
|
// determined by unknown compression method
|
|
];
|
|
var err = function(ind, msg, nt) {
|
|
var e = new Error(msg || ec[ind]);
|
|
e.code = ind;
|
|
if (Error.captureStackTrace)
|
|
Error.captureStackTrace(e, err);
|
|
if (!nt)
|
|
throw e;
|
|
return e;
|
|
};
|
|
var inflt = function(dat, st, buf, dict) {
|
|
var sl = dat.length, dl = dict ? dict.length : 0;
|
|
if (!sl || st.f && !st.l)
|
|
return buf || new u8(0);
|
|
var noBuf = !buf;
|
|
var resize = noBuf || st.i != 2;
|
|
var noSt = st.i;
|
|
if (noBuf)
|
|
buf = new u8(sl * 3);
|
|
var cbuf = function(l2) {
|
|
var bl = buf.length;
|
|
if (l2 > bl) {
|
|
var nbuf = new u8(Math.max(bl * 2, l2));
|
|
nbuf.set(buf);
|
|
buf = nbuf;
|
|
}
|
|
};
|
|
var final = st.f || 0, pos = st.p || 0, bt = st.b || 0, lm = st.l, dm = st.d, lbt = st.m, dbt = st.n;
|
|
var tbts = sl * 8;
|
|
do {
|
|
if (!lm) {
|
|
final = bits(dat, pos, 1);
|
|
var type = bits(dat, pos + 1, 3);
|
|
pos += 3;
|
|
if (!type) {
|
|
var s = shft(pos) + 4, l = dat[s - 4] | dat[s - 3] << 8, t = s + l;
|
|
if (t > sl) {
|
|
if (noSt)
|
|
err(0);
|
|
break;
|
|
}
|
|
if (resize)
|
|
cbuf(bt + l);
|
|
buf.set(dat.subarray(s, t), bt);
|
|
st.b = bt += l, st.p = pos = t * 8, st.f = final;
|
|
continue;
|
|
} else if (type == 1)
|
|
lm = flrm, dm = fdrm, lbt = 9, dbt = 5;
|
|
else if (type == 2) {
|
|
var hLit = bits(dat, pos, 31) + 257, hcLen = bits(dat, pos + 10, 15) + 4;
|
|
var tl = hLit + bits(dat, pos + 5, 31) + 1;
|
|
pos += 14;
|
|
var ldt = new u8(tl);
|
|
var clt = new u8(19);
|
|
for (var i = 0; i < hcLen; ++i) {
|
|
clt[clim[i]] = bits(dat, pos + i * 3, 7);
|
|
}
|
|
pos += hcLen * 3;
|
|
var clb = max(clt), clbmsk = (1 << clb) - 1;
|
|
var clm = hMap(clt, clb, 1);
|
|
for (var i = 0; i < tl; ) {
|
|
var r = clm[bits(dat, pos, clbmsk)];
|
|
pos += r & 15;
|
|
var s = r >> 4;
|
|
if (s < 16) {
|
|
ldt[i++] = s;
|
|
} else {
|
|
var c = 0, n = 0;
|
|
if (s == 16)
|
|
n = 3 + bits(dat, pos, 3), pos += 2, c = ldt[i - 1];
|
|
else if (s == 17)
|
|
n = 3 + bits(dat, pos, 7), pos += 3;
|
|
else if (s == 18)
|
|
n = 11 + bits(dat, pos, 127), pos += 7;
|
|
while (n--)
|
|
ldt[i++] = c;
|
|
}
|
|
}
|
|
var lt = ldt.subarray(0, hLit), dt = ldt.subarray(hLit);
|
|
lbt = max(lt);
|
|
dbt = max(dt);
|
|
lm = hMap(lt, lbt, 1);
|
|
dm = hMap(dt, dbt, 1);
|
|
} else
|
|
err(1);
|
|
if (pos > tbts) {
|
|
if (noSt)
|
|
err(0);
|
|
break;
|
|
}
|
|
}
|
|
if (resize)
|
|
cbuf(bt + 131072);
|
|
var lms = (1 << lbt) - 1, dms = (1 << dbt) - 1;
|
|
var lpos = pos;
|
|
for (; ; lpos = pos) {
|
|
var c = lm[bits16(dat, pos) & lms], sym = c >> 4;
|
|
pos += c & 15;
|
|
if (pos > tbts) {
|
|
if (noSt)
|
|
err(0);
|
|
break;
|
|
}
|
|
if (!c)
|
|
err(2);
|
|
if (sym < 256)
|
|
buf[bt++] = sym;
|
|
else if (sym == 256) {
|
|
lpos = pos, lm = null;
|
|
break;
|
|
} else {
|
|
var add = sym - 254;
|
|
if (sym > 264) {
|
|
var i = sym - 257, b = fleb[i];
|
|
add = bits(dat, pos, (1 << b) - 1) + fl[i];
|
|
pos += b;
|
|
}
|
|
var d = dm[bits16(dat, pos) & dms], dsym = d >> 4;
|
|
if (!d)
|
|
err(3);
|
|
pos += d & 15;
|
|
var dt = fd[dsym];
|
|
if (dsym > 3) {
|
|
var b = fdeb[dsym];
|
|
dt += bits16(dat, pos) & (1 << b) - 1, pos += b;
|
|
}
|
|
if (pos > tbts) {
|
|
if (noSt)
|
|
err(0);
|
|
break;
|
|
}
|
|
if (resize)
|
|
cbuf(bt + 131072);
|
|
var end = bt + add;
|
|
if (bt < dt) {
|
|
var shift = dl - dt, dend = Math.min(dt, end);
|
|
if (shift + bt < 0)
|
|
err(3);
|
|
for (; bt < dend; ++bt)
|
|
buf[bt] = dict[shift + bt];
|
|
}
|
|
for (; bt < end; ++bt)
|
|
buf[bt] = buf[bt - dt];
|
|
}
|
|
}
|
|
st.l = lm, st.p = lpos, st.b = bt, st.f = final;
|
|
if (lm)
|
|
final = 1, st.m = lbt, st.d = dm, st.n = dbt;
|
|
} while (!final);
|
|
return bt != buf.length && noBuf ? slc(buf, 0, bt) : buf.subarray(0, bt);
|
|
};
|
|
var et = /* @__PURE__ */ new u8(0);
|
|
var gzs = function(d) {
|
|
if (d[0] != 31 || d[1] != 139 || d[2] != 8)
|
|
err(6, "invalid gzip data");
|
|
var flg = d[3];
|
|
var st = 10;
|
|
if (flg & 4)
|
|
st += (d[10] | d[11] << 8) + 2;
|
|
for (var zs = (flg >> 3 & 1) + (flg >> 4 & 1); zs > 0; zs -= !d[st++])
|
|
;
|
|
return st + (flg & 2);
|
|
};
|
|
var gzl = function(d) {
|
|
var l = d.length;
|
|
return (d[l - 4] | d[l - 3] << 8 | d[l - 2] << 16 | d[l - 1] << 24) >>> 0;
|
|
};
|
|
function gunzipSync(data, opts) {
|
|
var st = gzs(data);
|
|
if (st + 8 > data.length)
|
|
err(6, "invalid gzip data");
|
|
return inflt(data.subarray(st, -8), { i: 2 }, opts && opts.out || new u8(gzl(data)), opts && opts.dictionary);
|
|
}
|
|
var td = typeof TextDecoder != "undefined" && /* @__PURE__ */ new TextDecoder();
|
|
var tds = 0;
|
|
try {
|
|
td.decode(et, { stream: true });
|
|
tds = 1;
|
|
} catch (e) {
|
|
}
|
|
var dutf8 = function(d) {
|
|
for (var r = "", i = 0; ; ) {
|
|
var c = d[i++];
|
|
var eb = (c > 127) + (c > 223) + (c > 239);
|
|
if (i + eb > d.length)
|
|
return { s: r, r: slc(d, i - 1) };
|
|
if (!eb)
|
|
r += String.fromCharCode(c);
|
|
else if (eb == 3) {
|
|
c = ((c & 15) << 18 | (d[i++] & 63) << 12 | (d[i++] & 63) << 6 | d[i++] & 63) - 65536, r += String.fromCharCode(55296 | c >> 10, 56320 | c & 1023);
|
|
} else if (eb & 1)
|
|
r += String.fromCharCode((c & 31) << 6 | d[i++] & 63);
|
|
else
|
|
r += String.fromCharCode((c & 15) << 12 | (d[i++] & 63) << 6 | d[i++] & 63);
|
|
}
|
|
};
|
|
function strFromU8(dat, latin1) {
|
|
if (latin1) {
|
|
var r = "";
|
|
for (var i = 0; i < dat.length; i += 16384)
|
|
r += String.fromCharCode.apply(null, dat.subarray(i, i + 16384));
|
|
return r;
|
|
} else if (td) {
|
|
return td.decode(dat);
|
|
} else {
|
|
var _a2 = dutf8(dat), s = _a2.s, r = _a2.r;
|
|
if (r.length)
|
|
err(8);
|
|
return s;
|
|
}
|
|
}
|
|
|
|
// src/miruro/http.js
|
|
var BASE_URL = "https://www.miruro.cx";
|
|
var HEADERS = {
|
|
"Accept": "*/*",
|
|
"Accept-Language": "en-US,en;q=0.9",
|
|
"Referer": `${BASE_URL}/`,
|
|
"User-Agent": "Mozilla/5.0 (Linux; Android 15) AppleWebKit/537.36 Chrome/140.0.0.0 Mobile Safari/537.36",
|
|
"Sec-Fetch-Dest": "empty",
|
|
"Sec-Fetch-Mode": "cors",
|
|
"Sec-Fetch-Site": "same-origin"
|
|
};
|
|
var CATALOG_OBFUSCATION_KEY = "miruro/catalog";
|
|
var RETRY_STATUSES = /* @__PURE__ */ new Set([400, 403, 408, 429, 500, 502, 503, 504]);
|
|
var MAX_ATTEMPTS = 3;
|
|
function decodeCatalogPayload(bytes) {
|
|
for (let index = 0; index < bytes.length; index++) {
|
|
bytes[index] ^= CATALOG_OBFUSCATION_KEY.charCodeAt(index % CATALOG_OBFUSCATION_KEY.length);
|
|
}
|
|
return JSON.parse(strFromU8(gunzipSync(bytes)));
|
|
}
|
|
function mergeHeaders(headers) {
|
|
return __spreadValues(__spreadValues({}, HEADERS), headers || {});
|
|
}
|
|
function buildQuery(query) {
|
|
const parts = [];
|
|
for (const [key, value] of Object.entries(query || {})) {
|
|
if (value === void 0 || value === null || value === "") {
|
|
continue;
|
|
}
|
|
const serialized = Array.isArray(value) ? value.join(",") : String(value);
|
|
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(serialized)}`);
|
|
}
|
|
return parts.length > 0 ? `?${parts.join("&")}` : "";
|
|
}
|
|
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();
|
|
}
|
|
async function fetchJson(url, options = {}) {
|
|
const raw = await fetchText(url, options);
|
|
return JSON.parse(raw);
|
|
}
|
|
async function apiGet(path, query = {}) {
|
|
const url = `${BASE_URL}/api/v1/${path.replace(/^\/+/, "")}${buildQuery(query)}`;
|
|
let response;
|
|
let lastError;
|
|
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
try {
|
|
response = await fetch(url, { headers: HEADERS });
|
|
lastError = void 0;
|
|
} catch (error) {
|
|
response = void 0;
|
|
lastError = error;
|
|
}
|
|
if (response && !RETRY_STATUSES.has(response.status)) {
|
|
break;
|
|
}
|
|
if (attempt < MAX_ATTEMPTS - 1) {
|
|
await new Promise((resolve) => setTimeout(resolve, 400 * (attempt + 1)));
|
|
}
|
|
}
|
|
if (!response) {
|
|
throw lastError || new Error(`Miruro API request failed for ${url}`);
|
|
}
|
|
if (!response.ok) {
|
|
const error = new Error(`Miruro API returned HTTP ${response.status}`);
|
|
error.status = response.status;
|
|
throw error;
|
|
}
|
|
const contentType = String(
|
|
response.headers && response.headers.get ? response.headers.get("content-type") || "" : ""
|
|
).toLowerCase();
|
|
if (contentType.includes("application/octet-stream")) {
|
|
return decodeCatalogPayload(new Uint8Array(await response.arrayBuffer()));
|
|
}
|
|
const raw = (await response.text()).trim();
|
|
return raw ? JSON.parse(raw) : null;
|
|
}
|
|
|
|
// src/miruro/extractor.js
|
|
var ANIMAP_URL = "https://animap.id/api/map";
|
|
var ANILIST_URL = "https://graphql.anilist.co";
|
|
var SEARCH_LIMIT = 15;
|
|
var MAX_SEARCH_QUERIES = 5;
|
|
var PROVIDER_LABELS = {
|
|
animepahe: "AnimePahe",
|
|
anikoto: "Anikoto",
|
|
aniwaves: "Aniwaves",
|
|
icarus: "Icarus",
|
|
kickassanime: "KickAssAnime"
|
|
};
|
|
var anilistCache = /* @__PURE__ */ new Map();
|
|
var episodeCache = /* @__PURE__ */ new Map();
|
|
var showCache = /* @__PURE__ */ new Map();
|
|
function cacheGet(cache, key) {
|
|
return cache.has(key) ? cache.get(key) : void 0;
|
|
}
|
|
function cacheSet(cache, key, value) {
|
|
if (cache.size >= 40) {
|
|
const oldest = cache.keys().next().value;
|
|
cache.delete(oldest);
|
|
}
|
|
cache.set(key, value);
|
|
return value;
|
|
}
|
|
function asNumber(value) {
|
|
const number = Number(value);
|
|
return Number.isFinite(number) ? number : null;
|
|
}
|
|
function asArray(value) {
|
|
if (value == null) {
|
|
return [];
|
|
}
|
|
return Array.isArray(value) ? value : [value];
|
|
}
|
|
function parseIdentifier(value) {
|
|
const id = String(value || "").trim();
|
|
if (/^tt\d+$/i.test(id)) {
|
|
return { service: "imdb", value: id.toLowerCase() };
|
|
}
|
|
if (/^\d+$/.test(id)) {
|
|
return { service: "tmdb", value: id };
|
|
}
|
|
return null;
|
|
}
|
|
function recordMatchesIdentifier(record, identifier, mediaType) {
|
|
const expectedType = mediaType === "movie" ? "movie" : "tv";
|
|
if (identifier.service === "imdb") {
|
|
const imdbMatches = asArray(record.imdb_id).some((id) => String(id).toLowerCase() === identifier.value);
|
|
const tmdbMappings = asArray(record.tmdb_id);
|
|
const typeMatches = tmdbMappings.length === 0 || tmdbMappings.some((mapping) => mapping.type === expectedType);
|
|
return imdbMatches && typeMatches;
|
|
}
|
|
return asArray(record.tmdb_id).some((mapping) => String(mapping.id) === identifier.value && mapping.type === expectedType);
|
|
}
|
|
function getMapping(records, identifier, mediaType) {
|
|
const ids = [];
|
|
const titles = [];
|
|
const synonyms = [];
|
|
const normalizedRecords = Array.isArray(records) ? records : records && typeof records === "object" ? [records] : [];
|
|
for (const record of normalizedRecords) {
|
|
if (!recordMatchesIdentifier(record, identifier, mediaType)) {
|
|
continue;
|
|
}
|
|
for (const id of asArray(record.anilist_id)) {
|
|
const numericId = asNumber(id);
|
|
if (numericId && !ids.includes(numericId)) {
|
|
ids.push(numericId);
|
|
}
|
|
}
|
|
if (record.title) {
|
|
titles.push(String(record.title));
|
|
}
|
|
for (const synonym of asArray(record.synonyms)) {
|
|
if (synonym) {
|
|
synonyms.push(String(synonym));
|
|
}
|
|
}
|
|
}
|
|
return { ids, titles, synonyms };
|
|
}
|
|
async function getAniListMeta(anilistId) {
|
|
if (anilistCache.has(anilistId)) {
|
|
return anilistCache.get(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) {
|
|
title {
|
|
romaji
|
|
english
|
|
native
|
|
}
|
|
synonyms
|
|
relations {
|
|
edges {
|
|
relationType
|
|
node {
|
|
id
|
|
type
|
|
format
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`,
|
|
variables: { id: anilistId }
|
|
})
|
|
});
|
|
const media = response && response.data && response.data.Media ? response.data.Media : null;
|
|
const edges = media && media.relations && Array.isArray(media.relations.edges) ? 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));
|
|
const meta = {
|
|
title: media && media.title ? media.title : null,
|
|
synonyms: media && Array.isArray(media.synonyms) ? media.synonyms : [],
|
|
sequelId: sequel ? asNumber(sequel.node.id) : null
|
|
};
|
|
anilistCache.set(anilistId, meta);
|
|
return meta;
|
|
}
|
|
async function getSeasonAniListId(anilistId, season) {
|
|
const requestedSeason = asNumber(season) || 1;
|
|
let currentId = anilistId;
|
|
for (let currentSeason = 1; currentSeason < requestedSeason; currentSeason++) {
|
|
const meta = await getAniListMeta(currentId);
|
|
if (!meta.sequelId || meta.sequelId === currentId) {
|
|
return null;
|
|
}
|
|
currentId = meta.sequelId;
|
|
}
|
|
return currentId;
|
|
}
|
|
function responseItems(response) {
|
|
if (Array.isArray(response)) {
|
|
return response;
|
|
}
|
|
if (response && Array.isArray(response.data)) {
|
|
return response.data;
|
|
}
|
|
if (response && Array.isArray(response.items)) {
|
|
return response.items;
|
|
}
|
|
return [];
|
|
}
|
|
function getExternalIds(anime, key) {
|
|
return asArray(anime && anime.external_ids ? anime.external_ids[key] : null).map((id) => asNumber(id)).filter((id) => id != null);
|
|
}
|
|
async function searchCatalogAnime(title) {
|
|
const response = await apiGet("anime", {
|
|
q: title,
|
|
limit: SEARCH_LIMIT,
|
|
sort: "-popularity"
|
|
});
|
|
return responseItems(response);
|
|
}
|
|
function matchesAnyAniListId(anime, aniListIds) {
|
|
const targets = new Set(aniListIds.map((id) => Number(id)));
|
|
return getExternalIds(anime, "anilist").some((id) => targets.has(Number(id)));
|
|
}
|
|
function buildSearchTitles(targetMeta, mapping) {
|
|
const titles = [
|
|
targetMeta && targetMeta.title ? targetMeta.title.romaji : null,
|
|
mapping.titles[0],
|
|
targetMeta && targetMeta.title ? targetMeta.title.english : null,
|
|
...mapping.synonyms.slice(0, 2),
|
|
...targetMeta ? targetMeta.synonyms.slice(0, 2) : []
|
|
];
|
|
const seen = /* @__PURE__ */ new Set();
|
|
return titles.filter((title) => typeof title === "string" && title.trim().length >= 3).map((title) => title.trim()).filter((title) => {
|
|
const key = title.toLowerCase();
|
|
if (seen.has(key)) {
|
|
return false;
|
|
}
|
|
seen.add(key);
|
|
return title;
|
|
}).slice(0, MAX_SEARCH_QUERIES);
|
|
}
|
|
async function findCatalogAnime(candidateIds, targetId, searchTitles) {
|
|
for (const title of searchTitles) {
|
|
let results;
|
|
try {
|
|
results = await searchCatalogAnime(title);
|
|
} catch (error) {
|
|
console.warn(`[Miruro] Catalog search "${title}" failed: ${error.message}`);
|
|
if (error.status === 403) {
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
if (results.length === 0) {
|
|
continue;
|
|
}
|
|
const exact = results.find((anime) => getExternalIds(anime, "anilist").includes(Number(targetId)));
|
|
if (exact) {
|
|
return exact;
|
|
}
|
|
const candidate = results.find((anime) => matchesAnyAniListId(anime, candidateIds));
|
|
if (candidate) {
|
|
return candidate;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
async function listEpisodes(animeId) {
|
|
const cached = cacheGet(episodeCache, animeId);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
const episodes = [];
|
|
let cursor;
|
|
for (let page = 0; page < 5; page++) {
|
|
const response = await apiGet(`anime/${encodeURIComponent(animeId)}/episodes`, {
|
|
limit: 1e4,
|
|
cursor
|
|
});
|
|
const items = responseItems(response);
|
|
episodes.push(...items);
|
|
if (!response || !response.has_more || !response.next_cursor || items.length === 0) {
|
|
break;
|
|
}
|
|
cursor = response.next_cursor;
|
|
}
|
|
return cacheSet(episodeCache, animeId, episodes);
|
|
}
|
|
async function listShowEntries(animeId) {
|
|
const cached = cacheGet(showCache, animeId);
|
|
if (cached) {
|
|
return cached;
|
|
}
|
|
let entries = [];
|
|
try {
|
|
const response = await apiGet(`anime/${encodeURIComponent(animeId)}/show`);
|
|
entries = responseItems(response);
|
|
} catch (error) {
|
|
console.warn(`[Miruro] Show cluster for ${animeId} failed: ${error.message}`);
|
|
}
|
|
return cacheSet(showCache, animeId, entries);
|
|
}
|
|
function clusterPosition(anime) {
|
|
const position = asNumber(anime && anime.show_cluster_position);
|
|
return position == null ? Number.MAX_SAFE_INTEGER : position;
|
|
}
|
|
function seasonOf(anime) {
|
|
return asNumber(anime && anime.aired_season ? anime.aired_season.number : null);
|
|
}
|
|
function entryBelongsToRequest(anime, mapping, identifier) {
|
|
if (identifier.service === "tmdb") {
|
|
const tmdbIds = asArray(anime && anime.external_ids ? anime.external_ids.tmdb_tv : null).map((id) => String(id));
|
|
if (tmdbIds.includes(identifier.value)) {
|
|
return true;
|
|
}
|
|
}
|
|
if (identifier.service === "imdb") {
|
|
const imdbIds = asArray(anime && anime.external_ids ? anime.external_ids.imdb : null).map((id) => String(id).toLowerCase());
|
|
if (imdbIds.includes(identifier.value)) {
|
|
return true;
|
|
}
|
|
}
|
|
return matchesAnyAniListId(anime, mapping.ids);
|
|
}
|
|
function isMovieFormat(anime) {
|
|
return String(anime && anime.format || "").toUpperCase() === "MOVIE";
|
|
}
|
|
async function resolveSeasonEpisode(seed, mapping, identifier, season, episode) {
|
|
const requestedSeason = asNumber(season) || 1;
|
|
const requestedEpisode = asNumber(episode);
|
|
if (!requestedEpisode || requestedEpisode < 1) {
|
|
return null;
|
|
}
|
|
const seedEpisodes = await listEpisodes(seed.id);
|
|
const showEntries = await listShowEntries(seed.id);
|
|
const related = showEntries.length > 0 ? showEntries : [seed];
|
|
const seasonCandidates = related.filter((anime) => !isMovieFormat(anime) && seasonOf(anime) === requestedSeason && entryBelongsToRequest(anime, mapping, identifier)).sort((first, second) => clusterPosition(first) - clusterPosition(second));
|
|
const seasonEpisodes = [];
|
|
for (const anime of seasonCandidates) {
|
|
const episodes = anime.id === seed.id ? seedEpisodes : await listEpisodes(anime.id);
|
|
for (const item of episodes) {
|
|
seasonEpisodes.push({ anime, episode: item });
|
|
}
|
|
}
|
|
const fromCluster = seasonEpisodes[requestedEpisode - 1];
|
|
if (fromCluster) {
|
|
return fromCluster;
|
|
}
|
|
const tagged = seedEpisodes.filter((item) => item.aired_season_number != null && Number(item.aired_season_number) === requestedSeason);
|
|
const fromTags = tagged[requestedEpisode - 1];
|
|
if (fromTags) {
|
|
return { anime: seed, episode: fromTags };
|
|
}
|
|
const local = seedEpisodes.find((item) => Number(item.episode_number) === requestedEpisode);
|
|
return local ? { anime: seed, episode: local } : null;
|
|
}
|
|
function selectMovieEpisode(episodes) {
|
|
if (episodes.length === 0) {
|
|
return null;
|
|
}
|
|
return episodes.find((item) => Number(item.episode_number) === 1) || episodes[0];
|
|
}
|
|
async function getPlayback(animeId, episodeNumber) {
|
|
const response = await apiGet(
|
|
`anime/${encodeURIComponent(animeId)}/episodes/${encodeURIComponent(episodeNumber)}/play`
|
|
);
|
|
if (response && response.data && Array.isArray(response.data.tracks)) {
|
|
return response.data;
|
|
}
|
|
return response;
|
|
}
|
|
function providerLabel(providerId) {
|
|
const id = String(providerId || "miruro").toLowerCase();
|
|
if (PROVIDER_LABELS[id]) {
|
|
return PROVIDER_LABELS[id];
|
|
}
|
|
return id.replace(/[-_]+/g, " ").replace(/\b[a-z]/g, (letter) => letter.toUpperCase());
|
|
}
|
|
function getQuality(stream) {
|
|
var _a2;
|
|
const raw = stream && ((_a2 = stream.quality) != null ? _a2 : stream.resolution);
|
|
const height = raw && typeof raw === "object" ? raw.height : raw;
|
|
const match = String(height != null ? height : "").match(/(\d{3,4})/);
|
|
if (match) {
|
|
return `${match[1]}p`;
|
|
}
|
|
return /4k/i.test(String(height != null ? height : "")) ? "4K" : "HD";
|
|
}
|
|
function normalizeHeaders(headers, streamHeaders) {
|
|
const result = {};
|
|
for (const source of [streamHeaders, headers]) {
|
|
if (!source || typeof source !== "object") {
|
|
continue;
|
|
}
|
|
for (const [key, value] of Object.entries(source)) {
|
|
if (value == null) {
|
|
continue;
|
|
}
|
|
result[key] = String(value);
|
|
}
|
|
}
|
|
if (!Object.keys(result).some((key) => key.toLowerCase() === "user-agent")) {
|
|
result["User-Agent"] = HEADERS["User-Agent"];
|
|
}
|
|
return result;
|
|
}
|
|
function normalizePlayback(playback) {
|
|
const streams = [];
|
|
const seen = /* @__PURE__ */ new Set();
|
|
const tracks = playback && Array.isArray(playback.tracks) ? playback.tracks : [];
|
|
for (const track of tracks) {
|
|
const trackKey = String(track && track.track || "").toLowerCase();
|
|
const language = trackKey === "dub" ? "DUB" : "SUB";
|
|
const providers = track && Array.isArray(track.providers) ? track.providers : [];
|
|
for (const provider of providers) {
|
|
const label = providerLabel(provider && provider.provider);
|
|
const hasSubtitles = !!(provider && Array.isArray(provider.subtitles) && provider.subtitles.length > 0);
|
|
const subtitleLabel = hasSubtitles && language === "SUB" ? " \xB7 CC" : "";
|
|
const servers = provider && Array.isArray(provider.servers) ? provider.servers : [];
|
|
for (const server of servers) {
|
|
const serverName = String(server && server.server || "default");
|
|
const serverStreams = server && Array.isArray(server.streams) ? server.streams : [];
|
|
for (const stream of serverStreams) {
|
|
const url = stream && stream.url;
|
|
if (!url || typeof url !== "string" || !/^https?:\/\//i.test(url)) {
|
|
continue;
|
|
}
|
|
if (seen.has(url)) {
|
|
continue;
|
|
}
|
|
seen.add(url);
|
|
const quality = getQuality(stream);
|
|
streams.push({
|
|
name: `Miruro \xB7 ${label} \xB7 ${language}`,
|
|
title: `[${language}] ${label} \xB7 ${serverName} \xB7 ${quality}${subtitleLabel}`,
|
|
url,
|
|
quality,
|
|
headers: normalizeHeaders(server && server.headers, stream.headers)
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return streams;
|
|
}
|
|
async function resolveAnime(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 mapping = getMapping(mappingRecords, identifier, mediaType);
|
|
if (mapping.ids.length === 0) {
|
|
return null;
|
|
}
|
|
const requestedSeason = asNumber(season) || 1;
|
|
let targetId = mapping.ids[0];
|
|
if (mediaType === "tv" && requestedSeason > 1) {
|
|
try {
|
|
const seasonAniListId = await getSeasonAniListId(mapping.ids[0], requestedSeason);
|
|
if (seasonAniListId) {
|
|
targetId = seasonAniListId;
|
|
} else if (mapping.ids[requestedSeason - 1]) {
|
|
targetId = mapping.ids[requestedSeason - 1];
|
|
}
|
|
} catch (error) {
|
|
console.warn(`[Miruro] Season ${requestedSeason} mapping failed: ${error.message}`);
|
|
if (mapping.ids[requestedSeason - 1]) {
|
|
targetId = mapping.ids[requestedSeason - 1];
|
|
}
|
|
}
|
|
}
|
|
let targetMeta = null;
|
|
try {
|
|
targetMeta = await getAniListMeta(targetId);
|
|
} catch (error) {
|
|
console.warn(`[Miruro] AniList metadata for ${targetId} failed: ${error.message}`);
|
|
}
|
|
const searchTitles = buildSearchTitles(targetMeta, mapping);
|
|
const seed = await findCatalogAnime(mapping.ids, targetId, searchTitles);
|
|
if (!seed || !seed.id) {
|
|
console.warn(`[Miruro] No catalog entry for AniList ${targetId}`);
|
|
return null;
|
|
}
|
|
let selection;
|
|
if (mediaType === "movie") {
|
|
const episodeItem = selectMovieEpisode(await listEpisodes(seed.id));
|
|
if (!episodeItem) {
|
|
console.warn(`[Miruro] No playable episode for AniList ${targetId}`);
|
|
return null;
|
|
}
|
|
selection = { anime: seed, episode: episodeItem };
|
|
} else {
|
|
selection = await resolveSeasonEpisode(seed, mapping, identifier, requestedSeason, episode);
|
|
if (!selection) {
|
|
console.warn(`[Miruro] Episode ${episode} not found for AniList ${targetId}`);
|
|
return null;
|
|
}
|
|
}
|
|
const playback = await getPlayback(selection.anime.id, selection.episode.episode_number);
|
|
return __spreadProps(__spreadValues({}, selection), { playback });
|
|
}
|
|
async function extractStreams(tmdbId, mediaType, season, episode) {
|
|
if (!tmdbId || mediaType !== "movie" && mediaType !== "tv") {
|
|
return [];
|
|
}
|
|
const resolved = await resolveAnime(tmdbId, mediaType, season, episode);
|
|
if (!resolved || !resolved.playback) {
|
|
return [];
|
|
}
|
|
return normalizePlayback(resolved.playback);
|
|
}
|
|
|
|
// src/miruro/index.js
|
|
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 };
|