video: longer adaptive slskd wait + stop the automations page blink

two issues:
1. slskd search gave up at 22s (then stopped the search), but slskd gathers peers
   over its full ~60s window — so most movie searches were killed before results
   arrived. now waits up to 55s but returns early once results SETTLE (12+ hits, or
   no new hits for ~12s) so fast searches don't burn the whole window. matches the
   music side's longer wait.
2. the automations page re-rendered the WHOLE system section every 8s poll
   (replaceChild) → blink + wiped the socket's live progress. now it skips the
   rebuild unless something structural changed (added/removed/toggled/ran); live
   progress comes via socket, the countdown ticks locally.
This commit is contained in:
BoulderBadgeDad 2026-06-26 09:54:18 -07:00
parent b55c245fe6
commit 8fe73ac924
3 changed files with 39 additions and 6 deletions

View file

@ -261,11 +261,17 @@ def _fail_or_retry(db, dl, error_msg) -> None:
_archive_history(db, dl, {"status": "failed", "error": err, "completed_at": completed}) _archive_history(db, dl, {"status": "failed", "error": err, "completed_at": completed})
def _search_for_retry(query, max_seconds=22): def _search_for_retry(query, max_seconds=55):
"""A bounded blocking slskd search for the retry worker (shorter than the UI's). """A bounded blocking slskd search for the retry worker + the auto-grab automation.
slskd gathers peer responses over its full search window (~60s by default), so a short
wait misses almost everything. We poll up to ``max_seconds`` but return EARLY once
results have arrived and settled break on either plenty of hits (12+) or no new hits
for ~12s after getting some, so fast searches don't burn the whole window.
Always STOPS the slskd search when done otherwise it keeps running its full timeout Always STOPS the slskd search when done otherwise it keeps running its full timeout
(~60s) and, since the bounded auto-grab fires searches back-to-back, they pile up on and, since the auto-grab fires searches back-to-back, they pile up. Stopping each one
slskd. Stopping each one keeps concurrent slskd searches the worker pool size.""" keeps concurrent slskd searches the worker pool size."""
from core.video.slskd_search import poll_search, start_search, stop_search from core.video.slskd_search import poll_search, start_search, stop_search
res = start_search(query) res = start_search(query)
sid = res.get("id") sid = res.get("id")
@ -273,11 +279,20 @@ def _search_for_retry(query, max_seconds=22):
return {"hits": [], "total_files": 0} return {"hits": [], "total_files": 0}
deadline = time.monotonic() + max_seconds deadline = time.monotonic() + max_seconds
last = {"hits": [], "total_files": 0} last = {"hits": [], "total_files": 0}
prev, settle = 0, 0 # track hit growth; settle = consecutive no-growth polls (~1.5s each)
try: try:
while time.monotonic() < deadline: while time.monotonic() < deadline:
last = poll_search(sid) last = poll_search(sid)
if len(last.get("hits") or []) >= 12: n = len(last.get("hits") or [])
break if n >= 12:
break # plenty — stop waiting
if n > prev:
settle = 0 # still arriving — keep waiting
elif n > 0:
settle += 1
if settle >= 8: # ~12s with no new hits → results have settled
break
prev = n
time.sleep(1.5) time.sleep(1.5)
return last return last
finally: finally:

View file

@ -103,6 +103,13 @@ def test_video_page_exposes_reload_hook():
# --- the System list is shown in a logical pipeline order ----------------- # --- the System list is shown in a logical pipeline order -----------------
def test_video_automations_poll_does_not_rebuild_when_unchanged():
# regression: the 8s poll used to replaceChild the whole section every time → blink +
# wiped live progress. It must skip the rebuild when nothing structural changed.
assert 'if (sig === _lastSig) return;' in _VAUTO
assert 'var sig = JSON.stringify(' in _VAUTO
def test_video_system_automations_are_sorted_by_pipeline_order(): def test_video_system_automations_are_sorted_by_pipeline_order():
# The API returns newest-created-first (jumbled); the page re-sorts by an # The API returns newest-created-first (jumbled); the page re-sorts by an
# explicit order so it reads scans → processors → library → maintenance. # explicit order so it reads scans → processors → library → maintenance.

View file

@ -91,10 +91,21 @@
'<span class="auto-stat"><strong>' + sys.length + '</strong> System</span>'; '<span class="auto-stat"><strong>' + sys.length + '</strong> System</span>';
} }
var _lastSig = null;
function load() { function load() {
return getJSON('/api/automations').then(function (d) { return getJSON('/api/automations').then(function (d) {
var all = Array.isArray(d) ? d : (d && d.automations) || []; var all = Array.isArray(d) ? d : (d && d.automations) || [];
var sys = sortSystem(all.filter(isVideoAutomation)); var sys = sortSystem(all.filter(isVideoAutomation));
// Re-rendering the whole System section every 8s poll destroys + recreates every
// card — that's the blink, and it wipes the live progress the socket patches in.
// Only rebuild when something STRUCTURAL changed (added/removed/toggled/ran);
// live progress arrives via socket, the "Next: in Xm" countdown ticks locally.
var sig = JSON.stringify(sys.map(function (a) {
return [a.id, a.enabled, a.name, a.trigger_type, a.action_type,
a.last_run, a.next_run, a.run_count, a.last_result];
}));
if (sig === _lastSig) return;
_lastSig = sig;
renderStats(sys); renderStats(sys);
renderSystem(sys); renderSystem(sys);
renderHubOnce(); renderHubOnce();