Stats: GitHub-style watch-time heatmap

- api/stats gains by_day: local-date -> seconds (same watch-time
  heuristic as by_weekday)
- ?stats renders a Mon..Sun x week grid (30d window, matches the
  page header) with 4 intensity levels + tooltip per cell + legend;
  replaces the by-weekday bar chart the heatmap subsumes
- test_stats covers by_day (sums, slug-scoped, empty case)
This commit is contained in:
jozamudi committed 2026-08-24 22:24:16 +00:00
1 parent 7d9fab6bd9
commit 7b670cff24
4 files changed
+52 -20

No files matched your search

+9 -3
View File
@@ -281,6 +281,7 @@ def api_stats(request: Request, days: int = Query(30, ge=1, le=365),
# viewing; a backward jump >60s within an ep counts as a rewatch
eps = {}
watch_sec = [0.0] * 7 # Mon..Sun
by_day = {} # local YYYY-MM-DD -> seconds
for ts, payload in positions:
e = json.loads(payload)
k = (str(e.get("slug_id")), e.get("season_id"), e.get("ep_id"))
@@ -299,9 +300,13 @@ def api_stats(request: Request, days: int = Query(30, ge=1, le=365),
if e.get("total"):
s["total"] = e["total"]
if s["pos"] is not None and 0 < p - s["pos"] <= 90:
s["secs"] += p - s["pos"]
s["eng" if e.get("lang") == "eng" else "jpn"] += p - s["pos"]
watch_sec[datetime.fromtimestamp(ts).weekday()] += p - s["pos"]
d = p - s["pos"]
s["secs"] += d
s["eng" if e.get("lang") == "eng" else "jpn"] += d
dt = datetime.fromtimestamp(ts)
watch_sec[dt.weekday()] += d
k = dt.strftime("%Y-%m-%d")
by_day[k] = by_day.get(k, 0) + d
elif s["pos"] is not None and p < s["pos"] - 60:
s["rewinds"] += 1
s["pos"] = p
@@ -362,6 +367,7 @@ def api_stats(request: Request, days: int = Query(30, ge=1, le=365),
"plays": len(plays),
"errors": n_errors,
"by_weekday": [int(x) for x in watch_sec], # seconds, Mon..Sun
"by_day": {k: int(v) for k, v in by_day.items()}, # seconds per local day
"top_anime": top,
"top_searches": [q for q, _ in Counter(searches).most_common(5) if q],
"sub_dub": {"jpn": plays_by_lang.get("jpn", 0), "eng": plays_by_lang.get("eng", 0)},