Decode MegaPlay's encrypted getSourcesNew payload

MegaPlay (behind both hianime and anikoto) stopped returning
sources.file in plaintext and now ships the stream URL in an
AES-256-CBC 'enc' field. Key/IV are static, hardcoded in their public
player JS (newclient.min.js) — anti-scrape obscurity, not access
control. Shared megaplay_sources() in upstream.py tolerates both
shapes, so a revert upstream breaks nothing.

Also: hianime master_m3u8 now raises when servers are listed but none
resolve (403s, stale format) instead of returning None — a listed-but
dead server is a provider fault, not 'no dub', and the old None
silently short-circuited the ANI_SOURCE fallback chain.
This commit is contained in:
jozamudi committed 2026-09-29 17:17:33 +00:00
1 parent 731f44b99c
commit 0cd79dd701
4 files changed
+43 -10

No files matched your search

+3 -3
View File
@@ -10,7 +10,7 @@ times until a CDN domain actually serves us.
"""
import re
from ..upstream import _cached, _get
from ..upstream import _cached, _get, megaplay_sources
BASE = "https://anikototv.to"
XHR = {"X-Requested-With": "XMLHttpRequest"}
@@ -100,8 +100,8 @@ def _player_file(embed_url):
if not m:
return None
host = re.match(r'https?://([^/]+)/', embed_url).group(1)
d = _get(f"https://{host}/stream/getSourcesNew?id={m.group(1)}",
headers={"Referer": embed_url, "X-Requested-With": "XMLHttpRequest"}).json()
d = megaplay_sources(_get(f"https://{host}/stream/getSourcesNew?id={m.group(1)}",
headers={"Referer": embed_url, "X-Requested-With": "XMLHttpRequest"}).json())
return (d.get("sources") or {}).get("file")
+16 -7
View File
@@ -13,7 +13,7 @@ import json
import re
from urllib.parse import urljoin, urlparse
from ..upstream import _cached, _get
from ..upstream import _cached, _get, megaplay_sources
BASE = "https://hianime.at"
KEY = b"otaku-embed-v1"
@@ -106,9 +106,9 @@ def _embed_payload(embed):
return None, [], None
host = re.match(r'https?://([^/]+)', embed).group(1)
for _ in range(5):
d = _get(f"https://{host}/stream/getSourcesNew?id={mid.group(1)}",
d = megaplay_sources(_get(f"https://{host}/stream/getSourcesNew?id={mid.group(1)}",
headers={"Referer": embed, "X-Requested-With": "XMLHttpRequest"},
timeout=15).json()
timeout=15).json())
src = (d.get("sources") or {}).get("file")
if not src:
continue
@@ -129,13 +129,18 @@ def _master_m3u8(ep_id, lang="eng"):
want = "sub" if lang in ("jpn", "sub") else "dub"
# HD-1 has rotated between player chains (zokoanime blob, megaplay);
# try every listed server of the wanted type until one resolves
errs = []
for typ in (want, "sub") if want == "dub" else (want,):
for _name, h in re.findall(
for name, h in re.findall(
rf'data-type="{typ}"[\s\S]*?data-server-name="([^"]+)"'
rf'[\s\S]*?data-hash="([^"]+)"', t):
embed = base64.b64decode(h).decode()
master, subs, refr = _embed_payload(embed)
try:
master, subs, refr = _embed_payload(base64.b64decode(h).decode())
except Exception as e:
errs.append(f"{name}: {e}")
continue
if not master:
errs.append(f"{name}: no source")
continue
REFERER = refr
# prove the CDN serves us and register the hosts so the /stream proxy
@@ -154,7 +159,11 @@ def _master_m3u8(ep_id, lang="eng"):
if pick and pick.get("src"):
SUBS[ep_id] = pick["src"]
return master
return None
if not errs:
return None # no servers listed at all: a legitimate "no dub"
# listed but unresolvable (403s, stale player format) is a provider fault,
# not "no dub" — raise so the ANI_SOURCE fallback chain tries the next source
raise RuntimeError("hianime: no working server: " + "; ".join(errs[:3]))
# Provider interface. Cache keys are namespaced with "hianime:".