api: dedup ghost history rows (int vs string ep_id); normalize ep_id on write
This commit is contained in:
1 parent
8f57593002
commit
d39ee838ff
2 files changed
+32
-4
No files matched your search
+17
-2
@@ -125,9 +125,17 @@ HIST_DIR = Path(os.environ.get("ANIWEB_DATA", Path(__file__).parent / "data")) /
|
|||||||
|
|
||||||
def _load_hist(user: str):
|
def _load_hist(user: str):
|
||||||
try:
|
try:
|
||||||
return json.loads((HIST_DIR / f"{user}.json").read_text())
|
entries = json.loads((HIST_DIR / f"{user}.json").read_text())
|
||||||
except (FileNotFoundError, json.JSONDecodeError):
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
return []
|
return []
|
||||||
|
# ponytail: dedup legacy ghost rows — the same episode was stored with ep_id
|
||||||
|
# as int AND string; keep the newest per (slug_id, str(ep_id))
|
||||||
|
best = {}
|
||||||
|
for e in entries:
|
||||||
|
k = (str(e.get("slug_id")), str(e.get("ep_id")))
|
||||||
|
if e.get("t", 0) >= best.get(k, {"t": 0})["t"]:
|
||||||
|
best[k] = e
|
||||||
|
return sorted(best.values(), key=lambda e: e.get("t", 0), reverse=True)
|
||||||
|
|
||||||
|
|
||||||
def _save_hist(user: str, entries):
|
def _save_hist(user: str, entries):
|
||||||
@@ -162,11 +170,18 @@ def api_history_delete(request: Request, slug_id: str, ep_id: str):
|
|||||||
def api_history_post(entry: dict = Body(...), request: Request = None):
|
def api_history_post(entry: dict = Body(...), request: Request = None):
|
||||||
if "ep_id" not in entry:
|
if "ep_id" not in entry:
|
||||||
raise HTTPException(422, "ep_id required")
|
raise HTTPException(422, "ep_id required")
|
||||||
|
# normalize: ghost rows were born when ep_id arrived as str against an
|
||||||
|
# int row (or vice versa) — int() makes the stored type stable
|
||||||
|
try:
|
||||||
|
entry["ep_id"] = int(entry["ep_id"])
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise HTTPException(422, "ep_id must be an integer")
|
||||||
entry["t"] = time.time()
|
entry["t"] = time.time()
|
||||||
u = request.state.user
|
u = request.state.user
|
||||||
entries = _load_hist(u)
|
entries = _load_hist(u)
|
||||||
for i, e in enumerate(entries):
|
for i, e in enumerate(entries):
|
||||||
if e.get("slug_id") == entry.get("slug_id") and e.get("ep_id") == entry.get("ep_id"):
|
# loose str() match: merge onto a legacy ghost row of the other type
|
||||||
|
if e.get("slug_id") == entry.get("slug_id") and str(e.get("ep_id")) == str(entry["ep_id"]):
|
||||||
entries[i] = {**e, **entry}
|
entries[i] = {**e, **entry}
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
|
|||||||
+15
-2
@@ -111,8 +111,9 @@ def test_history_per_user(base=BASE):
|
|||||||
assert req("POST", "/api/history", entry, jar=a, base=base) == 200
|
assert req("POST", "/api/history", entry, jar=a, base=base) == 200
|
||||||
got_a = _json("GET", "/api/history", jar=a, base=base)
|
got_a = _json("GET", "/api/history", jar=a, base=base)
|
||||||
got_b = _json("GET", "/api/history", jar=b, base=base)
|
got_b = _json("GET", "/api/history", jar=b, base=base)
|
||||||
assert any(e["ep_id"] == "111" for e in got_a)
|
# ep_id is normalized to int on write (ghost-row fix)
|
||||||
assert all(e.get("ep_id") != "111" for e in got_b), "leak: admin entry in alice's history"
|
assert any(e["ep_id"] == 111 for e in got_a)
|
||||||
|
assert all(e.get("ep_id") != 111 for e in got_b), "leak: admin entry in alice's history"
|
||||||
|
|
||||||
|
|
||||||
def test_history_all_and_delete(base=BASE):
|
def test_history_all_and_delete(base=BASE):
|
||||||
@@ -138,6 +139,18 @@ def test_history_all_and_delete(base=BASE):
|
|||||||
jar=jar, base=base)["removed"] == 0
|
jar=jar, base=base)["removed"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_history_no_ghost_duplicates(base=BASE):
|
||||||
|
"""same episode posted with int and string ep_id -> exactly one row"""
|
||||||
|
jar = login("alice", "alicepw12345", base)
|
||||||
|
for ep in (301, "301"):
|
||||||
|
assert req("POST", "/api/history",
|
||||||
|
{"ep_id": ep, "slug_id": "ghost-a", "title": "Ghost", "season_id": 1,
|
||||||
|
"ep": "1", "lang": "eng", "pos": 5}, jar=jar, base=base) == 200
|
||||||
|
rows = [e for e in _json("GET", "/api/history?all=1", jar=jar, base=base)
|
||||||
|
if e["slug_id"] == "ghost-a"]
|
||||||
|
assert len(rows) == 1 and rows[0]["ep_id"] == 301, rows
|
||||||
|
|
||||||
|
|
||||||
def test_recent_searches(base=BASE):
|
def test_recent_searches(base=BASE):
|
||||||
a = login("alice", "alicepw12345", base)
|
a = login("alice", "alicepw12345", base)
|
||||||
assert req("POST", "/api/searches", {"q": "naruto"}, jar=a, base=base) == 200
|
assert req("POST", "/api/searches", {"q": "naruto"}, jar=a, base=base) == 200
|
||||||
|
|||||||
Reference in new issue
Block a user