From 731f44b99c0c205bf0d4249c5b57f4ae631cc9d2 Mon Sep 17 00:00:00 2001 From: Josue Zamudio Date: Mon, 21 Sep 2026 01:41:44 +0000 Subject: [PATCH] watched: single DONE_RATIO source constant replaces the seven hardcoded 0.95 literals server/app.py DONE_RATIO is injected into index.html like {{VERSION}}, app.js reads it as window.DONE_RATIO (fallback = same default). Change the threshold in exactly one place now. --- server/app.py | 13 ++++++++++--- static/app.js | 15 +++++++++------ static/index.html | 3 +++ 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/server/app.py b/server/app.py index f2b04bb..675ddc7 100644 --- a/server/app.py +++ b/server/app.py @@ -44,6 +44,11 @@ def _version(): VERSION = _version() +# An episode counts as "watched" once you're this far through it (fraction of +# duration). One knob for client + server: injected into index.html below so +# app.js uses the same value as these stats. Change here only. +DONE_RATIO = 0.95 + # latest probe result + last 20 samples (~1h at 3-min spacing), for GET /api/cdn _CDN = {"state": "unknown", "kbps": 0.0, "ts": 0.0, "hist": []} # origin (primary provider pages/API) health — separate host, separate failure @@ -455,7 +460,7 @@ def api_stats(request: Request, days: int = Query(30, ge=1, le=365), if s["last_ts"] > a["last_seen"]: a["last_seen"] = s["last_ts"] a["cur_ep"] = s["last_ep"] - if s["dur"] > 0 and s["max"] >= s["dur"] * 0.95: + if s["dur"] > 0 and s["max"] >= s["dur"] * DONE_RATIO: a["completed"] += 1 completed += 1 now = time.time() @@ -700,8 +705,10 @@ def stream(token: str): @app.get("/index.html", include_in_schema=False) def index(): # index.html is served (not mounted) so ?v={{VERSION}} becomes the git - # SHA at startup; other statics go through the mount below - return HTMLResponse(_INDEX_HTML.replace("{{VERSION}}", VERSION)) + # SHA and {{DONE_RATIO}} the watched threshold at startup; other statics + # go through the mount below + return HTMLResponse(_INDEX_HTML.replace("{{VERSION}}", VERSION) + .replace("{{DONE_RATIO}}", repr(DONE_RATIO))) _INDEX_HTML = (ROOT / "static" / "index.html").read_text() diff --git a/static/app.js b/static/app.js index 2da902c..c13ad2f 100644 --- a/static/app.js +++ b/static/app.js @@ -10,6 +10,9 @@ const BASE = (() => { return p.endsWith("/") ? p : p.slice(0, p.lastIndexOf("/") + 1); })(); const main = document.getElementById("main"); +// Watched threshold (fraction of duration) — injected inline by the server +// from DONE_RATIO in server/app.py; fallback matches that default. +const DONE_RATIO = Number(window.DONE_RATIO ?? 0.95); let hls = null, detail = null, lang = "jpn", seasonId = null; async function api(path) { @@ -190,7 +193,7 @@ const homeCard = x => { c.append(poster(x.poster, x.title)); let meta = `E${x.ep} \u00b7 ${x.lang === "eng" ? "dub" : "sub"}`; if (done) meta += " \u00b7 \u2713"; - else if (x.dur > 0 && x.pos < x.dur * 0.95) meta += ` \u00b7 ${fmtMin((x.dur - x.pos) / 60)} left`; + else if (x.dur > 0 && x.pos < x.dur * DONE_RATIO) meta += ` \u00b7 ${fmtMin((x.dur - x.pos) / 60)} left`; else if (hold) meta += ` \u00b7 ${Math.round(Date.now() / 1000 / 86400 - x.t / 86400)}d ago`; const info = el("div", "mini-info"); info.append(el("div", "card-name", x.title), el("div", "mini-meta", meta)); @@ -250,11 +253,11 @@ function showHome() { body.append(el("div", "hero-kicker", "continue watching"), el("h2", null, hero.title), el("div", "muted", `Ep ${hero.ep} · ${hero.lang === "eng" ? "dub" : "sub"} · ${fmtPos(hero.pos)} in` + - (hero.dur > 0 && hero.pos < hero.dur * 0.95 ? ` · ${fmtMin((hero.dur - hero.pos) / 60)} left` : "") + + (hero.dur > 0 && hero.pos < hero.dur * DONE_RATIO ? ` · ${fmtMin((hero.dur - hero.pos) / 60)} left` : "") + ` · ${timeago(hero.t)}`)); const p = bar(hero); if (p) body.append(p); - const heroDone = hero.dur > 0 && hero.pos >= hero.dur * 0.95; + const heroDone = hero.dur > 0 && hero.pos >= hero.dur * DONE_RATIO; const go = el("button", "btn resume", heroDone ? "\u25b6 Next ep" : "\u25b6 Resume"); go.onclick = () => location.href = histHref(hero); right.append(body, go); @@ -503,7 +506,7 @@ async function showAnime(slug_id, opts = {}) { for (const x of await api("/api/history?all=1")) { if (x.slug_id !== slug_id || x.season_id !== seasonId) continue; started = true; - if (x.dur > 0 && x.pos >= x.dur * 0.95) watchDone.add(String(x.ep_id)); + if (x.dur > 0 && x.pos >= x.dur * DONE_RATIO) watchDone.add(String(x.ep_id)); else if (x.dur > 0) epProg.set(String(x.ep_id), Math.min(100, Math.round(100 * x.pos / x.dur))); } } catch {} @@ -529,7 +532,7 @@ async function showAnime(slug_id, opts = {}) { function langLabel() { return lang === "eng" ? "▶ dub" : "▶ sub"; } // ---------- history page (full list + delete + client-side filter) ---------- -const histClass = x => x.dur > 0 && x.pos >= x.dur * 0.95 ? "done" +const histClass = x => x.dur > 0 && x.pos >= x.dur * DONE_RATIO ? "done" : (x.t && Date.now() / 1000 - x.t > 7 * 86400 ? "hold" : "watching"); function remapBoard() { // home: one stale card per stale anime (latest row), above the hero, so a @@ -1215,7 +1218,7 @@ async function play(ep_id, ep_no) { } if (saved?.pos > 10) { v.addEventListener("loadedmetadata", () => { - if (saved.pos < v.duration * 0.95) v.currentTime = saved.pos; + if (saved.pos < v.duration * DONE_RATIO) v.currentTime = saved.pos; }, {once: true}); } v.onerror = () => playerFail("media error"); diff --git a/static/index.html b/static/index.html index 33b4280..e3d3ecc 100644 --- a/static/index.html +++ b/static/index.html @@ -35,6 +35,9 @@ route) with the git SHA at startup — a correct cache key per commit. No hand-bumping; JSV in app.js reads the same value from its own URL. --> + +