Smart scan: poll the probe over a grace window (server auto-scan needs ~1-2 min)
The probe fired the instant a batch finished, but a fresh drop takes ~1-2 min to appear even with the server's auto-scan ON — so it always missed and we crawled anyway, defeating the optimization. Now probe_present_libraries POLLS each candidate over a grace window (probe_grace_minutes, default 2), skipping a library's crawl as soon as the server reports it has the item, and only crawling what's still missing when grace expires. The probe target for a media type you DIDN'T just download is an old item the server already has → confirmed instantly, no wait. grace=0 probes once.
This commit is contained in:
parent
9e845e760e
commit
0cf8654f47
3 changed files with 95 additions and 16 deletions
|
|
@ -236,6 +236,7 @@ ACTIONS: list[dict] = [
|
||||||
{"value": "show", "label": "TV only"}],
|
{"value": "show", "label": "TV only"}],
|
||||||
"default": "all"},
|
"default": "all"},
|
||||||
{"key": "skip_if_present", "type": "checkbox", "label": "Skip the scan if the server already has the download", "default": True},
|
{"key": "skip_if_present", "type": "checkbox", "label": "Skip the scan if the server already has the download", "default": True},
|
||||||
|
{"key": "probe_grace_minutes", "type": "number", "label": "Give the server's auto-scan this long to ingest first (min)", "default": 2, "min": 0},
|
||||||
{"key": "max_wait_minutes", "type": "number", "label": "Max wait for scan (min)", "default": 60, "min": 1},
|
{"key": "max_wait_minutes", "type": "number", "label": "Max wait for scan (min)", "default": 60, "min": 1},
|
||||||
{"key": "debounce_seconds", "type": "number", "label": "Fallback wait if status unknown (sec)", "default": 120, "min": 10}
|
{"key": "debounce_seconds", "type": "number", "label": "Fallback wait if status unknown (sec)", "default": 120, "min": 10}
|
||||||
]},
|
]},
|
||||||
|
|
|
||||||
|
|
@ -233,6 +233,36 @@ def wait_for_server_scan(scan_status, sleep, *, grace_seconds: int = 15,
|
||||||
return waited
|
return waited
|
||||||
|
|
||||||
|
|
||||||
|
def probe_present_libraries(candidates, has_item, sleep, *, grace_seconds: int = 120,
|
||||||
|
interval_seconds: int = 15):
|
||||||
|
"""Which libraries does the server ALREADY have the newest grab for?
|
||||||
|
|
||||||
|
``candidates`` = [(scope, item), …]. A freshly-dropped file takes ~1-2 min to show
|
||||||
|
up even with the server's auto-scan ON, so we POLL: check each candidate, and give
|
||||||
|
the server up to ``grace_seconds`` (re-checking every ``interval_seconds``) to
|
||||||
|
ingest before giving up on it. Returns the set of scopes confirmed present (→ skip
|
||||||
|
their crawl); anything still missing when grace expires stays out (→ gets crawled).
|
||||||
|
A probe that errors is treated as 'not present' (conservative). ``sleep`` injected."""
|
||||||
|
pending = list(candidates or [])
|
||||||
|
present = set()
|
||||||
|
waited = 0
|
||||||
|
while True:
|
||||||
|
still = []
|
||||||
|
for scope, item in pending:
|
||||||
|
try:
|
||||||
|
if has_item(scope, item):
|
||||||
|
present.add(scope)
|
||||||
|
continue
|
||||||
|
except Exception: # noqa: BLE001 - uncertainty → keep probing, then scan
|
||||||
|
pass
|
||||||
|
still.append((scope, item))
|
||||||
|
pending = still
|
||||||
|
if not pending or waited >= grace_seconds:
|
||||||
|
return present
|
||||||
|
sleep(interval_seconds)
|
||||||
|
waited += interval_seconds
|
||||||
|
|
||||||
|
|
||||||
_LIB = {'movie': 'Movie', 'show': 'TV'}
|
_LIB = {'movie': 'Movie', 'show': 'TV'}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -274,29 +304,39 @@ def auto_video_scan_server(
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
cap = 3600
|
cap = 3600
|
||||||
try:
|
try:
|
||||||
# Which libraries actually need the expensive crawl? Skip any the server
|
_pg = config.get('probe_grace_minutes') # 0 is valid (probe once, no wait)
|
||||||
# already has the newest download for (it auto-ingested → nothing to find).
|
grace = max(0, int(float(_pg if _pg is not None else 2) * 60))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
grace = 120
|
||||||
|
try:
|
||||||
|
# Which libraries actually need the expensive crawl? Skip any the server already
|
||||||
|
# has the newest grab for (it auto-ingested → nothing to find). Fresh drops take
|
||||||
|
# ~1-2 min to appear even with auto-scan ON, so POLL the probe over a grace
|
||||||
|
# window instead of checking once immediately (which would always miss).
|
||||||
scopes = ['movie', 'show'] if media_type == 'all' else \
|
scopes = ['movie', 'show'] if media_type == 'all' else \
|
||||||
([media_type] if media_type in ('movie', 'show') else ['movie', 'show'])
|
([media_type] if media_type in ('movie', 'show') else ['movie', 'show'])
|
||||||
to_scan = []
|
present = set()
|
||||||
for sc in scopes:
|
if skip_if_present:
|
||||||
if skip_if_present:
|
candidates = []
|
||||||
latest = None
|
for sc in scopes:
|
||||||
try:
|
try:
|
||||||
latest = latest_completed(sc)
|
latest = latest_completed(sc)
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
latest = None
|
latest = None
|
||||||
if latest:
|
if latest:
|
||||||
try:
|
candidates.append((sc, latest))
|
||||||
present = bool(server_has_item(sc, latest))
|
if candidates:
|
||||||
except Exception: # noqa: BLE001
|
deps.update_progress(automation_id, progress=15,
|
||||||
present = False
|
phase='Checking whether the server already has the new downloads…',
|
||||||
if present:
|
log_line='Probing the server (giving its auto-scan time to ingest)…',
|
||||||
deps.update_progress(
|
log_type='info')
|
||||||
automation_id, log_line=f'{_LIB[sc]} library: server already has the newest grab — skipping its scan',
|
present = probe_present_libraries(candidates, server_has_item, sleep,
|
||||||
log_type='info')
|
grace_seconds=grace)
|
||||||
continue
|
for sc in present:
|
||||||
to_scan.append(sc)
|
deps.update_progress(
|
||||||
|
automation_id, log_line=f'{_LIB[sc]} library: server already has the newest grab — skipping its scan',
|
||||||
|
log_type='info')
|
||||||
|
to_scan = [sc for sc in scopes if sc not in present]
|
||||||
|
|
||||||
waited, server_name = 0, ''
|
waited, server_name = 0, ''
|
||||||
if to_scan:
|
if to_scan:
|
||||||
|
|
|
||||||
|
|
@ -383,6 +383,44 @@ class TestSmartScanSkip:
|
||||||
latest_completed=lambda sc: {'title': 'X'}, server_has_item=_boom)
|
latest_completed=lambda sc: {'title': 'X'}, server_has_item=_boom)
|
||||||
assert refreshed == ['all'] # uncertainty → scan (safe)
|
assert refreshed == ['all'] # uncertainty → scan (safe)
|
||||||
|
|
||||||
|
def test_waits_for_the_servers_autoscan_then_skips(self):
|
||||||
|
# The file isn't on the server immediately (fresh drop) — it appears on the 3rd
|
||||||
|
# probe. We poll over the grace window and skip the crawl once it shows up.
|
||||||
|
calls = {'show': 0}
|
||||||
|
slept = []
|
||||||
|
|
||||||
|
def _has(sc, item):
|
||||||
|
calls[sc] += 1
|
||||||
|
return calls[sc] >= 3 # present only on the 3rd check
|
||||||
|
|
||||||
|
deps = _RecordingDeps()
|
||||||
|
refreshed = []
|
||||||
|
res = auto_video_scan_server(
|
||||||
|
{'_automation_id': 'a', 'media_type': 'show', 'probe_grace_minutes': 5}, deps,
|
||||||
|
server_refresh=lambda mt=None: refreshed.append(mt) or {'ok': True},
|
||||||
|
sleep=slept.append, scan_status=lambda mt: False,
|
||||||
|
latest_completed=lambda sc: {'title': 'X'}, server_has_item=_has,
|
||||||
|
emit=lambda ev, data: None)
|
||||||
|
assert refreshed == [] # never crawled — auto-scan won
|
||||||
|
assert res['skipped'] == ['show'] and len(slept) == 2 # polled twice before it appeared
|
||||||
|
|
||||||
|
def test_probe_grace_zero_checks_once_immediately(self):
|
||||||
|
calls = {'show': 0}
|
||||||
|
|
||||||
|
def _has(sc, item):
|
||||||
|
calls[sc] += 1
|
||||||
|
return False
|
||||||
|
|
||||||
|
deps = _RecordingDeps()
|
||||||
|
refreshed, slept = [], []
|
||||||
|
auto_video_scan_server(
|
||||||
|
{'_automation_id': 'a', 'media_type': 'show', 'probe_grace_minutes': 0}, deps,
|
||||||
|
server_refresh=lambda mt=None: refreshed.append(mt) or {'ok': True},
|
||||||
|
sleep=slept.append, scan_status=lambda mt: False,
|
||||||
|
latest_completed=lambda sc: {'title': 'X'}, server_has_item=_has,
|
||||||
|
emit=lambda ev, data: None)
|
||||||
|
assert calls['show'] == 1 and refreshed == ['show'] # one probe, no grace wait, then scan
|
||||||
|
|
||||||
|
|
||||||
# ── post-download chain: video_update_database (stage 2) ───────────────────
|
# ── post-download chain: video_update_database (stage 2) ───────────────────
|
||||||
class TestUpdateDatabaseStage:
|
class TestUpdateDatabaseStage:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue