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