jozamudi cea04a20f6 player: fullscreen button (scales with the window)
'⛶ full' in the controls: native requestFullscreen on the video (it fills
the viewport and rescales on window resize for free), with the iOS
webkitEnterFullscreen fallback where the Fullscreen API doesn't exist.
Click again to exit. Live browser test: enter + exit via the button.
2026-09-19 00:11:45 +00:00
2026-09-01 21:25:13 +00:00

ani-web

Web front-end for anime streaming: search, browse, and stream anime in the browser (hianime.at primary; anidb.app and anikototv.to as fallbacks). Multi-user, with per-user watch history and HLS playback proxied through the backend. Tuned for phone as much as desktop.

login

Quick start

uv venv .venv
uv pip install --python .venv/bin/python -r requirements.txt
.venv/bin/python -m server.users add <you>   # create your account (one-time)
./run.sh

Open it through your HTTPS proxy (or http://localhost:8321 from the host itself — browsers treat localhost as trusted). run.sh restarts cleanly if the server is already running; logs go to server.log. With ANIWEB_SECURE=1 (set by run.sh), the session cookie is Secure, so remote plain-HTTP access can't hold a session — only the HTTPS proxy works. A legacy credentials.json password, if any, is migrated to user admin on first start.

Account management (no self-signup by design):

.venv/bin/python -m server.users add <name>    # add user (prompted password)
.venv/bin/python -m server.users rm <name>     # remove user + their history
.venv/bin/python -m server.users list          # list users

Security: passwords are PBKDF2-SHA256 (210k iters, per-user salts) in 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.

Features

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/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.

The history page shows every entry with poster, progress, and a per-entry delete button:

history

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

The header search box queries the active source 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 and season tabs (where the source provides them), plus filler badges.

search

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. 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)
  • 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

Stats

?stats (hamburger menu) — watch minutes, episodes started/completed, active days + best streak, sub/dub split, weekday and per-day heatmaps, top-10 shows, top searches. Derived server-side from the position-event stream (POST /api/events), 30–365 day windows.

Layout

server/   FastAPI app (app.py), source providers (providers/: hianime,
          anidb, anikoto) over shared HTTP + dispatch (upstream.py),
          accounts (users.py). upstream.py + providers/ are the ONLY
          Cloudflare-facing code
static/   front-end: index.html, login.html, app.js, style.css, hls.min.js (vendored)
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

Dev notes

  • The upstream sites sit behind Cloudflare; all upstream requests use curl_cffi Chrome TLS impersonation. Sources are ordered by ANI_SOURCE (default hianime,anidb,anikoto) — a dead one falls through to the next. All upstream-facing code is isolated in server/upstream.py + server/providers/.
  • The video CDN sends no CORS headers, so playback goes through the backend HLS proxy (/stream/{token}), which rewrites playlist URIs.
  • ANI_SOURCE=anidb .venv/bin/python -m server.upstream runs a live self-check of the anidb fetchers (search → details → episodes → dub stream). Must be the -m form — plain script execution breaks the providers' relative imports.
  • A background probe fetches 64KB from the CDN (and checks the origin site) every 3 min and logs state flips to server.log; GET /api/cdn (public) returns the latest throughput + ~1h history + origin state. Use it to check if "playback broken" is the CDN again.
  • The front-end is version-pinned: the server injects the git SHA as the ?v= cache-bust on app.js/style.css at startup (index.html itself is no-store), so a restart is a cache bump — nothing to hand-bump.
  • Mutations need the X-Aniweb: 1 header (CSRF: cross-site form POSTs can't set it), /login POST included — forced-login is dead.
  • Login throttling (server/auth.py): per-username 5 fails -> 15 min lock, plus a per-source-IP budget; both sliding windows, in-memory.
  • 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 test suite against a throwaway server (:8399, temp data dir — never touches the real data). The default suite (API + auth + dispatch tests) is hermetic and deterministic; the Playwright browser tests scrape and play LIVE upstream content, so they are opt-in: ANIWEB_LIVE_TESTS=1 .venv/bin/pytest tests/ -q (needs playwright chromium installed).
S
Description
No description provided
Readme
8 MiB
0 Stars 1 Watchers 0 Forks
Languages
Python 65.2%
JavaScript 25.9%
CSS 5.9%
HTML 2.1%
Shell 0.9%