# ani-web Web front-end for the [ani-cli](https://github.com/pystardust/ani-cli) workflow: search, browse, and stream anime in the browser via anidb.app. ## 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/users.json` (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 - Per-user accounts (CLI-created, no self-signup): 30-day signed session cookie, 5 wrong attempts → 15-min lockout per user. PBKDF2-SHA256 hashes in `server/data/users.json` (0600). - Per-user watch history (`server/data/history/.json`): position saved every ~10s, resume where you left off, top-5 "recently played" on the home page, full history page with per-entry delete. Nothing stored in the browser — new devices pick up your watch state after login, isolated per account. In-header chip shows the user, with logout + delete-account (hamburger menu). - Search with poster grid, detail view with synopsis + season tabs - HLS playback (vendored hls.js), browser ABR across 1080/720/360p; loading/buffering overlay, and a failure overlay with Retry when a stream can't be played - Dub/sub toggle - 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 (first load still pays the upstream round-trips) - Autoplays the first episode (or resumes the last watched one); if the browser blocks autoplay, a "click to play" overlay appears ## 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/ test suite (run.py orchestrator, HTTP + browser tests) 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. - `python server/upstream.py` runs a self-check of the fetchers (search → details → episodes → dub stream for Chainsaw Man). - `python tests/run.py` runs the full test suite against a throwaway server (:8399, temp data dir — never touches the real data). Browser tests need playwright (`pip install playwright && playwright install chromium`); they're 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.