add: queue total ETA from current encode speed

The queue card now shows an estimated time to clear: current job remaining +
sum(pending source durations) / current speed. Durations come from a
path+mtime-keyed cache that probes each file once in the background (ffprobe
format=duration, header-only), so the 1s poll never re-probes; entries are
pruned to the live queue. No speed (idle/held) -> no estimate; files still
being probed mark it partial (shown as '~Xh+').

Closes #9
This commit is contained in:
Esa Kataja
2026-06-21 20:48:37 +03:00
parent 04d6c85712
commit dfbd74b0d2
4 changed files with 198 additions and 16 deletions
+8 -3
View File
@@ -97,6 +97,7 @@
.qsum { display:flex; align-items:baseline; gap:.5rem; flex-wrap:wrap; margin-bottom:.8rem; }
.qsum .qn { font-family:var(--mono); font-size:1.4rem; color:var(--amber); line-height:1; }
.qsum .ql { color:var(--muted); font-size:.85rem; }
.qsum .qeta { font-family:var(--mono); font-size:.8rem; color:var(--amber-soft); margin-left:auto; }
ul.list { list-style:none; margin:0; padding:0; font-family:var(--mono); font-size:.82rem; }
ul.list li { padding:.35rem 0; border-bottom:1px solid var(--line); color:var(--muted); word-break:break-word; }
ul.list li:last-child { border-bottom:0; }
@@ -325,7 +326,8 @@
for (const s of arr) { while (!s.startsWith(p)) p = p.slice(0, -1); if (!p) break; }
return p;
}
function renderQueue(queue, current) {
function renderQueue(d) {
const queue = d.queue, current = d.current;
const cur = base(current && current.file);
const pending = (queue || []).filter(f => f !== cur);
const el = $("queue");
@@ -334,7 +336,10 @@
return;
}
const pre = pending.length > 1 ? commonPrefix(pending) : "";
const head = `<div class="qsum"><span class="qn">${pending.length}</span><span class="ql">${pending.length === 1 ? "file" : "files"} waiting</span></div>`;
const eta = d.queue_eta_sec > 0
? `<span class="qeta">~${fmtDurLong(d.queue_eta_sec)}${d.queue_eta_partial ? "+" : ""} to clear</span>`
: "";
const head = `<div class="qsum"><span class="qn">${pending.length}</span><span class="ql">${pending.length === 1 ? "file" : "files"} waiting</span>${eta}</div>`;
const rows = pending.slice(0, 3).map(f => {
const tail = pre ? f.slice(pre.length) : f;
return `<li>${pre ? `<span class="pre">${esc(pre)}</span>` : ""}<span class="key">${esc(tail)}</span></li>`;
@@ -429,7 +434,7 @@
renderControls(d);
renderJob(d.current);
renderFailed(d.failed);
renderQueue(d.queue, d.current);
renderQueue(d);
renderEvents(d.recent);
const active = d.current && d.current.file && d.current.phase !== "idle";
setLive(d.current && d.current.paused ? "idle" : active ? "ok" : "idle");