ani-web: FastAPI + HLS proxy over anidb.app, vanilla UI
- upstream.py: Cloudflare-proof fetchers (curl_cffi), self-checking
- app.py: /api/* routes + /stream/{token} HLS proxy (CDN has no CORS)
- static/: search -> detail/seasons/episodes -> hls.js player, history
- verified end-to-end: search chainsaw man -> ep1 dub 1080p segment
This commit is contained in:
commit
e4dde86d85
10 files changed
+1151
No files matched your search
@@ -0,0 +1,80 @@
|
||||
"""app.py — ani-web: FastAPI over upstream.py + HLS proxy + static UI.
|
||||
|
||||
Browser playback: hls.anidb.app sends no CORS headers, so the player is
|
||||
pointed at /stream/{token}; the proxy fetches upstream (with the same
|
||||
Cloudflare workaround) and rewrites playlist URIs to go through itself.
|
||||
"""
|
||||
import base64
|
||||
from urllib.parse import urljoin
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Query
|
||||
from fastapi.responses import Response
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
import upstream
|
||||
|
||||
app = FastAPI(title="ani-web")
|
||||
|
||||
|
||||
def _b64(url: str) -> str:
|
||||
return base64.urlsafe_b64encode(url.encode()).decode()
|
||||
|
||||
|
||||
def _url(token: str) -> str:
|
||||
return base64.urlsafe_b64decode(token.encode()).decode()
|
||||
|
||||
|
||||
def _upstream(fn, *a, **kw):
|
||||
try:
|
||||
return fn(*a, **kw)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(502, f"upstream error: {e}")
|
||||
|
||||
|
||||
@app.get("/api/search")
|
||||
def api_search(q: str = Query(min_length=1)):
|
||||
return _upstream(upstream.search, q)
|
||||
|
||||
|
||||
@app.get("/api/anime/{slug_id}")
|
||||
def api_anime(slug_id: str):
|
||||
return _upstream(upstream.details, slug_id)
|
||||
|
||||
|
||||
@app.get("/api/anime/{anime_id}/episodes")
|
||||
def api_episodes(anime_id: int):
|
||||
return _upstream(upstream.episodes, anime_id)
|
||||
|
||||
|
||||
@app.get("/api/episode/{ep_id}/stream")
|
||||
def api_stream(ep_id: int, lang: str = "eng"):
|
||||
master = _upstream(upstream.master_m3u8, ep_id, lang)
|
||||
if not master:
|
||||
raise HTTPException(404, f"no {lang} stream for episode {ep_id}")
|
||||
return {"token": _b64(master),
|
||||
"qualities": _upstream(upstream.master_variants, master)}
|
||||
|
||||
|
||||
def _rewrite_m3u8(text: str, base: str) -> str:
|
||||
# ponytail: rewrites plain URIs only; URI-list attributes in
|
||||
# #EXT-X-* tags (e.g. SUBTITLES) are ignored — fine for video-only playlists
|
||||
return "\n".join(
|
||||
line if line.startswith("#") or not line
|
||||
else "/stream/" + _b64(urljoin(base, line))
|
||||
for line in text.splitlines()) + "\n"
|
||||
|
||||
|
||||
@app.get("/stream/{token}")
|
||||
def stream(token: str):
|
||||
# ponytail: buffers whole response in memory; .ts segments are small
|
||||
url = _url(token)
|
||||
r = _upstream(upstream._get, url)
|
||||
body = r.content
|
||||
if url.endswith(".m3u8") or "mpegurl" in r.headers.get("content-type", ""):
|
||||
body = _rewrite_m3u8(body.decode(), url).encode()
|
||||
return Response(body, media_type=r.headers.get("content-type", "application/octet-stream"))
|
||||
|
||||
|
||||
app.mount("/", StaticFiles(directory="static", html=True), name="static")
|
||||
Reference in new issue
Block a user