Files
ani-web/app.py
T
jozamudi a3a58e7d3c ui: no-store cache headers, synopsis below episode grid
- no-store on everything except /stream: stale cached app.js (old CDN
  hls.js ref) was the cause of the black screen — video plays fine in a
  real Chromium once the fresh JS loads (verified via playwright)
- synopsis moved under the episode grid
2026-08-19 15:18:14 +00:00

90 lines
2.7 KiB
Python

"""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.middleware("http")
async def no_cache(request, call_next):
# ponytail: no-store on everything except /stream (segments should cache)
r = await call_next(request)
if not request.url.path.startswith("/stream"):
r.headers["Cache-Control"] = "no-store"
return r
@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")