Files
ani-web/README.md
T
jozamudi 0f6bd0a52a docs: sync README + SPEC with the shipped reality
Both docs still described the pre-provider-layer single-source app:

- README: anidb-only framing, missing stats page, 'upstream.py is the
  ONLY Cloudflare-facing code', and a self-check command that does not
  work (plain script execution breaks the providers' relative imports;
  the asserts are anidb-specific under the hianime-first default) —
  now documents ANI_SOURCE=anidb + the -m form. Dropped the stale
  'episode titles are not available' note (Jikan titles shipped).
- SPEC: 'no database' -> SQLite, architecture -> 3 providers, full
  endpoint list (intros/searches/events/stats/cdn, DELETE history,
  ?all=1, {sub}), auth hardening (per-IP budget, X-Aniweb on /login),
  tests/run.py -> pytest + ANIWEB_LIVE_TESTS gating, out-of-scope list
  (countdown/intro-skip/titles shipped), risks updated for the
  fallback layer, milestone 8 pointer.
2026-09-16 21:14:26 +00:00

164 lines
7.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 anime streaming: search, browse, and stream anime in
the browser (hianime.at primary; anidb.app and anikototv.to as
fallbacks). Multi-user, with per-user watch history and HLS playback
proxied through the backend. Tuned for phone as much as desktop.
![login](docs/img/login.png)
## 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:
![history](docs/img/history.png)
### 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.
![home](docs/img/home.png)
### Search
The header search box queries the active source 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 and season tabs (where the source provides
them), plus filler badges.
![search](docs/img/search.png)
### 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)
![detail](docs/img/detail.png)
### Stats
`?stats` (hamburger menu) — watch minutes, episodes started/completed,
active days + best streak, sub/dub split, weekday and per-day heatmaps,
top-10 shows, top searches. Derived server-side from the position-event
stream (`POST /api/events`), 30–365 day windows.
## Layout
```
server/ FastAPI app (app.py), source providers (providers/: hianime,
anidb, anikoto) over shared HTTP + dispatch (upstream.py),
accounts (users.py). upstream.py + providers/ are 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
- The upstream sites sit behind Cloudflare; all upstream requests use
`curl_cffi` Chrome TLS impersonation. Sources are ordered by
`ANI_SOURCE` (default `hianime,anidb,anikoto`) — a dead one falls
through to the next. All upstream-facing code is isolated in
`server/upstream.py` + `server/providers/`.
- The video CDN sends no CORS headers, so playback goes through the
backend HLS proxy (`/stream/{token}`), which rewrites playlist URIs.
- `ANI_SOURCE=anidb .venv/bin/python -m server.upstream` runs a live
self-check of the anidb fetchers (search → details → episodes → dub
stream). Must be the `-m` form — plain script execution breaks the
providers' relative imports.
- A background probe fetches 64KB from the CDN (and checks the origin
site) every 3 min and logs state flips to `server.log`; `GET
/api/cdn` (public) returns the latest throughput + ~1h history +
origin state. Use it to check if "playback broken" is the CDN again.
- The front-end is version-pinned: the server injects the git SHA as the
`?v=` cache-bust on `app.js`/`style.css` at startup (index.html itself
is `no-store`), so a restart is a cache bump — nothing to hand-bump.
- Mutations need the `X-Aniweb: 1` header (CSRF: cross-site form POSTs
can't set it), `/login` POST included — forced-login is dead.
- Login throttling (server/auth.py): per-username 5 fails -> 15 min lock,
plus a per-source-IP budget; both sliding windows, in-memory.
- 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 test suite against a
throwaway server (:8399, temp data dir — never touches the real
data). The default suite (API + auth + dispatch tests) is hermetic
and deterministic; the Playwright browser tests scrape and play LIVE
upstream content, so they are opt-in: `ANIWEB_LIVE_TESTS=1
.venv/bin/pytest tests/ -q` (needs playwright chromium installed).