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
This commit is contained in:
commit
e4dde86d85
10 files changed
+1151
No files matched your search
@@ -0,0 +1,100 @@
|
||||
# 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.
|
||||
Reference in new issue
Block a user