move server code into server/, update docs

- app.py + upstream.py -> server/ (relative import, __init__.py)
- static/ resolved relative to repo root, works from any CWD
- watch history migrated to server/data/history.json
- README.md: quick start, features, layout, dev notes
- SPEC.md: marked implemented, updated endpoints/run/deviations
- ignore *.tar.gz (local curl-impersonate download at repo root)
This commit is contained in:
jozamudi committed 2026-08-19 15:33:31 +00:00
1 parent a27ff5c931
commit 2c203990bc
7 files changed
+101 -37

No files matched your search

+45 -32
View File
@@ -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) → `<video>` with hls.js pointed at
`/stream/{token}`; quality follows the master playlist (auto,
hls.js handles ABR).
(synopsis under the episode grid, season tabs) → episode grid (number
+ filler badge, sub/dub toggle) → `<video>` with hls.js pointed at
`/stream/{token}`; quality follows the master playlist (auto ABR).
- History: last 20 `{title, season, episode}` in `localStorage`
(mirrors ani-cli's `-c` continue feature).
- No login, no accounts, no build step.
- 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, a
"click to play" overlay appears.
- Watch position saved to `/api/history` every ~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 fastapi uvicorn curl_cffi
.venv/bin/python -m uvicorn app:app --host 0.0.0.0 --port 8321
```sh
uv venv .venv && uv pip install --python .venv/bin/python -r requirements.txt
./run.sh # launch/restart, logs to server.log
```
Open `http://localhost:8321`.
Open `http://localhost:8321` (bound to 0.0.0.0 for LAN access).
## Out of scope (v1)
Downloads (`-d`), next-episode countdown (`-N`), syncplay, intro skip,
ani-schedule integration, auth, multi-user. Add when someone asks.
ani-schedule integration, auth, multi-user, episode titles (not in
anidb's API — needs a MyAnimeList API key). Add when someone asks.
## Risks
@@ -89,12 +102,12 @@ ani-schedule integration, auth, multi-user. Add when someone asks.
- **ToS** — anidb.app is a third-party scraper site; personal use,
no redistribution, bind to LAN only by default.
## Milestones
## Milestones (all done)
1. **Spike (done)** — curl_cffi passes Cloudflare; all 7 steps verified.
2. **upstream.py** — the 6 fetch functions above, one assert-based
self-check (`python upstream.py` prints a chainsaw-man episode m3u8).
3. **API** — FastAPI routes over upstream.py.
4. **HLS proxy** — `/stream/{token}` with playlist rewriting.
5. **UI** — search → detail → episodes → player.
6. Polish — history, sub/dub toggle, error states.
1. Spike — curl_cffi passes Cloudflare; all 7 steps verified.
2. `server/upstream.py` — the 6 fetch functions, assert self-check.
3. API — FastAPI routes over upstream.py.
4. HLS proxy — `/stream/{token}` with playlist rewriting.
5. UI — search → detail → episodes → player.
6. Polish — history with position + resume, sub/dub toggle, error
states, autoplay default, `run.sh`.