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

101 lines
4.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.