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
2026-09-01 21:25:13 +00:00

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

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/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

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

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

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

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).
S
Description
No description provided
Readme
8 MiB
0 Stars 1 Watchers 0 Forks
Languages
Python 65.2%
JavaScript 25.9%
CSS 5.9%
HTML 2.1%
Shell 0.9%