diff --git a/core/automation/handlers/video_process_wishlist.py b/core/automation/handlers/video_process_wishlist.py index bbe1f6dc..b2f25e46 100644 --- a/core/automation/handlers/video_process_wishlist.py +++ b/core/automation/handlers/video_process_wishlist.py @@ -213,6 +213,8 @@ def auto_video_process_wishlist( grabbed = [0] searched = [0] + noresults = [0] # search came back empty (the source had nothing) + rejected = [0] # source had hits, but none passed the quality profile total = len(todo) lock = threading.Lock() @@ -224,25 +226,45 @@ def auto_video_process_wishlist( if media_type == 'episode': name = "%s S%02dE%02d" % (name, int(it.get('season_number') or 0), int(it.get('episode_number') or 0)) + # distinguish "source returned nothing" from "returned hits but all rejected" + # (the rejected case carries the reason on the top-ranked hit) so the log is useful. + if ok: + msg, lt = "Grabbed '%s'" % name, 'success' + elif not cands: + msg, lt = "No search results for '%s'" % name, 'info' + else: + why = (cands[0].get('rejected') or 'none met your quality profile') + msg, lt = "%d result(s) for '%s', none accepted — %s" % (len(cands), name, why), 'info' with lock: searched[0] += 1 if ok: grabbed[0] += 1 + elif not cands: + noresults[0] += 1 + else: + rejected[0] += 1 deps.update_progress( automation_id, phase='Searching + grabbing…', progress=10 + int(85 * searched[0] / max(total, 1)), - log_line=("Grabbed '%s'" % name) if ok else ("No acceptable release for '%s'" % name), - log_type='success' if ok else 'info') + log_line=msg, log_type=lt) with ThreadPoolExecutor(max_workers=concurrency) as ex: list(ex.map(_one, todo)) - done = ('Grabbed %d %s(s) of %d searched' % (grabbed[0], label, searched[0])) if grabbed[0] \ - else ('Searched %d %s(s) — no acceptable releases found' % (searched[0], label)) + # Headline with the WHY breakdown: it's the difference between "the source has + # nothing" (noresults) and "it has stuff but your quality profile rejects it" (rejected). + tail = [] + if noresults[0]: + tail.append('%d had no results' % noresults[0]) + if rejected[0]: + tail.append('%d rejected on quality' % rejected[0]) + breakdown = (' · ' + ', '.join(tail)) if tail else '' + done = ('Grabbed %d %s(s) of %d searched%s' % (grabbed[0], label, searched[0], breakdown)) if grabbed[0] \ + else ('Searched %d %s(s), grabbed 0%s' % (searched[0], label, breakdown)) deps.update_progress(automation_id, status='finished', progress=100, phase='Complete', log_line=done, log_type='success' if grabbed[0] else 'info') return {'status': 'completed', 'searched': searched[0], 'grabbed': grabbed[0], - '_manages_own_progress': True} + 'noresults': noresults[0], 'rejected': rejected[0], '_manages_own_progress': True} except Exception as e: # noqa: BLE001 deps.update_progress(automation_id, status='error', phase='Error', log_line=str(e), log_type='error') return {'status': 'error', 'error': str(e), '_manages_own_progress': True} diff --git a/tests/test_video_process_wishlist.py b/tests/test_video_process_wishlist.py index ba07748d..a08ad054 100644 --- a/tests/test_video_process_wishlist.py +++ b/tests/test_video_process_wishlist.py @@ -123,6 +123,19 @@ def test_no_acceptable_release_grabs_nothing(): assert res["searched"] == 1 and res["grabbed"] == 0 and enq == [] +def test_breakdown_distinguishes_no_results_from_quality_rejection(): + # the diagnostic: "the source had nothing" vs "had hits but quality rejected them" + items = [{"tmdb_id": 1, "title": "A"}, {"tmdb_id": 2, "title": "B"}] + res, enq, _, deps = _run(items, searches={ + ("movie", "1"): [], # source had nothing + ("movie", "2"): [dict(_cand("junk", accepted=False), rejected="Unknown / unsupported quality")], + }) + assert res["grabbed"] == 0 and res["noresults"] == 1 and res["rejected"] == 1 + logs = " ".join(p.get("log_line") or "" for p in deps.progress) + assert "No search results for 'A'" in logs + assert "none accepted — Unknown / unsupported quality" in logs # reason surfaced + + def test_missing_library_folder_is_a_quiet_skip(): res, enq, _, deps = _run([{"tmdb_id": 1, "title": "A"}], root="") assert res["status"] == "completed" and res.get("skipped") == "no_folder"