- CDN probe no longer downloads 64KB of video (or even a variant playlist): one small master-playlist fetch proves reachability. State stays OK/DEGRADED, kbps is no longer measured, and the pill/ panel only show a rate when one exists. - hianime: 5x getSourcesNew re-roll -> 1 attempt. The enc payload is deterministic per data-id (observed identical across calls), so re-rolling only hammered the provider. - anikoto: 2x re-roll -> 1 attempt per server, same reason. Kept: idle-gated + 30-min degraded backoff from the previous commit, the single _get retry (protects real user playback from CF blips), and the origin page check (the 'down' signal). Net upstream traffic is now what an actual viewer would generate, nothing more.
139 lines
5.3 KiB
Python
139 lines
5.3 KiB
Python
"""providers/anikoto.py — anikototv.to (AniKoto / Hianime) fallback source.
|
|
|
|
Flow: search page -> watch page (numeric anime id) -> ajax episode list
|
|
(per-episode token) -> ajax server list (sub/dub groups) -> ajax/server?get=
|
|
(MegaPlay embed page) -> getSourcesNew?id= (master m3u8).
|
|
|
|
MegaPlay's CDN domains rotate per getSources call and some (e.g. norami.top)
|
|
hard-403 non-browser clients, so _master_m3u8 re-rolls the playlist a few
|
|
times until a CDN domain actually serves us.
|
|
"""
|
|
import re
|
|
|
|
from ..upstream import _cached, _get, megaplay_sources
|
|
|
|
BASE = "https://anikototv.to"
|
|
XHR = {"X-Requested-With": "XMLHttpRequest"}
|
|
_MEGAPLAY_ID = re.compile(r'data-id="(\d+)"')
|
|
|
|
|
|
def _search(q):
|
|
html = _get(f"{BASE}/search", params={"keyword": q}).text
|
|
out, seen = [], set()
|
|
for m in re.finditer(
|
|
r'href="https://anikototv\.to/watch/([a-z0-9-]+)[^"]*"[^>]*>\s*([^<]{2,80})</a>',
|
|
html):
|
|
slug, name = m.group(1), m.group(2).strip()
|
|
if slug in seen:
|
|
continue
|
|
seen.add(slug)
|
|
imgs = re.findall(r'<img src="(https://cdn\.anipixcdn\.co/[^"]+)"',
|
|
html[max(0, m.start() - 600):m.start()])
|
|
out.append({"id": 0, "slug_id": slug, "name": name,
|
|
"poster": imgs[-1] if imgs else None})
|
|
return out
|
|
|
|
|
|
def _details(slug):
|
|
t = _get(f"{BASE}/watch/{slug}").text
|
|
h1 = re.search(r'<h1[^>]*>(.*?)</h1>', t, re.S)
|
|
syn = re.search(r'class="synopsis[^"]*"[\s\S]*?class="content"[\s\S]*?>(.*?)</div>', t)
|
|
post = re.search(r'(https://cdn\.anipixcdn\.co/thumbnail/[^"\']+)', t)
|
|
aid = re.search(r'data-anime-id="(\d+)"', t)
|
|
return {"id": int(aid.group(1)) if aid else 0,
|
|
"name": re.sub(r"<[^>]+>", "", h1.group(1)).strip() if h1 else None,
|
|
"synopsis": re.sub(r"<[^>]+>", "", syn.group(1)).strip() if syn else None,
|
|
"mal_id": None,
|
|
"poster": post.group(1) if post else None,
|
|
"seasons": []}
|
|
|
|
|
|
def _ep_entries(anime_id):
|
|
d = _get(f"{BASE}/ajax/episode/list/{anime_id}", headers=XHR).json()
|
|
out = []
|
|
for tag in re.findall(r'<a[^>]+>', d.get("result") or ""):
|
|
num = re.search(r'data-num="(\d+)"', tag)
|
|
tok = re.search(r'data-ids="([^"]+)"', tag)
|
|
if num and tok:
|
|
out.append({"num": int(num.group(1)), "token": tok.group(1),
|
|
"sub": 'data-sub="1"' in tag, "dub": 'data-dub="1"' in tag})
|
|
return out
|
|
|
|
|
|
def _ep_entry(ep_id):
|
|
# ep_id encodes (anime_id, num): anime_id * 1000 + num (num < 1000)
|
|
anime_id, num = divmod(ep_id, 1000)
|
|
return next((e for e in _ep_entries(anime_id) if e["num"] == num), None)
|
|
|
|
|
|
def _episodes(anime_id):
|
|
return [{"id": anime_id * 1000 + e["num"], "number": e["num"], "filler": False}
|
|
for e in _ep_entries(anime_id)]
|
|
|
|
|
|
def _languages(ep_id):
|
|
e = _ep_entry(ep_id)
|
|
if not e:
|
|
return []
|
|
out = []
|
|
if e["sub"]:
|
|
out.append({"code": "jpn", "name": "Sub", "embed_url": "sub"})
|
|
if e["dub"]:
|
|
out.append({"code": "eng", "name": "Dub", "embed_url": "dub"})
|
|
return out
|
|
|
|
|
|
def _server_link_ids(token, want):
|
|
d = _get(f"{BASE}/ajax/server/list", params={"servers": token}, headers=XHR).json()
|
|
res = d.get("result") or ""
|
|
for m in re.finditer(r'data-type="(\w+)"[\s\S]*?<ul>([\s\S]*?)</ul>', res):
|
|
if m.group(1) == want:
|
|
return re.findall(r'data-link-id="([^"]+)"', m.group(2))
|
|
return re.findall(r'data-link-id="([^"]+)"', res) # no group split: take all
|
|
|
|
|
|
def _player_file(embed_url):
|
|
"""MegaPlay AND its re-skins (vidtube etc.): same player libs, so the
|
|
same getSourcesNew endpoint on the embed's own host."""
|
|
page = _get(embed_url, timeout=15)
|
|
m = _MEGAPLAY_ID.search(page.text)
|
|
if not m:
|
|
return None
|
|
host = re.match(r'https?://([^/]+)/', embed_url).group(1)
|
|
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")
|
|
|
|
|
|
def _master_m3u8(ep_id, lang="eng"):
|
|
e = _ep_entry(ep_id)
|
|
if not e:
|
|
return None
|
|
want = "sub" if lang in ("jpn", "sub") else "dub"
|
|
ids = _server_link_ids(e["token"], want) or _server_link_ids(e["token"], "sub")
|
|
errs = []
|
|
for lid in ids[:3]:
|
|
d = _get(f"{BASE}/ajax/server", params={"get": lid}, headers=XHR).json()
|
|
url = (d.get("result") or {}).get("url")
|
|
if not url:
|
|
continue
|
|
# ponytail: one attempt per server — the enc payload is deterministic,
|
|
# re-rolling would just spam the provider
|
|
try:
|
|
f = _player_file(url)
|
|
if not f:
|
|
break
|
|
_get(f, timeout=10) # prove this CDN serves us before committing
|
|
return f
|
|
except Exception as ex:
|
|
errs.append(str(ex)[:60])
|
|
raise RuntimeError("anikoto: no working server: " + "; ".join(errs[:3]))
|
|
|
|
|
|
# Provider interface (cache keys namespaced "anikoto:").
|
|
search = lambda q: _cached(f"anikoto:search:{q.lower()}", 300, _search, q)
|
|
details = lambda s: _cached(f"anikoto:details:{s}", 3600, _details, s)
|
|
episodes = lambda i: _cached(f"anikoto:episodes:{i}", 300, _episodes, i)
|
|
languages = lambda e: _cached(f"anikoto:languages:{e}", 3600, _languages, e)
|
|
master_m3u8 = lambda e, l="eng": _cached(f"anikoto:master:{e}:{l}", 3600, _master_m3u8, e, l)
|