README: storage is SQLite (aniweb.db), not JSON files; CDN serves a single quality (no ABR) — replaced the wrong 1080/720/360 claim; document the 16-lane parallel fetch, intro-skip, episode names, buffering UI, /api/cdn + background probe, the ?v= cache-busting rule, and the client self-report diagnostics. SPEC: same single-rendition correction (2 places). PLAN: marked complete (all items shipped; deferred list was stale).
6.3 KiB
ani-web — Web Front-End for ani-cli
Browse, search, and stream anime from the browser, using the same anidb.app data source that ani-cli scrapes.
Status: implemented and running. Deviations from the original
draft: hls.js is vendored locally (CDN was a silent failure point),
history is server-side with watch-position resume (was: localStorage,
last 20), and the front-end lives in static/ with the server code in
server/. See README.md.
Verified data flow (spiked 2026-07, all 200 OK with Chrome TLS impersonation)
| Step | Request | Yields |
|---|---|---|
| 1. Search | GET anidb.app/browse?q={query} |
HTML cards: href=".../anime/{slug}-{id}", title="{name}", poster img src |
| 2. Details | GET anidb.app/anime/{slug}-{id} |
MAL id, synopsis, related seasons (same-card links) |
| 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: '<master m3u8>' |
| 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).
All upstream requests must use TLS-fingerprint impersonation
(verified: curl_cffi impersonate="chrome" passes the challenge).
Since the CDN sends no CORS headers, the browser cannot fetch playlists
or segments directly — all HLS traffic must go through a backend proxy.
Architecture
Browser (vanilla JS + hls.js)
│ /api/* JSON /stream/* proxied HLS
▼
Python backend (FastAPI, uvicorn)
│ curl_cffi (Chrome impersonation)
▼
anidb.app + hls.anidb.app
Two moving parts, no database, no framework on the frontend.
Backend
server/app.py + server/upstream.py. Python 3.12, deps:
fastapi, uvicorn, curl_cffi, itsdangerous.
GET /api/search?q=→[{id, slug_id, name, poster}](parse step 1)GET /api/anime/{slug_id}→{id, name, synopsis, mal_id, seasons:[{slug_id,name,id}]}GET /api/anime/{id}/episodes→[{id, number, filler}](step 3, passthrough)GET /api/episode/{ep_id}/stream?lang=eng|jpn→{token}(steps 5+6; token is base64url of the master m3u8 URL)GET /api/history/POST /api/history→ per-user watch history with position, persisted toserver/data/history/<user>.json(50 kept, top 5 returned; upsert per anime+episode).GET /stream/{token}→ proxied HLS. Token = base64url of the upstream URL. Rewrites every non-comment line of m3u8 bodies to point back at/stream/; proxies master, variants, and segments (bytes, no transcoding). Responses areCache-Control: public, max-age=86400— VOD playlists/segments are immutable and the token is the exact upstream URL, so the browser disk cache serves re-opens and re-seeks without touching the proxy.GET /login/POST /login/POST /logout→ multi-user auth: PBKDF2-SHA256 passwords (server/users.py;users.json0600), signed 30-day cookie carrying the username, 5-fail/15-min in-memory lockout per user. Accounts are CLI-created only (python -m server.users add|rm|list) — no self-signup. One-time migration: legacy single-passwordcredentials.json→ useradmin;history.json→history/admin.json.- Session hardening: cookies are
HttpOnly,SameSite=Lax, andSecurewhenANIWEB_SECURE=1(set byrun.sh); the middleware re-verifies the account still exists per request, so deleted accounts' cookies are rejected immediately. GET /api/me,POST /account/delete→ current user / self-delete (account + history file).
Frontend
static/: single index.html + app.js + style.css. hls.js is
vendored (static/hls.min.js) — a CDN dependency silently
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) →
<video>with hls.js pointed at/stream/{token}; single quality — the proxy fetches each segment across parallel byte-range lanes (single-stream fallback).
- 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 play button on the video is the way to start.
- Watch position saved to
/api/historyevery ~10s + on pause/unload; home page lists the 5 most recent entries with timecode.
Run
uv venv .venv && uv pip install --python .venv/bin/python -r requirements.txt
.venv/bin/python -m server.users add <you> # one-time account creation
./run.sh # launch/restart, logs to server.log, sets ANIWEB_SECURE=1
Use the app through your HTTPS proxy (with Secure cookies, plain
remote HTTP can't hold a session). python tests/run.py runs the
full suite against a throwaway server on :8399.
Out of scope (v1)
Downloads (-d), next-episode countdown (-N), syncplay, intro skip,
ani-schedule integration, episode titles (not in anidb's API — needs
a MyAnimeList API key). Add when someone asks.
Risks
- Cloudflare policy change → upstream breaks. Mitigation: swap
impersonate="chrome"for"safari"/binary curl-impersonate; all upstream access is isolated in one module (upstream.py). - anidb.app layout change → parsers break. The 4 API endpoints (steps 3–6) are clean JSON; only steps 1, 2, 5 are HTML/JS scraping.
- ToS — anidb.app is a third-party scraper site; personal use, no redistribution, bind to LAN only by default.
Milestones (all done)
- Spike — curl_cffi passes Cloudflare; all 7 steps verified.
server/upstream.py— the 6 fetch functions, assert self-check.- API — FastAPI routes over upstream.py.
- HLS proxy —
/stream/{token}with playlist rewriting. - UI — search → detail → episodes → player.
- Polish — history with position + resume, sub/dub toggle, error
states, autoplay default,
run.sh. - Auth — login screen, then multi-user CLI-created accounts with per-user history + test suite.