Files
ani-web/SPEC.md
T
jozamudi 2c203990bc move server code into server/, update docs
- app.py + upstream.py -> server/ (relative import, __init__.py)
- static/ resolved relative to repo root, works from any CWD
- watch history migrated to server/data/history.json
- README.md: quick start, features, layout, dev notes
- SPEC.md: marked implemented, updated endpoints/run/deviations
- ignore *.tar.gz (local curl-impersonate download at repo root)
2026-08-19 15:33:31 +00:00

114 lines
4.9 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.
**Status: implemented and running.** Deviations from the original
draft: hls.js is vendored locally (CDN was a silent failure point),
history is server-side with watch-position resume (was: localStorage,
last 20), and the front-end lives in `static/` with the server code in
`server/`. See README.md.
## 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
`server/app.py` + `server/upstream.py`. Python 3.12, deps:
`fastapi`, `uvicorn`, `curl_cffi`.
- `GET /api/search?q=` → `[{id, slug_id, name, poster}]` (parse step 1)
- `GET /api/anime/{slug_id}` → `{id, name, synopsis, mal_id, seasons:[{slug_id,name,id}]}`
- `GET /api/anime/{id}/episodes` → `[{id, number, filler}]` (step 3, passthrough)
- `GET /api/episode/{ep_id}/stream?lang=eng|jpn` → `{token, qualities}`
(steps 5+6; token is base64url of the master m3u8 URL)
- `GET /api/history` / `POST /api/history` → watch history with
position, persisted to `server/data/history.json` (50 kept, top 5
returned; upsert per anime+episode)
- `GET /stream/{token}` → proxied HLS. Token = base64url of the
upstream URL. Rewrites every non-comment line of m3u8 bodies to
point back at `/stream/`; proxies master, variants, and segments
(bytes, no transcoding).
## Frontend
`static/`: single `index.html` + `app.js` + `style.css`. hls.js is
**vendored** (`static/hls.min.js`) — a CDN dependency silently
produced black screens.
Flow: search box → results grid (poster + title, click) → detail view
(synopsis under the episode grid, season tabs) → episode grid (number
+ filler badge, sub/dub toggle) → `<video>` with hls.js pointed at
`/stream/{token}`; quality follows the master playlist (auto ABR).
- Default play: opening an anime resumes the last watched episode of
the season (seeking to the saved position unless ≥95% through),
else autoplays episode 1. If the browser blocks autoplay, a
"click to play" overlay appears.
- Watch position saved to `/api/history` every ~10s + on pause/unload;
home page lists the 5 most recent entries with timecode.
## Run
```sh
uv venv .venv && uv pip install --python .venv/bin/python -r requirements.txt
./run.sh # launch/restart, logs to server.log
```
Open `http://localhost:8321` (bound to 0.0.0.0 for LAN access).
## Out of scope (v1)
Downloads (`-d`), next-episode countdown (`-N`), syncplay, intro skip,
ani-schedule integration, auth, multi-user, episode titles (not in
anidb's API — needs a MyAnimeList API key). 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 (all done)
1. Spike — curl_cffi passes Cloudflare; all 7 steps verified.
2. `server/upstream.py` — the 6 fetch functions, assert self-check.
3. API — FastAPI routes over upstream.py.
4. HLS proxy — `/stream/{token}` with playlist rewriting.
5. UI — search → detail → episodes → player.
6. Polish — history with position + resume, sub/dub toggle, error
states, autoplay default, `run.sh`.