diff --git a/.gitignore b/.gitignore index 2920eef..2d6a363 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .venv/ __pycache__/ *.pyc -data/ +*.tar.gz +server/data/ server.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..b07f567 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# 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. + +## Quick start + +```sh +uv venv .venv +uv pip install --python .venv/bin/python -r requirements.txt +./run.sh +``` + +Open . `run.sh` restarts cleanly if the server +is already running; logs go to `server.log`. + +## Features + +- Search with poster grid, detail view with synopsis + season tabs +- HLS playback (vendored hls.js), browser ABR across 1080/720/360p +- Dub/sub toggle +- Autoplays the first episode (or resumes the last watched one); if the + browser blocks autoplay, a "click to play" overlay appears +- Server-persisted watch history (`server/data/history.json`): watch + position saved every ~10s, resume where you left off, top-5 "recently + played" on the home page — shared across browsers and devices + +## Layout + +``` +server/ FastAPI app (app.py) + anidb.app fetchers (upstream.py) + upstream.py is the ONLY Cloudflare-facing code +static/ front-end: index.html, app.js, style.css, hls.min.js (vendored) +run.sh launch/restart the server +SPEC.md design + the verified anidb.app data flow +``` + +## Dev notes + +- anidb.app is behind Cloudflare; all upstream requests use + `curl_cffi` Chrome TLS impersonation. If that ever stops working, + 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). +- 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/SPEC.md b/SPEC.md index ef27a7a..ac7d1b3 100644 --- a/SPEC.md +++ b/SPEC.md @@ -3,6 +3,12 @@ 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 | @@ -37,47 +43,54 @@ Two moving parts, no database, no framework on the frontend. ## Backend -Python 3.12, deps: `fastapi`, `uvicorn`, `curl_cffi`. +`server/app.py` + `server/upstream.py`. Python 3.12, deps: +`fastapi`, `uvicorn`, `curl_cffi`. -- `GET /api/search?q=` → `[{id, slug, name, poster}]` (parse step 1) -- `GET /api/anime/{slug_id}` → `{name, synopsis, mal_id, seasons:[{id,name}]}` +- `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}/languages` → `[{code, name, embed_url}]` (step 4) -- `GET /stream/{token}` → proxied HLS. Token = base64(nats) of the - upstream URL. Rewrites `#EXT-X-...` bodies: every relative/absolute - URL in the playlist is re-pointed at `/stream/...`. Serves master, - variants, and `.ts` segments through the same endpoint (bytes - proxied, no transcoding). -- Upstream cache: embed tokens expire — cache embed→m3u8 mapping for - ~10 min; cache nothing else (search is cheap). +- `GET /api/episode/{ep_id}/stream?lang=eng|jpn` → `{token, qualities}` + (steps 5+6; token is base64url of the master m3u8 URL) +- `GET /api/history` / `POST /api/history` → watch history with + position, persisted to `server/data/history.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). ## Frontend -Single `index.html` + `app.js` + `style.css`. hls.js from CDN. +`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, season tabs) → episode list (number + filler badge, -sub/dub toggle) → `