From 7444e29592c277a774baf0641c3a15f98c3f5933 Mon Sep 17 00:00:00 2001 From: Josue Zamudio Date: Tue, 8 Sep 2026 22:16:38 +0000 Subject: [PATCH] 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. --- server/providers/hianime.py | 105 ++++++++++++++++++++++++------------ server/upstream.py | 11 ++-- 2 files changed, 79 insertions(+), 37 deletions(-) diff --git a/server/providers/hianime.py b/server/providers/hianime.py index ff8442f..9a23537 100644 --- a/server/providers/hianime.py +++ b/server/providers/hianime.py @@ -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 diff --git a/server/upstream.py b/server/upstream.py index 5282c86..231d0a1 100644 --- a/server/upstream.py +++ b/server/upstream.py @@ -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