diff --git a/CHANGELOG.md b/CHANGELOG.md index f95528e..655abef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.52.18 - 2026-08-30 + +- Fixed Anikoto dub availability detection when episode-list flags report sub-only episodes but the episode server list exposes dub streams. + ## 0.52.17 - 2026-08-20 - Fixed the Watchlist refresh-all progress fill color so the advancing bar is visible. diff --git a/README.md b/README.md index 081480b..d574a06 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ Kaizoku uses additive SQLite migrations for queue and watchlist schema changes. The Anikoto, AniNeko, and AnimePahe parser modules in `providers/extensions/Anime/` are adapted from [TheYogMehta/extensions](https://github.com/TheYogMehta/extensions) and retain their GPL/license headers. The bundled anime provider versions are currently: -- Anikoto `5.0.2` +- Anikoto `5.0.3` - AniNeko `3.0.3` - AnimePahe `4.0.1` diff --git a/VERSION b/VERSION index c92f670..e2783cb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.52.17 +0.52.18 diff --git a/providers/extensions/Anime/anikoto.js b/providers/extensions/Anime/anikoto.js index 52c5533..f867bdd 100644 --- a/providers/extensions/Anime/anikoto.js +++ b/providers/extensions/Anime/anikoto.js @@ -217,6 +217,46 @@ async function AnimeInfo(id) { } } +async function fetchServerTypes(dataIds) { + if (!dataIds) return new Set(); + try { + const serverUrl = `${baseUrl}/ajax/server/list?servers=${dataIds}`; + const serverRes = await global.axios.get(serverUrl, { + headers: { + "X-Requested-With": "XMLHttpRequest", + }, + timeout: 7000, + }); + const $ = cheerio.load(serverRes.data?.result || ""); + const types = new Set(); + $(".servers .type[data-type], [data-type]").each((i, el) => { + const type = String($(el).attr("data-type") || "") + .trim() + .toLowerCase(); + if (type) types.add(type); + }); + return types; + } catch (e) { + return new Set(); + } +} + +async function mapWithConcurrency(items, limit, mapper) { + const results = new Array(items.length); + let nextIndex = 0; + const workerCount = Math.min(Math.max(Number(limit) || 1, 1), items.length); + await Promise.all( + Array.from({ length: workerCount }, async () => { + while (nextIndex < items.length) { + const index = nextIndex; + nextIndex += 1; + results[index] = await mapper(items[index], index); + } + }), + ); + return results; +} + async function fetchEpisode(dataId, page = 1) { try { let numericId = dataId; @@ -247,6 +287,9 @@ async function fetchEpisode(dataId, page = 1) { const $ = cheerio.load(data.result || ""); let episodes = []; + const hasDubFilter = + $('.filter.type [data-value="dub"], [data-value="dub"]').length > 0; + $("a[data-id][data-ids], .ep-item, li a, .ssl-item, a.item").each( (i, el) => { const epNum = @@ -274,6 +317,21 @@ async function fetchEpisode(dataId, page = 1) { }, ); + const hasDubFlag = episodes.some((episode) => + (episode.langs || []).includes("dub"), + ); + if (hasDubFilter && episodes.length > 0 && !hasDubFlag) { + episodes = await mapWithConcurrency(episodes, 4, async (episode) => { + const dataIds = String(episode.id || "").split("|")[1] || ""; + const serverTypes = await fetchServerTypes(dataIds); + const langs = new Set(episode.langs || []); + if (serverTypes.has("hsub")) langs.add("sub"); + if (serverTypes.has("sub")) langs.add("sub"); + if (serverTypes.has("dub")) langs.add("dub"); + return { ...episode, langs: Array.from(langs) }; + }); + } + return { episodes: episodes, totalPages: 1, @@ -526,7 +584,7 @@ async function fetchEpisodeSources(episodeIdStr, category = null) { module.exports = { name: "anikoto", - version: "5.0.2", + version: "5.0.3", SearchAnime, AnimeInfo, fetchEpisodeSources,