Files
ani-web/SPEC.md
T
jozamudi e4dde86d85 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
2026-08-19 15:06:43 +00:00

4.2 KiB
Raw Blame History

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 .ts segments 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} in localStorage (mirrors ani-cli's -c continue 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

  1. Spike (done) — curl_cffi passes Cloudflare; all 7 steps verified.
  2. upstream.py — the 6 fetch functions above, one assert-based self-check (python upstream.py prints a chainsaw-man episode m3u8).
  3. API — FastAPI routes over upstream.py.
  4. HLS proxy — /stream/{token} with playlist rewriting.
  5. UI — search → detail → episodes → player.
  6. Polish — history, sub/dub toggle, error states.