From bee6625a9e73e829d70146dfcc0eb5e9ca959006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=81nis=20L?= Date: Tue, 27 Oct 2026 13:12:57 +0200 Subject: [PATCH] fix unmapped anime --- manifest.json | 4 +-- providers/miruro.js | 57 ++++++++++++++++++++++++++++--- src/miruro/extractor.js | 74 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 123 insertions(+), 12 deletions(-) diff --git a/manifest.json b/manifest.json index 64959ac..a4376e3 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { "name": "Nuvio Providers", - "version": "1.0.14", + "version": "1.0.15", "scrapers": [ { "id": "bcine", @@ -54,7 +54,7 @@ "id": "miruro", "name": "Miruro", "description": "Anime streams from Miruro with subbed and dubbed sources.", - "version": "1.0.6", + "version": "1.0.7", "author": "Jānis L", "supportedTypes": [ "movie", diff --git a/providers/miruro.js b/providers/miruro.js index 0e814e5..f07843c 100644 --- a/providers/miruro.js +++ b/providers/miruro.js @@ -520,6 +520,7 @@ async function apiGet(path, query = {}) { // 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; @@ -600,6 +601,45 @@ function getMapping(records, identifier, mediaType) { } 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); @@ -926,18 +966,25 @@ async function resolveAnime(mediaId, mediaType, season, episode) { if (!identifier) { return null; } - let mappingRecords; + let mapping; try { - mappingRecords = await fetchJson( + 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] No anime mapping for ${mediaId}: ${error.message}`); - return null; + console.warn(`[Miruro] AniMap lookup for ${mediaId} failed: ${error.message}`); + mapping = { ids: [], titles: [], synonyms: [] }; } - const mapping = getMapping(mappingRecords, identifier, mediaType); 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; diff --git a/src/miruro/extractor.js b/src/miruro/extractor.js index 860648a..fe8982b 100644 --- a/src/miruro/extractor.js +++ b/src/miruro/extractor.js @@ -15,6 +15,7 @@ import { HEADERS, apiGet, fetchJson } from './http.js'; const ANIMAP_URL = 'https://animap.id/api/map'; +const ANIZIP_URL = 'https://api.ani.zip/mappings'; const ANILIST_URL = 'https://graphql.anilist.co'; const SEARCH_LIMIT = 15; const MAX_SEARCH_QUERIES = 5; @@ -131,6 +132,60 @@ function getMapping(records, identifier, mediaType) { return { ids, titles, synonyms }; } +/** + * AniMap's legacy TMDB/IMDb mapping is missing for newer shows. AnimeAPI + * (api.ani.zip) indexes the same services and fills those gaps. + */ +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); @@ -611,21 +666,30 @@ async function resolveAnime(mediaId, mediaType, season, episode) { return null; } - let mappingRecords; + let mapping; try { - mappingRecords = await fetchJson( + 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] No anime mapping for ${mediaId}: ${error.message}`); - return null; + console.warn(`[Miruro] AniMap lookup for ${mediaId} failed: ${error.message}`); + mapping = { ids: [], titles: [], synonyms: [] }; } - const mapping = getMapping(mappingRecords, identifier, mediaType); + 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; }