README: storage is SQLite (aniweb.db), not JSON files; CDN serves a single quality (no ABR) — replaced the wrong 1080/720/360 claim; document the 16-lane parallel fetch, intro-skip, episode names, buffering UI, /api/cdn + background probe, the ?v= cache-busting rule, and the client self-report diagnostics. SPEC: same single-rendition correction (2 places). PLAN: marked complete (all items shipped; deferred list was stale).
148 lines
6.2 KiB
Markdown
148 lines
6.2 KiB
Markdown
# ani-web
|
||
|
||
Web front-end for anime streaming: search, browse, and stream anime in
|
||
the browser via anidb.app. Multi-user, with per-user watch history and
|
||
HLS playback proxied through the backend. Tuned for phone as much as
|
||
desktop.
|
||
|
||

|
||
|
||
## Quick start
|
||
|
||
```sh
|
||
uv venv .venv
|
||
uv pip install --python .venv/bin/python -r requirements.txt
|
||
.venv/bin/python -m server.users add <you> # create your account (one-time)
|
||
./run.sh
|
||
```
|
||
|
||
Open it through your HTTPS proxy (or <http://localhost:8321> from the
|
||
host itself — browsers treat localhost as trusted). `run.sh` restarts
|
||
cleanly if the server is already running; logs go to `server.log`.
|
||
With `ANIWEB_SECURE=1` (set by `run.sh`), the session cookie is
|
||
`Secure`, so remote plain-HTTP access can't hold a session — only the
|
||
HTTPS proxy works. A legacy `credentials.json` password, if any,
|
||
is migrated to user `admin` on first start.
|
||
|
||
Account management (no self-signup by design):
|
||
|
||
```sh
|
||
.venv/bin/python -m server.users add <name> # add user (prompted password)
|
||
.venv/bin/python -m server.users rm <name> # remove user + their history
|
||
.venv/bin/python -m server.users list # list users
|
||
```
|
||
|
||
Security: passwords are PBKDF2-SHA256 (210k iters, per-user salts) in
|
||
`server/data/aniweb.db` (SQLite, 0600). Sessions are signed 30-day `HttpOnly`
|
||
`SameSite=Lax` cookies; `run.sh` sets `ANIWEB_SECURE=1` so cookies are
|
||
`Secure` and only work through your HTTPS proxy. Accounts deleted via
|
||
the app or CLI are rejected immediately, even with an old cookie.
|
||
|
||
## Features
|
||
|
||
### Accounts & watch history
|
||
|
||
Accounts are created from the CLI only — there is no signup screen,
|
||
because the app is meant to sit behind your proxy on the open
|
||
internet. Each user gets a fully isolated watch history
|
||
(`server/data/aniweb.db`, SQLite): nothing is stored in the browser,
|
||
so a new device picks up your exact watch state after login. Deleting
|
||
an account (hamburger menu → delete, or `server.users rm`) kills its
|
||
session cookies immediately and removes its history.
|
||
|
||
The history page shows every entry with poster, progress, and a
|
||
per-entry delete button:
|
||
|
||

|
||
|
||
### Home: continue watching
|
||
|
||
The home screen leads with a **continue watching** hero — the most
|
||
recently watched episode with its poster, position, and a Resume
|
||
button — followed by a **recently played** grid with progress bars and
|
||
relative timestamps.
|
||
|
||

|
||
|
||
### Search
|
||
|
||
The header search box queries anidb.app and shows results as a poster
|
||
grid. Your last 8 searches (per user) appear in a dropdown under the
|
||
box, filter live as you type, and click to re-run. Results open the
|
||
detail view: synopsis, stats, season tabs, and filler badges.
|
||
|
||

|
||
|
||
### Player
|
||
|
||
Playback is HLS through a backend proxy (`/stream/{token}`) — the
|
||
video CDN sends no CORS headers, so the browser can't fetch it
|
||
directly. The CDN serves a single quality (no ABR); the proxy pulls
|
||
each segment across 16 parallel byte-range lanes (falling back to a
|
||
single stream if the CDN refuses ranges), so a slow connection doesn't
|
||
stall playback.
|
||
|
||
- **Resume** — autoplay on open, resumes at the saved position; the
|
||
position is saved every ~10s
|
||
- **Skip intro** — MAL/Jikan intro timings (30-day cache) drive a
|
||
"skip intro ⏭" pill while the intro window is on screen
|
||
- **Episode names** — MAL titles (via keyless Jikan) in the episode
|
||
tooltips and the "now playing" line
|
||
- **Buffering UI** — spinner overlay, a buffer-ahead gauge under the
|
||
video, and a CDN chip (live KB/s from `/api/cdn`, amber when the CDN
|
||
is degraded)
|
||
- **Dub/sub toggle**, **playback speed** (0.5–2×, remembered), and a
|
||
**hide fillers** switch (fillers are badged and skipped by
|
||
prev/next/auto-next)
|
||
- **Auto-next** — when an episode ends, a 5-second countdown offers the
|
||
next episode; Skip jumps straight to it, ✕ cancels
|
||
- **Watched state** — completed episodes are dimmed with a ✓, the next
|
||
unwatched one is highlighted
|
||
- **Failure overlay** with a Retry button if a stream can't be played,
|
||
plus loading/buffering indicators
|
||
- **Fast repeat navigation** — upstream data is TTL-cached and HLS
|
||
playlists/segments are LRU-cached, so re-opening a show or re-seeking
|
||
is near-instant (the first load still pays the upstream round-trips)
|
||
|
||

|
||
|
||
## Layout
|
||
|
||
```
|
||
server/ FastAPI app (app.py), anidb.app fetchers (upstream.py),
|
||
accounts (users.py). upstream.py is the ONLY Cloudflare-facing code
|
||
static/ front-end: index.html, login.html, app.js, style.css, hls.min.js (vendored)
|
||
tests/ pytest suite: conftest.py (throwaway server), HTTP + browser tests
|
||
docs/img/ README screenshots
|
||
run.sh launch/restart the server (sets ANIWEB_SECURE=1)
|
||
SPEC.md design + the verified anidb.app data flow
|
||
```
|
||
|
||
## Dev notes
|
||
|
||
- anidb.app is behind Cloudflare; all upstream requests use
|
||
`curl_cffi` Chrome TLS impersonation. If that ever stops working,
|
||
everything is isolated in `server/upstream.py`.
|
||
- The video CDN sends no CORS headers, so playback goes through the
|
||
backend HLS proxy (`/stream/{token}`), which rewrites playlist URIs.
|
||
- `.venv/bin/python server/upstream.py` runs a self-check of the
|
||
fetchers (search → details → episodes → dub stream for Chainsaw Man).
|
||
- A background probe fetches 64KB from the CDN every 3 min and logs
|
||
state flips to `server.log`; `GET /api/cdn` (public) returns the
|
||
latest throughput + ~1h history. Use it to check if "playback
|
||
broken" is the CDN again.
|
||
- The front-end is version-pinned: bump `?v=N` on `app.js`/`style.css`
|
||
in `index.html` on every static change — phones/proxies otherwise
|
||
serve stale JS (index.html itself is `no-store`).
|
||
- Playback failures self-report: the client posts a step timeline +
|
||
capability fingerprint to `/api/events` (see the `events` table),
|
||
so a broken browser is diagnosable from the server.
|
||
- `.venv/bin/pytest tests/ -q` runs the full test suite against a
|
||
throwaway server (:8399, temp data dir — never touches the real
|
||
data). The API tests use stdlib only; the browser tests need
|
||
playwright (`uv pip install playwright && playwright install
|
||
chromium`) and are skipped if absent.
|
||
- Episode titles are not available: anidb's episode API only returns
|
||
`{id, number, number2, filler}`. Adding titles needs a free
|
||
MyAnimeList API key.
|