# 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 # create your account (one-time) ./run.sh ``` Open it through your HTTPS proxy (or 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 # add user (prompted password) .venv/bin/python -m server.users rm # 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).