From 9c4c6437f459f2ab187a8d30a327f58a24e5b0f8 Mon Sep 17 00:00:00 2001 From: Dymas Date: Thu, 10 Sep 2026 09:28:14 +0200 Subject: [PATCH] Prefer reachable Megaplay CDN sources --- CHANGELOG.md | 5 ++++ README.md | 2 +- VERSION | 2 +- providers/extensions/Anime/anikoto.js | 36 ++++++++++++++++----------- test_app.py | 7 +++--- 5 files changed, 32 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 590fddb..7008106 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.52.32 - 2026-09-10 + +- Preferred reachable alternate Megaplay CDN source variants for Anikoto streams, fixing real episode downloads where the default decoded CDN playlist returned `HTTP 403`. +- Passed the Megaplay referer through Anikoto subtitle metadata so subtitles can download from the selected provider source. + ## 0.52.31 - 2026-09-10 - Fixed Anikoto Megaplay source resolution by decoding encrypted `enc` source responses, restoring playable dub links that were visible on the website but returned as empty sources in Kaizoku. diff --git a/README.md b/README.md index d51bd88..c075924 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,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.4` +- Anikoto `5.0.5` - AniNeko `3.0.3` - AnimePahe `4.0.1` diff --git a/VERSION b/VERSION index 389bff2..fcbd55d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.52.31 +0.52.32 diff --git a/providers/extensions/Anime/anikoto.js b/providers/extensions/Anime/anikoto.js index 38c4bc0..55c4566 100644 --- a/providers/extensions/Anime/anikoto.js +++ b/providers/extensions/Anime/anikoto.js @@ -28,6 +28,7 @@ const cheerio = require("cheerio"); const baseUrl = "https://anikototv.to"; const megaPlayEncKey = "i?LMTAx0Q6,:}50U"; const megaPlayEncIv = "W0;27ToaUpl_P%'c"; +const megaPlaySourceVariants = ["bcdn", "tcdn", ""]; function decodeMegaPlayEnc(value) { if (!value || typeof value !== "string") return null; @@ -428,21 +429,25 @@ async function processServer(server) { const domainName = new URL(iframeUrl).origin; const playerReferer = domainName + "/"; - const sourcesRes = await global.axios.get( - `${domainName}/stream/getSources?id=${playerDbId}${type ? `&type=${encodeURIComponent(type)}` : ""}${cidu ? `&cidu=${encodeURIComponent(cidu)}` : ""}`, - { - headers: { - "X-Requested-With": "XMLHttpRequest", - Referer: playerReferer, + let sourcesRes = null; + let decodedSourcesData = null; + for (const variant of megaPlaySourceVariants) { + sourcesRes = await global.axios.get( + `${domainName}/stream/getSources?id=${playerDbId}${type ? `&type=${encodeURIComponent(type)}` : ""}${cidu ? `&cidu=${encodeURIComponent(cidu)}` : ""}${variant ? `&s=${encodeURIComponent(variant)}` : ""}`, + { + headers: { + "X-Requested-With": "XMLHttpRequest", + Referer: playerReferer, + }, + timeout: 7000, }, - timeout: 7000, - }, - ); - - const decodedSourcesData = - sourcesRes.data?.sources || sourcesRes.data?.file - ? sourcesRes.data - : decodeMegaPlayEnc(sourcesRes.data?.enc); + ); + decodedSourcesData = + sourcesRes.data?.sources || sourcesRes.data?.file + ? sourcesRes.data + : decodeMegaPlayEnc(sourcesRes.data?.enc); + if (decodedSourcesData?.sources || decodedSourcesData?.file) break; + } if (decodedSourcesData && (decodedSourcesData.sources || decodedSourcesData.file)) { const rawSrc = decodedSourcesData.sources || decodedSourcesData.file; @@ -482,6 +487,7 @@ async function processServer(server) { return { url: sUrl, lang: t.label || t.language || "English", + referer: playerReferer, }; }); @@ -622,7 +628,7 @@ async function fetchEpisodeSources(episodeIdStr, category = null) { module.exports = { name: "anikoto", - version: "5.0.4", + version: "5.0.5", SearchAnime, AnimeInfo, fetchEpisodeSources, diff --git a/test_app.py b/test_app.py index 6a46d8c..7508e28 100644 --- a/test_app.py +++ b/test_app.py @@ -4952,7 +4952,7 @@ provider.fetchEpisode("123").then((data) => { def test_anikoto_process_server_decodes_encrypted_megaplay_sources(self): script = r""" -const encryptedPayload = "wdeBruh3qqn_i5wUNnyaPQQl1wp7r0SrL6KQPNFd24_nH3bjQ-zZlyDs9ryZRqOqluMFxGXpzqi4xA9i5N3T4SDfaQAFzkb6BCj2QorrmxfAd1_nBhcma5SSJZ9rSUWBrBzsNlbhi3a9aCjlekwZGRoHmqEcH3gn2pl0RNQ2JsM"; +const encryptedPayload = "wdeBruh3qqn_i5wUNnyaPbW9ZonPa4hISsp_E11QSCvgMDwqFDKp4YU05edNoqP7oBt0VrdAY8ZIJSs6S-axabGaqAYhyjxDjPGgJ58M9y9y4vWP8mNi0PiPKVQ2bHBRhkqlVFSz5WHTMs6CrfU3KVwQZ1ZH2ymo75JCecqjQJo"; global.setDynamicReferer = () => {}; global.setFallbackReferer = () => {}; global.axios = { @@ -4973,7 +4973,7 @@ global.axios = { ` }; } - if (url.includes("/stream/getSources?id=179510&type=dub&cidu=6aa1ade0620e3")) { + if (url.includes("/stream/getSources?id=179510&type=dub&cidu=6aa1ade0620e3&s=bcdn")) { return { data: { enc: encryptedPayload, tracks: [{ file: "https://cdn.example/sub.vtt", label: "English" }] } }; } throw new Error(`unexpected url ${url}`); @@ -4999,10 +4999,11 @@ provider.processServer({ name: "HD-2", type: "dub", linkId: "dub-link" }).then(( source = json.loads(proc.stdout) self.assertEqual( source["url"], - "https://cdn.imgnex.top/anime/a7e93c78e547abf7dce1e0f3f0b77977/0eb80d76b8867b42b6e6e62ebe515704/master.m3u8", + "https://ncdn.imgnex.top/anime/a7e93c78e547abf7dce1e0f3f0b77977/0eb80d76b8867b42b6e6e62ebe515704/master.m3u8", ) self.assertEqual(source["type"], "dub") self.assertEqual(source["subtitles"][0]["url"], "https://cdn.example/sub.vtt") + self.assertEqual(source["subtitles"][0]["referer"], "https://megaplay.buzz/") def test_provider_downloader_tries_next_same_provider_source_after_failure(self): calls = []