Commit Graph
84 Commits
Author SHA1 Message Date
jozamudi 202bfa41c3 storage: JSON files -> SQLite (users/history/searches/events)
- one-time import from the legacy JSON (left in place as snapshot);
  ghost rows (int vs str ep_id) deduped on import, unrepresentable after
- history POST is now an upsert: the (user, slug_id, ep_id) PK is the
  merge key, so the unguarded read-modify-write (lost updates) and the
  corruption-swallow (JSONDecodeError -> [] -> fresh overwrite) are gone
- searches: NOCASE PK upsert + cap 8
- events table + position/search logging (watch-time trajectory),
  90-day retention pruned at boot
- account delete cascades all four tables
- 22/22 tests green; old JSON files untouched
2026-08-24 16:02:27 +00:00
jozamudi 2c0ee8eae3 ui: center player overlay buttons (scope hero's align-self to .hero-body) 2026-08-24 16:02:27 +00:00
jozamudi 3f8c387a48 fix: only surface tap-to-play on NotAllowedError (autoplay block)
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).
2026-08-24 02:54:40 +00:00
jozamudi db715be54b fix: blocked autoplay left 'Loading…' parked over unreachable controls
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.
2026-08-24 02:51:05 +00:00
jozamudi aae5857dba harden: stream-token host allowlist (SSRF), CSRF header on mutating routes, atomic JSON writes
- /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
2026-08-23 23:22:09 +00:00
jozamudi d39ee838ff api: dedup ghost history rows (int vs string ep_id); normalize ep_id on write 2026-08-23 21:29:01 +00:00
jozamudi 8f57593002 ui: remove click-to-play overlay; native play button covers autoplay-block 2026-08-23 21:29:01 +00:00
jozamudi 2752d49411 fix: "view all" link pointed at /history path (404); use ?history query 2026-08-23 21:28:24 +00:00
jozamudi 50d30a8d3f ui: default to sub (jpn) instead of dub for playback 2026-08-23 21:28:24 +00:00
jozamudi d6bf7e149b docs: README feature walkthrough with screenshots; css: .active wins over .nextup 2026-08-19 20:23:39 +00:00
jozamudi 29fab7ff5f tests: refactor to pytest (conftest session fixture, sync wrappers for async, importorskip for playwright) 2026-08-19 18:59:48 +00:00
jozamudi bb1297f8e2 ui: watched-episode state (dimmed + check, next-up highlight), favicon 2026-08-19 18:49:29 +00:00
jozamudi 98fb4e5461 features: auto-next w/ 5s countdown, prev/next, playback speed, hide fillers, recent searches 2026-08-19 18:41:13 +00:00
jozamudi 9fdf3944fc ui: mobile polish — compact hero row, cropped history posters, wider search, tab ellipsis, header z-index 2026-08-19 18:26:22 +00:00
jozamudi 2ec0f8a034 ui: loading states, player failure overlay + retry, toasts, back nav, full history page with delete
Bundle 1-4+7 from PLAN.md: skeleton loaders, player Loading/Buffering/fatal overlay with Retry, action toasts, back-to-home crumb + logo fix, ?history full-history view with per-entry delete (GET /api/history?all=1, DELETE /api/history). 14/14 tests green.
2026-08-19 18:20:42 +00:00
jozamudi 19e878800c perf: keep-alive upstream sessions, TTL cache on upstream data, /stream LRU, cache hls.min.js
- shared thread-local curl_cffi sessions (no TLS handshake per call)
- in-memory TTL cache: search 5min, details/episodes/languages/
  playlist-URLs 1h; api_stream retries once with a cold cache on 502
  (recovers rotated playlist URLs)
- /stream LRU (24 entries) serves re-seeks and playlist re-fetches from memory
- hls.min.js: Cache-Control public max-age=30d (was no-store -> 500KB
  re-download per page load on mobile)

Measured (click->play chain, cold -> warm):
  search    0.45s -> 0.00s
  detail    0.46s -> 0.00s
  episodes  0.40s -> 0.00s
  stream    1.06s -> 0.00s
  m3u8 x2   0.64s -> 0.00s
  segment   0.20s -> 0.00s
2026-08-19 17:46:17 +00:00
jozamudi ca86daf9a6 api: Cache-Control no-store on /api/history (mutable JSON must not be cached) 2026-08-19 17:38:04 +00:00
jozamudi 0a86e70a54 fix: backfill posters for ALL history entries; skip <img> when no poster
- 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
2026-08-19 17:32:58 +00:00
jozamudi 2b7eb4cdd1 tests: fix rAF-stall flakes in headless chromium; login form submits on Enter
Headless Chromium 151 stops generating animation frames on idle pages,
which stalls Playwright's rAF-based waits (wait_for_function default
polling, click actionability 'stable' check). Fixes:
- wait_for_function: explicit timer polling (100ms/250ms)
- video readyState wait: 120s (cold upstream/CDN starts)
- .card click: force=True
- login: form submit handler (Enter key works, was button-only)
- test asserts the seeded entry, not a section header that only
  appeared via an API-test side effect
2026-08-19 17:17:36 +00:00
jozamudi 2efb537edc home polish: continue-watching hero, progress bars, posters, time-ago
- 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
2026-08-19 17:01:04 +00:00
jozamudi e0f5427002 ui: move logout into the hamburger menu too 2026-08-19 16:42:34 +00:00
jozamudi 1fb6db475c ui: hide delete-account behind a header hamburger menu 2026-08-19 16:41:11 +00:00
jozamudi f0f8207cf7 docs: README/SPEC current — multi-user, CLI accounts, Secure cookie, tests
- README: per-user history (was shared history.json), proxy/HTTPS
  access note, ANIWEB_SECURE, layout lists users.py + tests/ +
  login.html; merged duplicate history bullets
- SPEC: session hardening (Secure flag, per-request account
  existence check), run steps include account creation + test suite,
  milestone 7 (auth), fixed corrupted out-of-scope tail
2026-08-19 16:32:33 +00:00
jozamudi cdb25b1aba security: Secure cookie flag (ANIWEB_SECURE, on in run.sh) + kill sessions of deleted accounts
- middleware re-verifies the username against users.json per request:
  deleted accounts' signed cookies are rejected immediately
- set_cookie/delete_cookie honor the Secure flag; run.sh sets
  ANIWEB_SECURE=1 so sessions only work through the HTTPS proxy
2026-08-19 16:29:03 +00:00
jozamudi 7e6db5a41c frontend: prefix-agnostic fetch paths (reverse-proxy subpath safe)
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.
2026-08-19 16:24:22 +00:00
jozamudi c697500c7d multi-user auth (CLI-created accounts), per-user history, test suite
- 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
2026-08-19 16:17:56 +00:00
jozamudi 76c45a8f29 single-user login screen
- server/credentials.py: PBKDF2 password + session secret, 0600 json,
  one-time setup command (server refuses to start without it)
- app.py: signed 30-day cookie (itsdangerous), middleware 401s API /
  redirects pages to /login?next=, 5-fail/15-min lockout, /logout
- static/login.html: dark-themed form, inline error display
- app.js: api() 401 -> redirect to /login preserving current URL
- docs: README quick start + features, SPEC auth section
2026-08-19 15:54:50 +00:00
jozamudi 2c203990bc 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)
2026-08-19 15:33:31 +00:00
jozamudi a27ff5c931 add run.sh: one-shot server launch/restart 2026-08-19 15:30:02 +00:00
jozamudi a8426b5922 server-persisted watch history with position + resume
- 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)
2026-08-19 15:28:00 +00:00
jozamudi 32f93c0f65 autoplay first episode by default, resume watched
- 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
2026-08-19 15:21:15 +00:00
jozamudi a3a58e7d3c ui: no-store cache headers, synopsis below episode grid
- 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
2026-08-19 15:18:14 +00:00
jozamudi 6c752ae5bc fix playback: vendor hls.js locally, surface stream/API errors visibly
- 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
2026-08-19 15:13:51 +00:00
jozamudi e4dde86d85 ani-web: FastAPI + HLS proxy over anidb.app, vanilla UI
- upstream.py: Cloudflare-proof fetchers (curl_cffi), self-checking
- app.py: /api/* routes + /stream/{token} HLS proxy (CDN has no CORS)
- static/: search -> detail/seasons/episodes -> hls.js player, history
- verified end-to-end: search chainsaw man -> ep1 dub 1080p segment
2026-08-19 15:06:43 +00:00