Fix episode position logging/resume: NaN savePos, clobber-on-boot, unvalidated history writes
- app.js: pause/pagehide listeners passed the Event into savePos(floor), making pos NaN -> serialized as null -> every pause/unload clobbered the saved position. Wrap in arrows; pagehide now posts keepalive so the final save survives page unload - app.js: play() re-fetches history on a miss (boot fetch swallows errors) and only bumps an existing row instead of posting pos:0 at start-of-play - app.py: POST /api/history requires slug_id (was stored as "None"), validates/clamps pos; upsert keeps the old payload when the incoming pos is null so stale clients can't corrupt a row - tests: +3 API tests (slug required, pos clamping, null-pos keeps saved), +1 end-to-end browser test (resume ~400s with a failed boot fetch); lockout test now uses a throwaway username so it can't lock out users the browser tests log in with; hardened the flaky autoplay test
This commit is contained in:
1 parent
7444e29592
commit
d90b1af8d6
4 files changed
+156
-23
No files matched your search
+17
-9
@@ -102,9 +102,9 @@ async function refreshHistory() {
|
||||
// ?all=1: resume positions live beyond the top-5 cap
|
||||
try { history = await api("/api/history?all=1"); } catch { /* home just shows empty */ }
|
||||
}
|
||||
function postHistory(entry) {
|
||||
function postHistory(entry, keepalive = false) {
|
||||
fetch(BASE + "api/history", {method: "POST", headers: {"Content-Type": "application/json", "X-Aniweb": "1"},
|
||||
body: JSON.stringify(entry)}).catch(() => {});
|
||||
body: JSON.stringify(entry), keepalive}).catch(() => {});
|
||||
}
|
||||
function postEvent(ev) {
|
||||
fetch(BASE + "api/events", {method: "POST", headers: {"Content-Type": "application/json", "X-Aniweb": "1"},
|
||||
@@ -1023,7 +1023,13 @@ async function play(ep_id, ep_no) {
|
||||
lastSavedPos = 0;
|
||||
lastSeenPos = 0;
|
||||
if (nextCta) { nextCta.remove(); nextCta = null; }
|
||||
const saved = history.find(x => x.slug_id === detail.slug_id && x.ep_id === ep_id);
|
||||
let saved = history.find(x => x.slug_id === detail.slug_id && x.ep_id === ep_id);
|
||||
if (!saved) {
|
||||
// the global may be empty (boot fetch failed — it swallows errors):
|
||||
// re-fetch so the write below can't clobber a real saved position
|
||||
await refreshHistory();
|
||||
saved = history.find(x => x.slug_id === detail.slug_id && x.ep_id === ep_id);
|
||||
}
|
||||
if (saved?.pos > 10) {
|
||||
v.addEventListener("loadedmetadata", () => {
|
||||
if (saved.pos < v.duration * 0.95) v.currentTime = saved.pos;
|
||||
@@ -1062,8 +1068,10 @@ async function play(ep_id, ep_no) {
|
||||
setTimeout(() => { if (v.paused && v.buffered.length) doPlay(); }, 3000);
|
||||
watchPos(v);
|
||||
setupIntro(v, ep_no);
|
||||
postHistory({...meta(v), title: detail.name, slug_id: detail.slug_id, season_id: seasonId,
|
||||
ep_id, lang, ep: ep_no, pos: saved?.pos || 0});
|
||||
// only bump the existing row to the top; with nothing saved a pos-0
|
||||
// write would clobber the server's position if the re-fetch also failed
|
||||
if (saved) postHistory({...meta(v), title: detail.name, slug_id: detail.slug_id, season_id: seasonId,
|
||||
ep_id, lang, ep: ep_no, pos: saved.pos});
|
||||
} catch (e) {
|
||||
showStreamError(e.message, () => play(ep_id, ep_no));
|
||||
}
|
||||
@@ -1104,7 +1112,7 @@ const meta = v => ({...(Number.isFinite(v.duration) && v.duration > 0 ? {dur: v.
|
||||
|
||||
// one savePos covers all video elements: it reads currentEp, so stale
|
||||
// closures from earlier plays can't write the wrong episode's position
|
||||
function savePos(floor = 0) {
|
||||
function savePos(floor = 0, keep = false) {
|
||||
if (!currentEp) return;
|
||||
// a fatal frag error can reset video.currentTime to 0 before this runs
|
||||
// (hls.js recovery); playerFail passes the furthest point reached as floor
|
||||
@@ -1113,7 +1121,7 @@ function savePos(floor = 0) {
|
||||
lastSavedPos = pos;
|
||||
postHistory({...meta(currentEp.v), title: detail.name, slug_id: detail.slug_id, season_id: seasonId,
|
||||
ep_id: currentEp.ep_id, lang, ep: currentEp.ep_no, pos,
|
||||
...(epsList.length ? {total: epsList.length} : {})});
|
||||
...(epsList.length ? {total: epsList.length} : {})}, keep);
|
||||
}
|
||||
|
||||
function watchPos(v) {
|
||||
@@ -1122,12 +1130,12 @@ function watchPos(v) {
|
||||
const seen = () => { lastSeenPos = Math.max(lastSeenPos, v.currentTime); };
|
||||
v.addEventListener("timeupdate", () => { seen(); if (v.currentTime - lastSavedPos >= 10) savePos(); });
|
||||
v.addEventListener("seeking", seen);
|
||||
v.addEventListener("pause", savePos);
|
||||
v.addEventListener("pause", () => savePos()); // not savePos: the event would become `floor` -> NaN pos
|
||||
// pagehide is bound once at boot (per-play listeners accumulated and fired
|
||||
// with stale video elements)
|
||||
}
|
||||
|
||||
window.addEventListener("pagehide", savePos);
|
||||
window.addEventListener("pagehide", () => savePos(0, true)); // keepalive: a plain fetch dies with the page
|
||||
|
||||
// ---------- boot ----------
|
||||
(async () => {
|
||||
|
||||
Reference in new issue
Block a user