- 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
4.2 KiB
ani-web — Web Front-End for ani-cli
Browse, search, and stream anime from the browser, using the same anidb.app data source that ani-cli scrapes.
Verified data flow (spiked 2026-07, all 200 OK with Chrome TLS impersonation)
| Step | Request | Yields |
|---|---|---|
| 1. Search | GET anidb.app/browse?q={query} |
HTML cards: href=".../anime/{slug}-{id}", title="{name}", poster img src |
| 2. Details | GET anidb.app/anime/{slug}-{id} |
MAL id, synopsis, related seasons (same-card links) |
| 3. Episodes | GET anidb.app/api/frontend/anime/{id}/episodes |
JSON {episodes: [{id, number, number2, filler}]} |
| 4. Languages | GET anidb.app/api/frontend/episode/{ep_id}/languages |
JSON {languages: [{code: "jpn"|"eng", name, embed_url}]} — jpn=sub, eng=dub |
| 5. Embed page | GET {embed_url} |
JS blob containing file: '<master m3u8>' |
| 6. Master playlist | GET {master m3u8} (hls.anidb.app) |
variants: 1080p / 720p / 360p |
| 7. Variants + segments | GET ...m3u8, .ts |
streamable HLS — NO CORS headers |
Key constraint: anidb.app sits behind Cloudflare (plain curl → 403).
All upstream requests must use TLS-fingerprint impersonation
(verified: curl_cffi impersonate="chrome" passes the challenge).
Since the CDN sends no CORS headers, the browser cannot fetch playlists
or segments directly — all HLS traffic must go through a backend proxy.
Architecture
Browser (vanilla JS + hls.js)
│ /api/* JSON /stream/* proxied HLS
▼
Python backend (FastAPI, uvicorn)
│ curl_cffi (Chrome impersonation)
▼
anidb.app + hls.anidb.app
Two moving parts, no database, no framework on the frontend.
Backend
Python 3.12, deps: fastapi, uvicorn, curl_cffi.
GET /api/search?q=→[{id, slug, name, poster}](parse step 1)GET /api/anime/{slug_id}→{name, synopsis, mal_id, seasons:[{id,name}]}GET /api/anime/{id}/episodes→[{id, number, filler}](step 3, passthrough)GET /api/episode/{ep_id}/languages→[{code, name, embed_url}](step 4)GET /stream/{token}→ proxied HLS. Token = base64(nats) of the upstream URL. Rewrites#EXT-X-...bodies: every relative/absolute URL in the playlist is re-pointed at/stream/.... Serves master, variants, and.tssegments through the same endpoint (bytes proxied, no transcoding).- Upstream cache: embed tokens expire — cache embed→m3u8 mapping for ~10 min; cache nothing else (search is cheap).
Frontend
Single index.html + app.js + style.css. hls.js from CDN.
Flow: search box → results grid (poster + title, click) → detail view
(synopsis, season tabs) → episode list (number + filler badge,
sub/dub toggle) → <video> with hls.js pointed at
/stream/{token}; quality follows the master playlist (auto,
hls.js handles ABR).
- History: last 20
{title, season, episode}inlocalStorage(mirrors ani-cli's-ccontinue feature). - No login, no accounts, no build step.
Run
uv venv .venv && uv pip install --python .venv/bin/python fastapi uvicorn curl_cffi
.venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8321
Open http://localhost:8321.
Out of scope (v1)
Downloads (-d), next-episode countdown (-N), syncplay, intro skip,
ani-schedule integration, auth, multi-user. Add when someone asks.
Risks
- Cloudflare policy change → upstream breaks. Mitigation: swap
impersonate="chrome"for"safari"/binary curl-impersonate; all upstream access is isolated in one module (upstream.py). - anidb.app layout change → parsers break. The 4 API endpoints (steps 3–6) are clean JSON; only steps 1, 2, 5 are HTML/JS scraping.
- ToS — anidb.app is a third-party scraper site; personal use, no redistribution, bind to LAN only by default.
Milestones
- Spike (done) — curl_cffi passes Cloudflare; all 7 steps verified.
- upstream.py — the 6 fetch functions above, one assert-based
self-check (
python upstream.pyprints a chainsaw-man episode m3u8). - API — FastAPI routes over upstream.py.
- HLS proxy —
/stream/{token}with playlist rewriting. - UI — search → detail → episodes → player.
- Polish — history, sub/dub toggle, error states.