diff --git a/manifest.json b/manifest.json index a4376e3..675d6f4 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,6 @@ { "name": "Nuvio Providers", - "version": "1.0.15", + "version": "1.0.16", "scrapers": [ { "id": "bcine", @@ -54,7 +54,7 @@ "id": "miruro", "name": "Miruro", "description": "Anime streams from Miruro with subbed and dubbed sources.", - "version": "1.0.7", + "version": "1.0.8", "author": "Jānis L", "supportedTypes": [ "movie", diff --git a/providers/miruro.js b/providers/miruro.js index f07843c..55d8e08 100644 --- a/providers/miruro.js +++ b/providers/miruro.js @@ -714,12 +714,16 @@ function responseItems(response) { 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", { +async function searchCatalogAnime(title, includeAdult = false) { + const query = { q: title, limit: SEARCH_LIMIT, sort: "-popularity" - }); + }; + if (includeAdult) { + query.is_adult = true; + } + const response = await apiGet("anime", query); return responseItems(response); } function matchesAnyAniListId(anime, aniListIds) { @@ -744,28 +748,34 @@ function buildSearchTitles(targetMeta, mapping) { return title; }).slice(0, MAX_SEARCH_QUERIES); } +function pickCatalogMatch(results, candidateIds, targetId) { + if (results.length === 0) { + return null; + } + const exact = results.find((anime) => getExternalIds(anime, "anilist").includes(Number(targetId))); + if (exact) { + return exact; + } + return results.find((anime) => matchesAnyAniListId(anime, candidateIds)) || null; +} 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; + for (const includeAdult of [false, true]) { + const titles = includeAdult ? searchTitles.slice(0, 3) : searchTitles; + for (const title of titles) { + let results; + try { + results = await searchCatalogAnime(title, includeAdult); + } catch (error) { + console.warn(`[Miruro] Catalog search "${title}" failed: ${error.message}`); + if (error.status === 403) { + return null; + } + continue; + } + const match = pickCatalogMatch(results, candidateIds, targetId); + if (match) { + return match; } - 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; diff --git a/src/miruro/extractor.js b/src/miruro/extractor.js index fe8982b..bece430 100644 --- a/src/miruro/extractor.js +++ b/src/miruro/extractor.js @@ -285,13 +285,20 @@ function getExternalIds(anime, key) { .filter((id) => id != null); } -async function searchCatalogAnime(title) { - // Miruro rejects search limits above 15 with a generic 400. - const response = await apiGet('anime', { +async function searchCatalogAnime(title, includeAdult = false) { + // Miruro rejects search limits above 15 with a generic 400. Its catalog + // also hides adult entries unless is_adult=true is requested explicitly. + const query = { q: title, limit: SEARCH_LIMIT, sort: '-popularity' - }); + }; + + if (includeAdult) { + query.is_adult = true; + } + + const response = await apiGet('anime', query); return responseItems(response); } @@ -327,38 +334,48 @@ function buildSearchTitles(targetMeta, mapping) { .slice(0, MAX_SEARCH_QUERIES); } +function pickCatalogMatch(results, candidateIds, targetId) { + if (results.length === 0) { + return null; + } + + const exact = results.find((anime) => getExternalIds(anime, 'anilist').includes(Number(targetId))); + + if (exact) { + return exact; + } + + return results.find((anime) => matchesAnyAniListId(anime, candidateIds)) || null; +} + async function findCatalogAnime(candidateIds, targetId, searchTitles) { - for (const title of searchTitles) { - let results; + // The catalog excludes adult entries from a default search, so retry the + // same titles with is_adult=true when the normal pass found nothing. + for (const includeAdult of [false, true]) { + const titles = includeAdult ? searchTitles.slice(0, 3) : searchTitles; - try { - results = await searchCatalogAnime(title); - } catch (error) { - console.warn(`[Miruro] Catalog search "${title}" failed: ${error.message}`); + for (const title of titles) { + let results; - // A 403 means the catalog rejected the client itself (Cloudflare or - // an IP block); trying more title variants cannot help. - if (error.status === 403) { - break; + try { + results = await searchCatalogAnime(title, includeAdult); + } catch (error) { + console.warn(`[Miruro] Catalog search "${title}" failed: ${error.message}`); + + // A 403 means the catalog rejected the client itself (Cloudflare + // or an IP block); trying more title variants cannot help. + if (error.status === 403) { + return null; + } + + continue; } - continue; - } + const match = pickCatalogMatch(results, candidateIds, targetId); - 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; + if (match) { + return match; + } } }