diff --git a/README.md b/README.md index 7170a98..e132064 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ # ani-web -Web front-end for the [ani-cli](https://github.com/pystardust/ani-cli) -workflow: search, browse, and stream anime in the browser via anidb.app. +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. + +![login](docs/img/login.png) ## Quick start @@ -36,25 +40,61 @@ 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/.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 +### 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/.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: + +![history](docs/img/history.png) + +### 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](docs/img/home.png) + +### 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. + +![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. 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) + +![detail](docs/img/detail.png) ## Layout @@ -62,7 +102,8 @@ the app or CLI are rejected immediately, even with an old cookie. 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) +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 ``` @@ -74,12 +115,13 @@ SPEC.md design + the verified anidb.app data flow everything is isolated in `server/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.py` runs a self-check of the fetchers - (search → details → episodes → dub stream for Chainsaw Man). -- `python tests/run.py` runs 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. +- `.venv/bin/python server/upstream.py` runs a self-check of the + fetchers (search → details → episodes → dub stream for Chainsaw Man). +- `.venv/bin/pytest tests/ -q` runs 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. diff --git a/docs/img/detail.png b/docs/img/detail.png new file mode 100644 index 0000000..1bda601 Binary files /dev/null and b/docs/img/detail.png differ diff --git a/docs/img/history.png b/docs/img/history.png new file mode 100644 index 0000000..965151a Binary files /dev/null and b/docs/img/history.png differ diff --git a/docs/img/home.png b/docs/img/home.png new file mode 100644 index 0000000..71f9e27 Binary files /dev/null and b/docs/img/home.png differ diff --git a/docs/img/login.png b/docs/img/login.png new file mode 100644 index 0000000..96a88a6 Binary files /dev/null and b/docs/img/login.png differ diff --git a/docs/img/search.png b/docs/img/search.png new file mode 100644 index 0000000..9fa46dc Binary files /dev/null and b/docs/img/search.png differ diff --git a/static/style.css b/static/style.css index e821e64..a27428b 100644 --- a/static/style.css +++ b/static/style.css @@ -113,9 +113,9 @@ button.tiny.on { border-color: var(--acc); color: var(--acc); } .eps { display: grid; grid-template-columns: repeat(auto-fill, minmax(64px, 1fr)); gap: 0.4rem; } .ep { position: relative; font-size: 0.9rem; } -.ep.active { border-color: var(--acc); color: var(--acc); } .ep.filler { opacity: 0.55; } .ep.done { opacity: 0.5; } .ep.nextup { border-color: #4b5568; background: #1f2130; } +.ep.active { border-color: var(--acc); color: var(--acc); } /* declared last: wins over nextup */ .badge { font-size: 0.6rem; color: var(--muted); }