docs: slim README to what it uniquely owns (run, features, dev commands); implementation detail already lived in SPEC.md — moved the two stragglers (anidb self-check, cache-busting) over
This commit is contained in:
1 parent
cea04a20f6
commit
816e945018
2 files changed
+44
-109
No files matched your search
@@ -19,12 +19,8 @@ uv pip install --python .venv/bin/python -r requirements.txt
|
|||||||
Open it through your HTTPS proxy (or <http://localhost:8321> from the
|
Open it through your HTTPS proxy (or <http://localhost:8321> from the
|
||||||
host itself — browsers treat localhost as trusted). `run.sh` restarts
|
host itself — browsers treat localhost as trusted). `run.sh` restarts
|
||||||
cleanly if the server is already running; logs go to `server.log`.
|
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
|
```sh
|
||||||
.venv/bin/python -m server.users add <name> # add user (prompted password)
|
.venv/bin/python -m server.users add <name> # add user (prompted password)
|
||||||
@@ -32,132 +28,63 @@ Account management (no self-signup by design):
|
|||||||
.venv/bin/python -m server.users list # list users
|
.venv/bin/python -m server.users list # list users
|
||||||
```
|
```
|
||||||
|
|
||||||
Security: passwords are PBKDF2-SHA256 (210k iters, per-user salts) in
|
Passwords are PBKDF2-SHA256 in `server/data/aniweb.db` (SQLite, 0600);
|
||||||
`server/data/aniweb.db` (SQLite, 0600). Sessions are signed 30-day `HttpOnly`
|
sessions are signed 30-day `HttpOnly` `SameSite=Lax` cookies, `Secure`
|
||||||
`SameSite=Lax` cookies; `run.sh` sets `ANIWEB_SECURE=1` so cookies are
|
via `run.sh`, so remote plain-HTTP can't hold a session — the HTTPS
|
||||||
`Secure` and only work through your HTTPS proxy. Accounts deleted via
|
proxy is the way in. See [SPEC.md](SPEC.md) for the rest.
|
||||||
the app or CLI are rejected immediately, even with an old cookie.
|
|
||||||
|
|
||||||
## Features
|
## 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
|
||||||
Accounts are created from the CLI only — there is no signup screen,
|
per-entry delete.
|
||||||
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:
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Home: continue watching
|
- **Home** — continue-watching hero (poster, position, Resume) plus a
|
||||||
|
recently-played grid with progress bars.
|
||||||
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
|
- **Search** — poster-grid results from the active source; your last 8
|
||||||
|
searches (per user) in a live-filtering dropdown. Results open the
|
||||||
The header search box queries the active source and shows results as a
|
detail view (synopsis, season tabs, filler badges).
|
||||||
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.
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Player
|
- **Player** — resume at the saved position, skip intro (MAL timings),
|
||||||
|
dub/sub toggle, playback speed, hide fillers, auto-next with a
|
||||||
Playback is HLS through a backend proxy (`/stream/{token}`) — the
|
5-second countdown, ✓ on completed episodes. Playback goes through a
|
||||||
video CDN sends no CORS headers, so the browser can't fetch it
|
backend HLS proxy (the CDN sends no CORS headers); segments are
|
||||||
directly. The CDN serves a single quality (no ABR); the proxy pulls
|
pulled across parallel byte-range lanes so a slow connection doesn't
|
||||||
each segment across 16 parallel byte-range lanes (falling back to a
|
stall.
|
||||||
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)
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Stats
|
- **Stats** — `?stats`: watch minutes, episodes, streaks, sub/dub
|
||||||
|
split, weekday/per-day heatmaps, top-10 shows, top searches
|
||||||
`?stats` (hamburger menu) — watch minutes, episodes started/completed,
|
(30–365 day windows).
|
||||||
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
|
## Layout
|
||||||
|
|
||||||
```
|
```
|
||||||
server/ FastAPI app (app.py), source providers (providers/: hianime,
|
server/ FastAPI app (app.py), source providers (providers/: hianime,
|
||||||
anidb, anikoto) over shared HTTP + dispatch (upstream.py),
|
anidb, anikoto) over shared HTTP + dispatch (upstream.py)
|
||||||
accounts (users.py). upstream.py + providers/ are the ONLY
|
static/ front-end: index.html, login.html, app.js, hls.min.js (vendored)
|
||||||
Cloudflare-facing code
|
tests/ pytest suite: hermetic default, live browser e2e opt-in
|
||||||
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
|
docs/img/ README screenshots
|
||||||
run.sh launch/restart the server (sets ANIWEB_SECURE=1)
|
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
|
- `.venv/bin/pytest tests/ -q` — hermetic suite against a throwaway
|
||||||
`curl_cffi` Chrome TLS impersonation. Sources are ordered by
|
server (never touches real data). Live-upstream browser e2e is
|
||||||
`ANI_SOURCE` (default `hianime,anidb,anikoto`) — a dead one falls
|
opt-in: `ANIWEB_LIVE_TESTS=1 .venv/bin/pytest tests/ -q`
|
||||||
through to the next. All upstream-facing code is isolated in
|
(needs playwright chromium).
|
||||||
`server/upstream.py` + `server/providers/`.
|
- `ANI_SOURCE=anidb .venv/bin/python -m server.upstream` — live
|
||||||
- The video CDN sends no CORS headers, so playback goes through the
|
self-check of the anidb fetchers (must be the `-m` form; plain
|
||||||
backend HLS proxy (`/stream/{token}`), which rewrites playlist URIs.
|
script execution breaks the providers' relative imports).
|
||||||
- `ANI_SOURCE=anidb .venv/bin/python -m server.upstream` runs a live
|
- Everything else (Cloudflare/impersonation, HLS proxy, CDN probe,
|
||||||
self-check of the anidb fetchers (search → details → episodes → dub
|
auth hardening, cache-busting): [SPEC.md](SPEC.md).
|
||||||
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).
|
|
||||||
@@ -124,6 +124,10 @@ across parallel byte-range lanes (single-stream fallback).
|
|||||||
Views: home (continue watching), search, detail+player, full history
|
Views: home (continue watching), search, detail+player, full history
|
||||||
(`?history`), stats (`?stats`); PWA-installable.
|
(`?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
|
- Default play: opening an anime resumes the last watched episode of
|
||||||
the season (seeking to the saved position unless ≥95% through),
|
the season (seeking to the saved position unless ≥95% through),
|
||||||
else autoplays episode 1. If the browser blocks autoplay, the native
|
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
|
remote HTTP can't hold a session). `.venv/bin/pytest tests/ -q` runs
|
||||||
the hermetic suite against a throwaway server on :8399; the
|
the hermetic suite against a throwaway server on :8399; the
|
||||||
live-upstream browser e2e is opt-in (`ANIWEB_LIVE_TESTS=1`).
|
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)
|
## Out of scope (v1)
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user