// 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 ANIZIP_URL = "https://api.ani.zip/mappings"; 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 getAniZipMapping(identifier) { const queryKey = identifier.service === "tmdb" ? "themoviedb_id" : "imdb_id"; try { const response = await fetchJson( `${ANIZIP_URL}?${queryKey}=${encodeURIComponent(identifier.value)}`, { headers: { "Accept": "application/json" } } ); const entries = Array.isArray(response) ? response : [response]; let mapping = null; let titleTable = {}; for (const entry of entries) { const candidate = entry && entry.mappings ? entry.mappings : entry; if (candidate && asNumber(candidate.anilist_id)) { mapping = candidate; titleTable = entry && entry.titles && typeof entry.titles === "object" ? entry.titles : {}; break; } } if (!mapping) { return null; } const titles = []; const synonyms = []; for (const key of ["x-jat", "en", "ja"]) { if (titleTable[key]) { titles.push(String(titleTable[key])); } } for (const [key, value] of Object.entries(titleTable)) { if (!["x-jat", "en", "ja"].includes(key) && value) { synonyms.push(String(value)); } } return { ids: [asNumber(mapping.anilist_id)], titles, synonyms }; } catch (error) { console.warn(`[Miruro] AnimeAPI mapping for ${identifier.value} failed: ${error.message}`); return null; } } 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 continuation = related.filter((anime) => !isMovieFormat(anime) && seasonOf(anime) != null && seasonOf(anime) >= requestedSeason && entryBelongsToRequest(anime, mapping, identifier)).sort((first, second) => seasonOf(first) - seasonOf(second) || clusterPosition(first) - clusterPosition(second)); let offset = 0; for (const anime of continuation) { const episodes = anime.id === seed.id ? seedEpisodes : await listEpisodes(anime.id); if (episodes.length === 0) { continue; } const index = requestedEpisode - 1 - offset; if (index >= 0 && index < episodes.length) { return { anime, episode: episodes[index] }; } offset += episodes.length; } 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 mapping; try { const mappingRecords = await fetchJson( `${ANIMAP_URL}/${identifier.service}/${encodeURIComponent(identifier.value)}`, { headers: { "Accept": "application/json" } } ); mapping = getMapping(mappingRecords, identifier, mediaType); } catch (error) { console.warn(`[Miruro] AniMap lookup for ${mediaId} failed: ${error.message}`); mapping = { ids: [], titles: [], synonyms: [] }; } if (mapping.ids.length === 0) { const fallback = await getAniZipMapping(identifier); if (fallback) { mapping = fallback; } } if (mapping.ids.length === 0) { console.warn(`[Miruro] No anime mapping for ${mediaId}`); 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 };