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.
This commit is contained in:
jozamudi committed 2026-09-21 01:41:44 +00:00
1 parent 5f7aa010f1
commit 731f44b99c
3 files changed
+22 -9

No files matched your search

+10 -3
View File
@@ -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()
+9 -6
View File
@@ -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");
+3
View File
@@ -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. -->
<script src="hls.min.js"></script>
<!-- Watched threshold: server injects its DONE_RATIO source constant here;
app.js reads it as DONE_RATIO. Server uses the same value for its stats. -->
<script>window.DONE_RATIO = {{DONE_RATIO}};</script>
<script src="app.js?v={{VERSION}}"></script>
</body>
</html>