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)
This commit is contained in:
1 parent
202bfa41c3
commit
2f373b9bba
4 files changed
+218
-5
No files matched your search
@@ -167,6 +167,41 @@ def test_recent_searches(base=BASE):
|
||||
assert req("POST", "/api/searches", {"q": " "}, jar=a, base=base) == 422
|
||||
|
||||
|
||||
def test_stats(base=BASE):
|
||||
import sqlite3, time
|
||||
jar = login("erin", "erinpw12345", base)
|
||||
# fresh user: zeroed stats
|
||||
d = _json("GET", "/api/stats?days=7", jar=jar, base=base)
|
||||
assert d["watch_minutes"] == 0 and d["episodes"] == {"started": 0, "completed": 0}
|
||||
# synthetic viewing: 3 position events an hour ago, pos deltas of 80s each
|
||||
# (160s watched); one search
|
||||
c = sqlite3.connect(os.path.join(os.environ["ANIWEB_DATA"], "aniweb.db"))
|
||||
t0 = time.time() - 3600
|
||||
e = {"slug_id": "stat-a", "ep_id": 900, "title": "Stat Show", "lang": "jpn",
|
||||
"season_id": 1, "ep": "1", "dur": 1200}
|
||||
for i, pos in enumerate((0.0, 80.0, 160.0)):
|
||||
c.execute("INSERT INTO events (ts, user, type, payload) VALUES (?,?,?,?)",
|
||||
(t0 + 10 * i, "erin", "position", json.dumps({**e, "pos": pos, "t": t0 + 10 * i})))
|
||||
c.execute("INSERT INTO events (ts, user, type, payload) VALUES (?,?,?,?)",
|
||||
(t0, "erin", "search", json.dumps({"q": "stat anime"})))
|
||||
c.commit()
|
||||
c.close()
|
||||
d = _json("GET", "/api/stats?days=7", jar=jar, base=base)
|
||||
assert d["watch_minutes"] == 3, d # round(160/60)
|
||||
assert d["episodes"] == {"started": 1, "completed": 0}
|
||||
assert d["days_active"] >= 1
|
||||
assert sum(d["by_weekday"]) == 160 # seconds, Mon..Sun
|
||||
assert d["top_anime"][0]["title"] == "Stat Show"
|
||||
assert d["top_searches"] == ["stat anime"]
|
||||
assert d["sub_dub"]["jpn"] >= 1 and d["sub_dub"]["eng"] == 0
|
||||
# per-user isolation: alice never watched stat-a
|
||||
a = login("alice", "alicepw12345", base)
|
||||
da = _json("GET", "/api/stats?days=7", jar=a, base=base)
|
||||
assert all(x["title"] != "Stat Show" for x in da["top_anime"])
|
||||
# days window is validated
|
||||
assert req("GET", "/api/stats?days=0", jar=jar, base=base) == 422
|
||||
|
||||
|
||||
def test_self_delete(base=BASE):
|
||||
jar = login("dave", "davepw12345", base)
|
||||
# give dave a searches file too: account delete must remove it
|
||||
|
||||
Reference in new issue
Block a user