Add animekai source with 1anime direct-MP4 backend (independent CDN)
animekai.be fronted two server backends per episode: megaplay.buzz (same player/CDN family as hianime — 403s our egress IP) and my.1anime.site (direct progressive MP4, independent CDN — verified serving our IP at ~2.4MB/s). This is the rescue path around the MegaPlay Cloudflare block, per the ani-cli #1945/#1918 work. - providers/animekai.py: standard interface, per-episode backend dispatch (1anime first, then megaplay), same listed-but-dead -> raise semantics as hianime. All endpoints ride the shared TTL cache (no re-fetching on repeat UI hits). - upstream.py: megaplay_master() moved out of hianime — the MegaPlay chain (embed -> data-id -> getSourcesNew -> enc decrypt -> CDN proof) is now shared by every provider fronting that player. - app.py: /stream forwards Range for progressive files (206 + Content-Range passthrough, partial bodies never LRU-cached, 8MB cap on the parallel path so a 250MB MP4 can't pin RAM). api_stream reports hls=true/false from the URL — the player kind is a property of the URL, so future stream types slot in without app changes. - app.js: hls===false -> native <video> (existing Range-seeking), hls.js path untouched. - run.sh: ANI_SOURCE=animekai,hianime,anidb,anikoto (revert to the old chain when the MegaPlay block lifts). - tests: test_animekai.py (offline: interface, enc decrypt, /stream Range via monkeypatched network) + test_animekai_live.py (opt-in ANI_LIVE=1 end-to-end, skipped by default so the suite never touches animekai.be).
This commit is contained in:
1 parent
e3ef51d141
commit
d12ff79d29
8 files changed
+504
-35
No files matched your search
+26
-5
@@ -565,6 +565,9 @@ def api_stream(ep_id: int, lang: str = "jpn", request: Request = None):
|
||||
if not master:
|
||||
raise HTTPException(404, f"no {lang} stream for episode {ep_id}")
|
||||
out = {"token": _b64(master)}
|
||||
# the player kind is a property of the URL, not the provider: .m3u8
|
||||
# -> hls.js, anything else -> native <video> (app.js branches on this)
|
||||
out["hls"] = master.endswith(".m3u8")
|
||||
sub = upstream.subtitles(ep_id) # external .vtt (hianime); anidb bakes subs in
|
||||
if sub:
|
||||
out["sub"] = _b64(sub)
|
||||
@@ -615,9 +618,11 @@ def _cache_put(url, ct, body):
|
||||
_STREAM_CACHE.popitem(last=False)
|
||||
|
||||
|
||||
def _seg_stream(r, url, ct):
|
||||
def _seg_stream(r, url, ct, cache=True):
|
||||
"""Forward the CDN's bytes as they arrive; keep the LRU warm for
|
||||
completed downloads (a truncated one would poison the cache)."""
|
||||
completed downloads (a truncated one would poison the cache). cache=False
|
||||
for ranged file chunks — a partial body under the full-URL key would
|
||||
poison the LRU."""
|
||||
import time
|
||||
t0, t_first, buf, complete = time.time(), None, [], False
|
||||
try:
|
||||
@@ -629,7 +634,7 @@ def _seg_stream(r, url, ct):
|
||||
complete = True
|
||||
finally:
|
||||
r.close()
|
||||
if complete and buf:
|
||||
if complete and buf and cache:
|
||||
_cache_put(url, ct, b"".join(buf))
|
||||
# observability: a slow segment is the fragLoadTimeOut culprit. Log
|
||||
# only the slow ones so normal playback stays quiet. TTFB>3s or
|
||||
@@ -659,7 +664,7 @@ def _stream_playlist(url, meta=None):
|
||||
|
||||
|
||||
@app.get("/stream/{token}")
|
||||
def stream(token: str):
|
||||
def stream(token: str, request: Request = None):
|
||||
url = _url(token)
|
||||
p = urlparse(url)
|
||||
# ponytail: token is plain base64, so allowlist the host — otherwise any
|
||||
@@ -677,6 +682,19 @@ def stream(token: str):
|
||||
return Response(hit[1], media_type=hit[0], headers=_STREAM_CACHE_HDRS)
|
||||
if url.endswith(".m3u8"):
|
||||
return _stream_playlist(url, meta)
|
||||
# progressive file (1anime's direct MP4): the browser seeks with Range —
|
||||
# forward it, stream the chunk back, forward 206/Content-Range, never
|
||||
# cache (partial bodies under the full-URL key would poison the LRU)
|
||||
rng = request.headers.get("range") if request else None
|
||||
if rng:
|
||||
r = _upstream(upstream.get_stream, url, timeout=30,
|
||||
headers={**(meta or {}), "Range": rng})
|
||||
ct = r.headers.get("content-type", "video/mp4")
|
||||
return StreamingResponse(
|
||||
_seg_stream(r, url, ct, cache=False),
|
||||
status_code=r.status_code, media_type=ct,
|
||||
headers={k: v for k, v in r.headers.items()
|
||||
if k.lower() in ("content-range", "accept-ranges")})
|
||||
# segment miss: try parallel byte-range lanes first (4 concurrent fetches
|
||||
# beat per-connection CDN throttling); ANY failure falls back to the
|
||||
# single streaming path, so this can never be worse than before
|
||||
@@ -698,7 +716,10 @@ def stream(token: str):
|
||||
name = url.rsplit("/", 1)[-1][:40]
|
||||
print(f"[stream-slow] {name} parallel total={time.time()-t0:.1f}s "
|
||||
f"bytes={len(body)}", flush=True)
|
||||
_cache_put(url, ct, body)
|
||||
# ponytail: 8MB cap — the LRU is for playlists/segments; a multi-hundred-MB
|
||||
# progressive file (1anime MP4) would evict everything and pin RAM
|
||||
if len(body) < 8 * 1024 * 1024:
|
||||
_cache_put(url, ct, body)
|
||||
return Response(body, media_type=ct, headers=_STREAM_CACHE_HDRS)
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user