From 816e9450189b1eb31a61e98ecd99676abcd0bab3 Mon Sep 17 00:00:00 2001 From: Josue Zamudio Date: Sun, 20 Sep 2026 23:34:32 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20slim=20README=20to=20what=20it=20unique?= =?UTF-8?q?ly=20owns=20(run,=20features,=20dev=20commands);=20implementati?= =?UTF-8?q?on=20detail=20already=20lived=20in=20SPEC.md=20=E2=80=94=20move?= =?UTF-8?q?d=20the=20two=20stragglers=20(anidb=20self-check,=20cache-busti?= =?UTF-8?q?ng)=20over?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 153 ++++++++++++++---------------------------------------- SPEC.md | 8 +++ 2 files changed, 48 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index 99dad95..219e643 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,8 @@ uv pip install --python .venv/bin/python -r requirements.txt 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): +Accounts are CLI-created only (no self-signup): ```sh .venv/bin/python -m server.users add # add user (prompted password) @@ -32,132 +28,63 @@ Account management (no self-signup by design): .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. +Passwords are PBKDF2-SHA256 in `server/data/aniweb.db` (SQLite, 0600); +sessions are signed 30-day `HttpOnly` `SameSite=Lax` cookies, `Secure` +via `run.sh`, so remote plain-HTTP can't hold a session — the HTTPS +proxy is the way in. See [SPEC.md](SPEC.md) for the rest. ## Features -### Accounts & watch history +- **History** — server-side and per-user: a new device picks up your + exact watch state after login. Entries show poster, progress, and a + per-entry delete. -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. + ![history](docs/img/history.png) -The history page shows every entry with poster, progress, and a -per-entry delete button: +- **Home** — continue-watching hero (poster, position, Resume) plus a + recently-played grid with progress bars. -![history](docs/img/history.png) + ![home](docs/img/home.png) -### Home: continue watching +- **Search** — poster-grid results from the active source; your last 8 + searches (per user) in a live-filtering dropdown. Results open the + detail view (synopsis, season tabs, filler badges). -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](docs/img/search.png) -![home](docs/img/home.png) +- **Player** — resume at the saved position, skip intro (MAL timings), + dub/sub toggle, playback speed, hide fillers, auto-next with a + 5-second countdown, ✓ on completed episodes. Playback goes through a + backend HLS proxy (the CDN sends no CORS headers); segments are + pulled across parallel byte-range lanes so a slow connection doesn't + stall. -### Search + ![detail](docs/img/detail.png) -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. +- **Stats** — `?stats`: watch minutes, episodes, streaks, sub/dub + split, weekday/per-day heatmaps, top-10 shows, top searches + (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 + anidb, anikoto) over shared HTTP + dispatch (upstream.py) +static/ front-end: index.html, login.html, app.js, hls.min.js (vendored) +tests/ pytest suite: hermetic default, live browser e2e opt-in docs/img/ README screenshots run.sh launch/restart the server (sets ANIWEB_SECURE=1) -SPEC.md design + the verified anidb.app data flow +SPEC.md design, verified anidb.app data flow, security + runbook notes ``` -## Dev notes +## Dev -- 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). +- `.venv/bin/pytest tests/ -q` — hermetic suite against a throwaway + server (never touches real data). Live-upstream browser e2e is + opt-in: `ANIWEB_LIVE_TESTS=1 .venv/bin/pytest tests/ -q` + (needs playwright chromium). +- `ANI_SOURCE=anidb .venv/bin/python -m server.upstream` — live + self-check of the anidb fetchers (must be the `-m` form; plain + script execution breaks the providers' relative imports). +- Everything else (Cloudflare/impersonation, HLS proxy, CDN probe, + auth hardening, cache-busting): [SPEC.md](SPEC.md). diff --git a/SPEC.md b/SPEC.md index 4aa3261..ba21956 100644 --- a/SPEC.md +++ b/SPEC.md @@ -124,6 +124,10 @@ across parallel byte-range lanes (single-stream fallback). Views: home (continue watching), search, detail+player, full history (`?history`), stats (`?stats`); PWA-installable. +Cache-busting: the server injects the git SHA as `?v=` on +`app.js`/`style.css` at startup (index.html itself is `no-store`), so +a restart is a cache bump — nothing to hand-bump. + - Default play: opening an anime resumes the last watched episode of the season (seeking to the saved position unless ≥95% through), else autoplays episode 1. If the browser blocks autoplay, the native @@ -143,6 +147,10 @@ Use the app through your HTTPS proxy (with `Secure` cookies, plain remote HTTP can't hold a session). `.venv/bin/pytest tests/ -q` runs the hermetic suite against a throwaway server on :8399; the live-upstream browser e2e is opt-in (`ANIWEB_LIVE_TESTS=1`). +`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). ## Out of scope (v1)