Commit Graph
27 Commits
Author SHA1 Message Date
jozamudi 834ca80a8c Provider layer + hianime source (primary)
- providers/{anidb,anikoto,hianime}.py: per-source modules behind the
  ANI_SOURCE fallback dispatch (upstream.py _call/_providers)
- hianime.py: pystardust/ani-cli#1897 flow — search/episode-list/
  servers API, HD-1 hash -> base64 embed url, window.__P blob
  (base64 + XOR 'otaku-embed-v1') -> master m3u8; registers CDN hosts
  and the required Referer via stream_meta()
- upstream.py: get_stream/fetch_parallel accept extra headers;
  default source order hianime,anidb,anikoto (anidb.app is 503)
- app.py: /stream allowlist accepts provider-registered hosts and
  sends their Referer; status probe follows the primary provider
  (hianime variant URIs are relative to the variant path)
- app.js: error-time position save floors at furthest point reached
  (hls.js resets currentTime to 0 before playerFail on fatal errors)
- tests: default-order + hianime import checks; live browser suite green
2026-09-07 01:38:53 +00:00
jozamudi ff8311c1a8 Status pill + git-SHA asset versioning
- _origin_probe(): 3-min check of anidb.app pages/API; OK/DOWN + reason
  in GET /api/cdn and server.log ([probe] ORIGIN ...)
- header pill: upstream-site dot (red = down) + CDN dot + label;
  per-dot hover tooltips, tap -> toast legend (mobile has no hover)
- ?v={{VERSION}} injected from 'git describe --always --dirty' at
  startup; JSV reads it from its own script URL - replaces hand-bumped
  numbers (JSV/?v drift) and gives play-probes the running commit
2026-09-03 16:33:51 +00:00
jozamudi aaaf8e9308 iPhone: full client timeline + self-diagnosing watchdog
Every step now beacons with millisecond offsets:
branch-decision -> new-hls-ok -> loadsource-called -> attach-ok ->
manifest-loaded -> level-loaded -> frag-loaded(1..2) -> buffer-appended
-> do-play -> play-resolved/play-rejected(name) + EVERY hls.js error
(fatal and silent retries). Plus a watchdog: if not playing within 15s,
it snapshots {readyState, networkState, paused, buffered, hls.state,
overlay text} every 15s (x6) — so a silent death anywhere produces a
state dump instead of nothing.

Verified on desktop: healthy timeline lands end-to-end, playback OK,
18/18 tests. assets v=13.
2026-08-31 20:53:18 +00:00
jozamudi aba266ee4b iPhone: step probes + full hls.js error stream to server
The play-probe now lands (iPhone: iOS 18.7 Safari, hls.js path, MSE ok),
but the /stream XHR never reaches the server on the failing attempts —
impossible under the known code path. Instrument each step
(new-hls-ok / loadsource-called / attach-ok) and log EVERY hls.js error
(fatal+non-fatal, with retry counter) so the client timeline is visible
server-side. Also fixed JSV constant drift (was stuck at 10).
events endpoint: accept hls-err, forward step/fatal/details/buffer/retry/http.
2026-08-31 20:48:02 +00:00
jozamudi 5268e77d00 events: accept client-fail + play-probe types (was 422)
The diagnostic beacons were rejected server-side (only 'error' was
whitelisted) — which is why the iPhone's fingerprint never arrived.
Now: error / client-fail / play-probe accepted, payload fields
forwarded; anything else still 422.
2026-08-31 20:22:53 +00:00
jozamudi a61e7dd0e7 GET /api/cdn: probe status endpoint (public, read-only)
Latest state + kbps + ~1h history (20 samples). curl it to check the
CDN without digging through server.log.
2026-08-31 15:23:36 +00:00
jozamudi eea538ab17 Background CDN probe: log throughput + state flips every 3 min
64KB Range fetch from a fixed episode (JJK S2 E25) on a daemon thread;
logs '[probe] CDN OK/DEGRADED (N KB/s)' on state change so server.log
shows exactly when a degraded window ends. 18/18 tests, verified live.
2026-08-31 15:17:20 +00:00
jozamudi ee2a888aca Proxy: fetch segments across 4 parallel byte-range lanes
The slow-segment logs show per-connection CDN throttling (15-40 KB/s
dribbles). yt-dlp's standard fix is -N 16 parallel fragment lanes; this
is the 4-lane version for the proxy: HEAD for content-length, then 4
concurrent Range GETs reassembled byte-exact (verified sha256-identical
to a direct CDN fetch). Small files (<512KB) still use a single GET.

ANY failure (no content-length, 416, lane error, timeout) falls back to
the existing single-stream path, so this is never worse than before.

Caveat (measured live): when the CDN caps SHARED egress rather than
per-connection, lanes just split the same pie — in that window both
1-lane and 4-lane time out. A different source is the only real fix
for those windows.

18/18 tests (new test pins lane stitch + fallback; also fixed a
pre-existing _get stub leak in the Jikan-shape test).
2026-08-31 14:51:55 +00:00
jozamudi 83a2953a1c Proxy: log slow segment transfers (TTFB>3s or total>8s)
Distinguishes CDN-side stalls (server sees slow TTFB) from client-path
stalls (server fast, user still times out) when fragLoadTimeOut recurs.
2026-08-31 01:16:38 +00:00
jozamudi 19bc2afb32 Proxy: stream HLS segments through instead of buffering
fragLoadTimeOut root cause: /stream buffered the whole segment
(r.content) before sending, so the browser's time-to-first-byte = the
CDN's entire upload time. On slow CDN moments that trips hls.js's
first-byte timer even though the segment would have completed.

- upstream.get_stream(): opens the upstream response without reading
  the body (stream=True), same retry/timeout rules as _get
- segments now flow to the browser as the CDN emits them (256KB chunks);
  the 12s cap becomes a total-transfer cap; cache is populated on
  completion only (a truncated body never poisons the LRU)
- playlists keep the buffered path (body needed for URI rewriting)
- cache-hit fast path restored for both (stream() checks the LRU first)

Verified: segment byte-identical to direct CDN (sha256), sequential
keep-alive 3/3 OK, 5 concurrent fetches 200+identical, cache hit
0.148s miss -> 0.001s hit, 17/17 API tests green.

Skipped: raising hls.js retry counts — the first-byte race was the
binding constraint, not the retry budget.
2026-08-31 01:05:00 +00:00
jozamudi 0613cf7e27 Intro skip: MAL timings via keyless Jikan + player button
- upstream: _intros(mal_id) -> per-episode {start,length} (seconds),
  30d TTL cache; Jikan is down or show uncovered -> [] (no button)
- app: GET /api/intros/{mal_id}, never errors into the player
- player: 'skip intro' pill appears while currentTime is inside the
  intro window (+8s grace); click seeks past; reappears if rewound.
  Ep-1 intros are often missing in MAL data, so falls back to the
  show's first known timing (usually identical across eps)
- test: _parse_intros unit test (Jikan itself is 504ing right now,
  live path verified by hand: endpoint returns [] cleanly)
2026-08-30 21:01:24 +00:00
jozamudi 0fd06f036a Playback: retry flaky upstream, faster proxy fail, cache /stream in browser
- upstream._get: one 300ms retry on transient errors (CF 403/503, 429,
  5xx); 404 stays final. Fixes the 502 cluster that hls.js couldn't
  recover from (fragLoadTimeOut x3 in error log)
- /stream: upstream timeout 12s < hls.js fragLoadingTimeOut (20s), so a
  hung segment 502s first and hls.js retries instead of fatal-timing-out
- /stream responses: Cache-Control public max-age=86400 (VOD + token is
  the exact upstream URL => immutable); re-opens/re-seeks skip the proxy
- /api/stream: drop unused qualities (frontend never read it; saved a
  ~0.2s cold upstream round-trip per episode)
- app.js/style.css: max-age=300 instead of no-store
- test_history_page_and_delete: stale selector (home redesign removed
  the 'view all' link; use 'history ->')
2026-08-30 17:23:57 +00:00
jozamudi 7b670cff24 Stats: GitHub-style watch-time heatmap
- api/stats gains by_day: local-date -> seconds (same watch-time
  heuristic as by_weekday)
- ?stats renders a Mon..Sun x week grid (30d window, matches the
  page header) with 4 intensity levels + tooltip per cell + legend;
  replaces the by-weekday bar chart the heatmap subsumes
- test_stats covers by_day (sums, slug-scoped, empty case)
2026-08-24 22:24:16 +00:00
jozamudi 1be3f4f307 Stats UI: per-anime progress rows, status chips, on-hold, streak, detail line
- server: /api/stats top_anime rows enriched — poster, total eps, cur_ep,
  last_seen, status (finished/ongoing/hold heuristic), pace (ep/day),
  sub/dub watch-time split, rewatches (backward jump >60s); new on_hold
  + best_streak top-level keys; optional ?slug= filter (json_extract)
- frontend: 'Most watched' rows = poster + meta + status chip + completion
  bar (ongoing -> hold -> finished order); stat cards swap in on-hold and
  best streak, errors demoted to a one-liner; anime detail page gets a
  progress line (E12/24 · 4h 30m · 2.1 ep/day · status); history cards
  dim + label 'on hold' after 7 days; savePos now includes total eps
- css: .chip, .stat-anime rows, .card.hold, .detail-prog; drop dead .stat-row
- test: test_stats asserts the new fields + slug filter
2026-08-24 17:30:01 +00:00
jozamudi 2f373b9bba Add per-user stats: play/error events + /api/stats + ?stats page
- server: 'play' event on successful stream fetch, 'error' on 404 stream
  failures; new POST /api/events for client-reported playback errors
  (only type=error accepted, fields allowlisted + length-capped)
- server: GET /api/stats?days=N — watch time from position-event pos
  deltas (<=90s cap: bigger jumps are seeks), episodes started/completed
  (>=95% of dur), days active, weekday histogram, top anime/searches,
  sub/dub split, error count
- frontend: ?stats page (cards, weekday bars, most-watched, top
  searches) + 'stats' link on home; playerFail now reports the error
- test: test_stats with synthetic viewing data (deltas, isolation, 422)
2026-08-24 17:09:27 +00:00
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 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 50d30a8d3f ui: default to sub (jpn) instead of dub for playback 2026-08-23 21:28:24 +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 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 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 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