playerFail() is the single choke point for every fatal error (hls.js fatal, v.onerror, autoplay block) but never saved the position — periodic saves (timeupdate) stop when hls is destroyed, so a fatal error at e.g. 40s lost everything after the last 10s save. Call savePos() there. test_stream_failure_retry redesigned around that: it no longer needs a healthy CDN (which hangs on segment bytes intermittently). It waits only for playlist metadata (duration comes from the VOD playlist, not segment bytes), wipes the show's history, seeks to 6s (below the 10s periodic-save threshold, above the 3s minimum — so only an error-time save could persist it), aborts /stream to force a deterministic fatal, and asserts the position landed. Retry-recovery now asserts a fresh token fetch instead of segment playback.
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
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
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/history/<user>.json): 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. hls.js handles ABR across 1080/720/360p.
- Resume — autoplay on open, resumes at the saved position; the position is saved every ~10s
- 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_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. .venv/bin/python server/upstream.pyruns a self-check of the fetchers (search → details → episodes → dub stream for Chainsaw Man)..venv/bin/pytest tests/ -qruns 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.




