Files
ani-web/SPEC.md
T
jozamudi c697500c7d multi-user auth (CLI-created accounts), per-user history, test suite
- server/users.py replaces credentials.py: users.json (0600), PBKDF2,
  one-time migration of the legacy single password to user admin
  (history.json -> history/admin.json)
- accounts are CLI-managed only: python -m server.users add|rm|list
  (no self-signup on an internet-exposed app); /signup route removed,
  login page back to username + password
- per-user 30-day signed session cookie (payload carries the
  username), per-user lockout, GET /api/me, POST /account/delete
- per-user watch history: server/data/history/<user>.json; ANIWEB_DATA
  env override for the data dir
- header chip: logged-in user, logout, delete account
- tests/: throwaway-server suite (port 8399, temp data dir) — 9 HTTP
  tests + 2 Playwright browser tests (login UI, real video playback
  behind auth); runs via python tests/run.py
2026-08-19 16:17:56 +00:00

125 lines
5.6 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`, `itsdangerous`.
- `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` → **per-user** watch
history with position, persisted to
`server/data/history/<user>.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).
- `GET /login` / `POST /login` / `POST /logout` → multi-user auth:
PBKDF2-SHA256 passwords (`server/users.py`; `users.json` 0600),
signed 30-day cookie carrying the username, 5-fail/15-min
in-memory lockout **per user**. Accounts are CLI-created only
(`python -m server.users add|rm|list`) — no self-signup. One-time
migration: legacy single-password `credentials.json` → user
`admin`; `history.json` → `history/admin.json`.
- `GET /api/me`, `POST /account/delete` → current user / self-delete
(account + history file).
## 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, episode titles (not in anidb's API — needs
a MyAnimeList API key). Add when someone asks.
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`.