Keep hianime alive across server rotation: try every listed server

hianime's HD-1 slot rotated to a megaplay chain whose CDN (norami.top)
is Cloudflare-blocked for us, which killed stream resolution (CDN probe
sat at 0 KB/s). The ZokoAnime server still serves the working
zokoanime chain. _master_m3u8 now walks every listed server of the
wanted type; _embed_payload handles both chains (window.__P blob and
megaplay data-id/getSourcesNew with re-roll). stream_meta returns {}
when the host is ours but no referer is needed, so the /stream
allowlist still holds.
This commit is contained in:
jozamudi committed 2026-09-08 22:16:38 +00:00
1 parent 65da3e848f
commit 7444e29592
2 files changed
+79 -37

No files matched your search

+72 -33
View File
@@ -72,9 +72,9 @@ def _servers(ep_id):
def _languages(ep_id):
t = _servers(ep_id)
out = []
if re.search(r'data-type="sub"[\s\S]*?data-server-name="HD-1"', t):
if 'data-type="sub"' in t:
out.append({"code": "jpn", "name": "Sub", "embed_url": "sub"})
if re.search(r'data-type="dub"[\s\S]*?data-server-name="HD-1"', t):
if 'data-type="dub"' in t:
out.append({"code": "eng", "name": "Dub", "embed_url": "dub"})
return out
@@ -84,39 +84,76 @@ def _deobfuscate(blob):
return json.loads(bytes(b ^ KEY[i % len(KEY)] for i, b in enumerate(raw)))
def _embed_payload(embed):
"""embed url -> (master_m3u8, [{label, src} subs], referer_or_None).
Two player chains have appeared: zokoanime (window.__P blob,
base64+XOR) and megaplay (page needs the hianime referer; data-id ->
getSourcesNew; CDN domains rotate and most 403 us, so re-roll until
one serves)."""
page = _get(embed, timeout=15).text
blob = re.search(r'window\.__P="([^"]+)"', page)
if blob:
j = _deobfuscate(blob.group(1))
subs = [{"label": (x or {}).get("label", ""), "src": (x or {}).get("src")}
for x in j.get("subtitles") or []]
return j.get("src"), subs, re.match(r'https?://[^/]+', embed).group(0) + "/"
# megaplay: without the hianime referer the page is an error stub with no data-id
if not re.search(r'data-id="\d+"', page):
page = _get(embed, headers={"Referer": BASE + "/"}, timeout=15).text
mid = re.search(r'data-id="(\d+)"', page)
if not mid:
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)}",
headers={"Referer": embed, "X-Requested-With": "XMLHttpRequest"},
timeout=15).json()
src = (d.get("sources") or {}).get("file")
if not src:
continue
subs = [{"label": (x or {}).get("label", ""), "src": (x or {}).get("file")}
for x in d.get("tracks") or []]
for cand, refr in (({"Referer": embed}, embed), ({}, None)):
try:
_get(src, headers=cand, timeout=10)
return src, subs, refr
except Exception:
continue
return None, [], None
def _master_m3u8(ep_id, lang="eng"):
global REFERER, STREAM_HOSTS
t = _servers(ep_id)
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
for typ in (want, "sub") if want == "dub" else (want,):
m = re.search(rf'data-type="{typ}"[\s\S]*?data-server-name="HD-1"'
r'[\s\S]*?data-hash="([^"]+)"', t)
if not m:
continue
embed = base64.b64decode(m.group(1)).decode()
REFERER = re.match(r'https?://[^/]+', embed).group(0) + "/"
j = _deobfuscate(re.search(r'window\.__P="([^"]+)"',
_get(embed, timeout=15).text).group(1))
master = j.get("src")
if not master:
continue
# prove the CDN serves us (it 403s without the referer) and register
# the hosts so the /stream proxy allowlist survives CDN rotation
r = _get(master, headers={"Referer": REFERER}, timeout=10)
hosts = {urlparse(master).netloc}
for line in r.text.splitlines():
if line and not line.startswith("#"):
hosts.add(urlparse(urljoin(master, line)).netloc)
STREAM_HOSTS |= hosts
subs = j.get("subtitles") or []
# the 'lang' field is useless (everything says 'en'); the real language
# is the label — and it isn't first (Chainsaw Man E1 leads with Arabic)
pick = next((x for x in subs if (x or {}).get("label", "").lower() == "english"), None)
if not pick and subs:
pick = subs[0]
if pick and pick.get("src"):
SUBS[ep_id] = pick["src"]
return master
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)
if not master:
continue
REFERER = refr
# prove the CDN serves us and register the hosts so the /stream proxy
# allowlist survives CDN rotation
r = _get(master, headers={"Referer": refr} if refr else {}, timeout=10)
hosts = {urlparse(master).netloc}
for line in r.text.splitlines():
if line and not line.startswith("#"):
hosts.add(urlparse(urljoin(master, line)).netloc)
STREAM_HOSTS |= hosts
# the 'lang' field is useless (everything says 'en'); the real language
# is the label — and it isn't first (Chainsaw Man E1 leads with Arabic)
pick = next((x for x in subs if (x or {}).get("label", "").lower() == "english"), None)
if not pick and subs:
pick = subs[0]
if pick and pick.get("src"):
SUBS[ep_id] = pick["src"]
return master
return None
@@ -129,9 +166,11 @@ master_m3u8 = lambda e, l="eng": _cached(f"hianime:master:{e}:{l}", 3600, _maste
def stream_meta(url):
"""{Referer} for our own playlist/segment hosts, else None."""
if REFERER and urlparse(url).netloc in STREAM_HOSTS:
return {"Referer": REFERER}
"""{Referer} (or {}) for our own playlist/segment hosts, else None —
non-None doubles as the /stream allowlist; {} = host is ours but the
CDN needs no referer."""
if urlparse(url).netloc in STREAM_HOSTS:
return {"Referer": REFERER} if REFERER else {}
return None
+7 -4
View File
@@ -196,12 +196,15 @@ master_m3u8 = lambda e, l="eng": _call("master_m3u8", e, l)
def stream_meta(url):
"""Provider hook: extra upstream headers for a /stream URL (hianime's
CDN demands the embed origin as Referer), or None if no enabled
provider claims the host — that doubles as the allowlist."""
CDN may demand the embed origin as Referer), or None if no enabled
provider claims the host — that doubles as the allowlist (an empty
dict means: allowed, no extra headers)."""
for p in _providers():
fn = getattr(_prov(p), "stream_meta", None)
if fn and fn(url):
return fn(url)
if fn:
v = fn(url)
if v is not None:
return v
return None