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:
1 parent
731f44b99c
commit
0cd79dd701
4 files changed
+43
-10
No files matched your search
@@ -17,12 +17,15 @@ Shared HTTP helpers (_get/get_stream/fetch_parallel, Chrome TLS
|
||||
impersonation, TTL cache) live here so every provider gets them for free;
|
||||
they work on any URL, not just anidb's.
|
||||
"""
|
||||
import base64
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
|
||||
from Crypto.Cipher import AES
|
||||
from curl_cffi import requests
|
||||
|
||||
H = dict(impersonate="chrome", timeout=20)
|
||||
@@ -55,6 +58,26 @@ def _cached(key, ttl, fn, *a, **kw):
|
||||
def clear_cache():
|
||||
_CACHE.clear()
|
||||
|
||||
# MegaPlay's getSourcesNew hid the stream URL in an AES-256-CBC "enc" field.
|
||||
# Key/IV are static and hardcoded in their public player JS (newclient.min.js)
|
||||
# — anti-scrape theater, not access control. ponytail: breaks if they rotate
|
||||
# the key; upgrade path = re-read newclient.min.js (or let it fail: both
|
||||
# shapes are tolerated below, so the old code path still works if reverted).
|
||||
_MP_KEY = b"i?LMTAx0Q6,:}50U".ljust(32, b"\x00")
|
||||
_MP_IV = b"W0;27ToaUpl_P%'c"
|
||||
|
||||
def megaplay_sources(d):
|
||||
"""getSourcesNew response -> same dict with sources["file"] filled.
|
||||
Old players return it plaintext; current ones base64url+AES-wrap it in
|
||||
"enc" (decrypted payload is {"file": ...})."""
|
||||
if not (d.get("sources") or {}).get("file") and d.get("enc"):
|
||||
raw = base64.urlsafe_b64decode(d["enc"] + "=" * (-len(d["enc"]) % 4))
|
||||
pt = AES.new(_MP_KEY, AES.MODE_CBC, _MP_IV).decrypt(raw)
|
||||
j = json.loads(pt[:-pt[-1]]) # strip PKCS7
|
||||
if j.get("file"):
|
||||
d = {**d, "sources": {"file": j["file"]}}
|
||||
return d
|
||||
|
||||
|
||||
def _get(url, **kw):
|
||||
# ponytail: one retry (300ms) on transient upstream errors (Cloudflare
|
||||
|
||||
Reference in new issue
Block a user