diff --git a/PLAN.md b/PLAN.md index 81a2473..efd7759 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,5 +1,9 @@ # Plan — UI/UX polish bundle (items 1–4 + 7) +**STATUS: complete** — every item below shipped (see git log, c2461f8 and +friends). Kept as the planning record; the "deferred" list at the bottom +is stale (favicon, a11y pass, autoplay-next were all done afterwards). + The agreed bundle from the polish list: 1. **Loading states** — skeleton cards + player spinner diff --git a/README.md b/README.md index e132064..5b9e6ae 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Account management (no self-signup by design): ``` Security: passwords are PBKDF2-SHA256 (210k iters, per-user salts) in -`server/data/users.json` (0600). Sessions are signed 30-day `HttpOnly` +`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. @@ -45,7 +45,7 @@ the app or CLI are rejected immediately, even with an old cookie. 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, +(`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. @@ -77,10 +77,20 @@ detail view: synopsis, stats, season tabs, and filler badges. 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. +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) @@ -117,6 +127,16 @@ SPEC.md design + the verified anidb.app data flow backend HLS proxy (`/stream/{token}`), which rewrites playlist URIs. - `.venv/bin/python server/upstream.py` runs a self-check of the fetchers (search → details → episodes → dub stream for Chainsaw Man). +- A background probe fetches 64KB from the CDN every 3 min and logs + state flips to `server.log`; `GET /api/cdn` (public) returns the + latest throughput + ~1h history. Use it to check if "playback + broken" is the CDN again. +- The front-end is version-pinned: bump `?v=N` on `app.js`/`style.css` + in `index.html` on every static change — phones/proxies otherwise + serve stale JS (index.html itself is `no-store`). +- 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 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 diff --git a/SPEC.md b/SPEC.md index 6adefb2..00e9a22 100644 --- a/SPEC.md +++ b/SPEC.md @@ -18,7 +18,7 @@ last 20), and the front-end lives in `static/` with the server code in | 3. Episodes | `GET anidb.app/api/frontend/anime/{id}/episodes` | JSON `{episodes: [{id, number, number2, filler}]}` | | 4. Languages | `GET anidb.app/api/frontend/episode/{ep_id}/languages` | JSON `{languages: [{code: "jpn"\|"eng", name, embed_url}]}` — `jpn`=sub, `eng`=dub | | 5. Embed page | `GET {embed_url}` | JS blob containing `file: ''` | -| 6. Master playlist | `GET {master m3u8}` (hls.anidb.app) | variants: 1080p / 720p / 360p | +| 6. Master playlist | `GET {master m3u8}` (hls.anidb.app) | single rendition (v1) — no ABR ladders | | 7. Variants + segments | `GET ...m3u8`, `.ts` | streamable HLS — **NO CORS headers** | Key constraint: anidb.app sits behind Cloudflare (plain curl → 403). @@ -85,7 +85,8 @@ produced black screens. Flow: search box → results grid (poster + title, click) → detail view (synopsis under the episode grid, season tabs) → episode grid (number + filler badge, sub/dub toggle) → `