- 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.
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.
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.
hianime's subtitle 'lang' field is useless (everything says 'en'); the
real language is the label, and it is not always first — Chainsaw Man
E1 leads with Arabic. Pick label=='English', fall back to first track.
- hianime.py: capture the first .vtt from the deobfuscated embed JSON
(SUBS), expose subtitles(ep_id)
- upstream.py: subtitles() dispatcher (None for anidb, which bakes subs in)
- app.py: /api/episode/{id}/stream also returns a 'sub' stream token
- app.js: add a native <track> after hls.attachMedia (hls.js _cleanTracks
disables tracks present at attach time — that silently killed earlier
attempts) and set mode=showing so Chrome fetches the cues
- providers/{anidb,anikoto,hianime}.py: per-source modules behind the
ANI_SOURCE fallback dispatch (upstream.py _call/_providers)
- hianime.py: pystardust/ani-cli#1897 flow — search/episode-list/
servers API, HD-1 hash -> base64 embed url, window.__P blob
(base64 + XOR 'otaku-embed-v1') -> master m3u8; registers CDN hosts
and the required Referer via stream_meta()
- upstream.py: get_stream/fetch_parallel accept extra headers;
default source order hianime,anidb,anikoto (anidb.app is 503)
- app.py: /stream allowlist accepts provider-registered hosts and
sends their Referer; status probe follows the primary provider
(hianime variant URIs are relative to the variant path)
- app.js: error-time position save floors at furthest point reached
(hls.js resets currentTime to 0 before playerFail on fatal errors)
- tests: default-order + hianime import checks; live browser suite green