The first version reacted to any play() rejection, which broke the
stream-failure path: with a dead stream play() rejects with AbortError
(no data), clobbering the hls.js 'Playback failed / Retry' overlay and
stealing the user's one click. Scope the swap to the autoplay policy
rejection; everything else stays with the fatal-error path.
test_stream_failure_retry caught it; test_autoplay_blocked_shows_tap_to_play
still passes (simulates NotAllowedError on the first gesture-less play).
The overlay only clears on 'playing', which never fires when the browser
blocks autoplay — and the overlay covers the native controls, so the
'swap to the native play button' assumption from the click-to-play
removal never held. A rejected v.play() is the signal: swap the dead
Loading… overlay for a tappable one (Play), and a second rejection
escalates to the Playback failed overlay.
Browser test fakes the policy (reject first gesture-less play()) since
headless Chromium allows autoplay. Logs showed the shape of it: six
loads of the same ep, 38-40 segments each, all 200, player never started.
- /stream/{token}: tokens are plain base64; allowlist https://hls.anidb.app
so a session can't use the proxy for arbitrary URLs (new test)
- X-Aniweb:1 required on POST/DELETE except /login — SameSite=Lax is
still sent on cross-site form POSTs, which could hit /account/delete
(new test; SPA + tests send the header)
- tmp+os.replace writes for users/history/searches: a crash mid-write
could corrupt the JSON -> load() returned {} -> silent data loss
- resume positions now read from full history (?all=1), not top-5
- pagehide bound once; savePos reads currentEp (no stale closures)
- account delete also removes the searches file; episodes TTL 5m
- ani-web.service: supervised restart (not installed; see header)
- gitignore pi-session dumps + .pytest_cache
- backfill script deduped by slug_id, so a second entry for the same
anime kept no poster -> rendered as a broken <img src="">
- re-backfilled from the already-fetched poster per slug
- showHome now only emits an <img> when the entry has a poster
- showHome(): most recent entry renders as a hero card (poster,
episode/lang/position, progress bar, Resume); the rest become
mini poster cards with progress + relative timestamp
- history entries now persist dur (video.duration) and poster;
/api/anime returns poster (parsed from the details page)
- old entries without dur render without a bar (backward compatible)
- browser test asserts the seeded entry, not a section header that
only existed via a cross-test side effect
login.html/app.js now resolve fetch targets relative to the page's
directory (BASE), so the app works served at / or under a path prefix
(https://host/ani/) as long as the proxy strips the prefix.
- server/users.py replaces credentials.py: users.json (0600), PBKDF2,
one-time migration of the legacy single password to user admin
(history.json -> history/admin.json)
- accounts are CLI-managed only: python -m server.users add|rm|list
(no self-signup on an internet-exposed app); /signup route removed,
login page back to username + password
- per-user 30-day signed session cookie (payload carries the
username), per-user lockout, GET /api/me, POST /account/delete
- per-user watch history: server/data/history/<user>.json; ANIWEB_DATA
env override for the data dir
- header chip: logged-in user, logout, delete account
- tests/: throwaway-server suite (port 8399, temp data dir) — 9 HTTP
tests + 2 Playwright browser tests (login UI, real video playback
behind auth); runs via python tests/run.py
- POST/GET /api/history: JSON file in data/history.json (50 entries
kept, top 5 shown on home), upsert per (anime, episode)
- watch position saved every ~10s of playback + on pause/unload
- resume: opening an anime seeks to the saved position unless
>=95% through (then starts fresh); position shown on home items
- drops localStorage; server is the source of truth (verified:
fresh watch -> new session resumes at saved pos -> home lists it)
- no ?ep= param: resume last watched ep of the season from history,
else start at ep 1 (verified in headless Chromium, no click needed)
- if the browser blocks autoplay (no user gesture yet), show a
'click to play' overlay instead of a silent black screen
- no-store on everything except /stream: stale cached app.js (old CDN
hls.js ref) was the cause of the black screen — video plays fine in a
real Chromium once the fresh JS loads (verified via playwright)
- synopsis moved under the episode grid
- jsdelivr CDN was a silent single point of failure (Hls undefined ->
unhandled rejection -> black screen with no feedback)
- play() now catches API errors; hls fatal + video media errors render
'Stream error: ...' under the player
- server now binds 0.0.0.0 (LAN access); was 127.0.0.1 only