# 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: ''` | | 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) → `