ani-web
Web front-end for the ani-cli workflow: search, browse, and stream anime in the browser via anidb.app.
Quick start
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):
.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/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/<user>.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_cffiChrome TLS impersonation. If that ever stops working, everything is isolated inserver/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.pyruns a self-check of the fetchers (search → details → episodes → dub stream for Chainsaw Man).python tests/run.pyruns 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.