Save watch position on fatal player error; make failure test CDN-independent

playerFail() is the single choke point for every fatal error (hls.js fatal,
v.onerror, autoplay block) but never saved the position — periodic saves
(timeupdate) stop when hls is destroyed, so a fatal error at e.g. 40s lost
everything after the last 10s save. Call savePos() there.

test_stream_failure_retry redesigned around that: it no longer needs a
healthy CDN (which hangs on segment bytes intermittently). It waits only for
playlist metadata (duration comes from the VOD playlist, not segment bytes),
wipes the show's history, seeks to 6s (below the 10s periodic-save threshold,
above the 3s minimum — so only an error-time save could persist it), aborts
/stream to force a deterministic fatal, and asserts the position landed.
Retry-recovery now asserts a fresh token fetch instead of segment playback.
This commit is contained in:
jozamudi committed 2026-08-30 18:48:52 +00:00
1 parent 0fd06f036a
commit 71a4055e89
2 files changed
+38 -9

No files matched your search

+34 -9
View File
@@ -157,22 +157,47 @@ async def _stream_failure_retry():
async def abort_stream(route):
await route.abort()
# note: **/stream/* (single star) — must NOT match the
# /api/episode/{id}/stream token endpoint, only the /stream/{token} proxy
await page.route("**/stream/*", abort_stream)
await _login(page, "admin", "adminpw12345")
await page.fill("#q", "chainsaw")
await page.keyboard.press("Enter")
await page.wait_for_selector("a.card", timeout=30000)
await page.click("a.card", force=True)
# detail auto-plays ep1; with /stream aborted hls.js exhausts its
# retries and surfaces the fatal error -> failure overlay with Retry
await page.wait_for_selector(".pl-overlay button", timeout=90000)
await page.unroute("**/stream/*", abort_stream)
await page.click(".pl-overlay button", force=True)
# detail auto-plays ep1. Wait only for playlist metadata (duration
# comes from the VOD playlist, NOT from segment bytes) — this must
# work even when the CDN is slow/flaky on segments
await page.wait_for_function(
"document.querySelector('video').readyState >= 4 && document.querySelector('video').currentTime > 1",
"document.querySelector('video')?.duration > 0",
timeout=120000, polling=250)
# wipe this show's history: the initial play() write (and any entry
# left by earlier tests) would otherwise mask whether the error-time
# save happened — from here on the only write can be that save
slug = await page.evaluate("new URLSearchParams(location.search).get('anime')")
h0 = await page.evaluate("fetch('api/history?all=1').then(r => r.json())")
for e in (x for x in h0 if x["slug_id"] == slug):
await page.evaluate(
"([s, ep]) => fetch('api/history?slug_id=' + encodeURIComponent(s) + '&ep_id=' + ep,"
" {method:'DELETE', headers:{'X-Aniweb':'1'}})", [e["slug_id"], e["ep_id"]])
# seek to 6s: below the 10s periodic-save threshold (and above the
# 3s minimum), so NO periodic save can fire — only an error-time
# save could persist ~6s. The seek reflects immediately even though
# the bytes never arrive (we kill /stream next)
await page.evaluate("document.querySelector('video').currentTime = 6")
# kill the stream: hls.js's frag loads abort, retries exhaust,
# fatal error -> playerFail, which must save the position
# (note: **/stream/* single star — must NOT match the
# /api/episode/{id}/stream token endpoint, only the /stream/{token} proxy)
await page.route("**/stream/*", abort_stream)
await page.wait_for_selector(".pl-overlay button", timeout=90000)
# pre-fix nothing was written after the DELETE above
hist = await page.evaluate("fetch('api/history?all=1').then(r => r.json())")
assert any(e["slug_id"] == slug and e["pos"] >= 5 for e in hist), hist
# and the player is recoverable: Retry re-invokes play(), which
# starts with a fresh token fetch (assert that — not segment bytes,
# which a hanging CDN would make environmental; real playback is
# covered by the other video tests)
await page.unroute("**/stream/*", abort_stream)
async with page.expect_request("**/api/episode/*/stream*", timeout=30000):
await page.click(".pl-overlay button", force=True)
await browser.close()