From 8fe73ac924eafb05f18775cd6bb85567d6326b7e Mon Sep 17 00:00:00 2001 From: BoulderBadgeDad Date: Fri, 26 Jun 2026 09:54:18 -0700 Subject: [PATCH] video: longer adaptive slskd wait + stop the automations page blink MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- core/video/download_monitor.py | 27 +++++++++++++++++++------ tests/test_video_automations_builder.py | 7 +++++++ webui/static/video/video-automations.js | 11 ++++++++++ 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/core/video/download_monitor.py b/core/video/download_monitor.py index 038d8462..a3ed2ece 100644 --- a/core/video/download_monitor.py +++ b/core/video/download_monitor.py @@ -261,11 +261,17 @@ def _fail_or_retry(db, dl, error_msg) -> None: _archive_history(db, dl, {"status": "failed", "error": err, "completed_at": completed}) -def _search_for_retry(query, max_seconds=22): - """A bounded blocking slskd search for the retry worker (shorter than the UI's). +def _search_for_retry(query, max_seconds=55): + """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 - (~60s) and, since the bounded auto-grab fires searches back-to-back, they pile up on - slskd. Stopping each one keeps concurrent slskd searches ≈ the worker pool size.""" + and, since the auto-grab fires searches back-to-back, they pile up. Stopping each one + keeps concurrent slskd searches ≈ the worker pool size.""" from core.video.slskd_search import poll_search, start_search, stop_search res = start_search(query) sid = res.get("id") @@ -273,11 +279,20 @@ def _search_for_retry(query, max_seconds=22): return {"hits": [], "total_files": 0} deadline = time.monotonic() + max_seconds last = {"hits": [], "total_files": 0} + prev, settle = 0, 0 # track hit growth; settle = consecutive no-growth polls (~1.5s each) try: while time.monotonic() < deadline: last = poll_search(sid) - if len(last.get("hits") or []) >= 12: - break + n = len(last.get("hits") or []) + 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) return last finally: diff --git a/tests/test_video_automations_builder.py b/tests/test_video_automations_builder.py index 506a20f5..b91b7742 100644 --- a/tests/test_video_automations_builder.py +++ b/tests/test_video_automations_builder.py @@ -103,6 +103,13 @@ def test_video_page_exposes_reload_hook(): # --- 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(): # The API returns newest-created-first (jumbled); the page re-sorts by an # explicit order so it reads scans → processors → library → maintenance. diff --git a/webui/static/video/video-automations.js b/webui/static/video/video-automations.js index b977a5c2..9059c206 100644 --- a/webui/static/video/video-automations.js +++ b/webui/static/video/video-automations.js @@ -91,10 +91,21 @@ '' + sys.length + ' System'; } + var _lastSig = null; function load() { return getJSON('/api/automations').then(function (d) { var all = Array.isArray(d) ? d : (d && d.automations) || []; 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); renderSystem(sys); renderHubOnce();