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
+23
-2
@@ -10,6 +10,7 @@ import os
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from math import isfinite
|
||||
from collections import Counter, OrderedDict
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
@@ -271,6 +272,25 @@ def api_history_post(entry: dict = Body(...), request: Request = None):
|
||||
entry["ep_id"] = int(entry["ep_id"])
|
||||
except (TypeError, ValueError):
|
||||
raise HTTPException(422, "ep_id must be an integer")
|
||||
slug = str(entry.get("slug_id") or "").strip()
|
||||
if not slug:
|
||||
raise HTTPException(422, "slug_id required") # a missing slug used to store a "None" row
|
||||
entry["slug_id"] = slug
|
||||
if entry.get("pos") is not None:
|
||||
try:
|
||||
pos = max(0.0, float(entry["pos"]))
|
||||
except (TypeError, ValueError):
|
||||
raise HTTPException(422, "pos must be a number")
|
||||
if not isfinite(pos):
|
||||
raise HTTPException(422, "pos not finite")
|
||||
if entry.get("dur"): # clamp above-duration junk out of the progress UI
|
||||
try:
|
||||
pos = min(pos, float(entry["dur"]))
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
entry["pos"] = pos
|
||||
# a missing/null pos stays null: the upsert keeps the existing row's
|
||||
# payload, so a stale client can never clobber the saved position
|
||||
entry["t"] = time.time()
|
||||
u = request.state.user
|
||||
c = db.connect()
|
||||
@@ -278,8 +298,9 @@ def api_history_post(entry: dict = Body(...), request: Request = None):
|
||||
# read-modify-write (and no lost-update race between the read and write)
|
||||
c.execute("INSERT INTO history (user, slug_id, ep_id, t, payload) VALUES (?,?,?,?,?) "
|
||||
"ON CONFLICT(user, slug_id, ep_id) DO UPDATE SET "
|
||||
"t=excluded.t, payload=excluded.payload",
|
||||
(u, str(entry.get("slug_id")), entry["ep_id"], entry["t"], json.dumps(entry)))
|
||||
"t=excluded.t, payload=CASE WHEN json_extract(excluded.payload, '$.pos') IS NULL "
|
||||
"THEN history.payload ELSE excluded.payload END",
|
||||
(u, entry["slug_id"], entry["ep_id"], entry["t"], json.dumps(entry)))
|
||||
# cap: newest 50 per user
|
||||
c.execute("DELETE FROM history WHERE user=? AND rowid NOT IN "
|
||||
"(SELECT rowid FROM history WHERE user=? ORDER BY t DESC LIMIT 50)", (u, u))
|
||||
|
||||
Reference in new issue
Block a user