From 9b1445b3b553b6cc1e298b173222043ffbacd45d Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 5 Jun 2026 23:38:41 +0200 Subject: [PATCH] Downloads: version-mismatch fallback also fires when retry search finds no candidates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing fallback (pipeline.py:1084) only ran inside post_process_matched_download_with_verification — i.e. when a file *was* downloaded and AcoustID retries were fully exhausted. If the retry *search* itself found zero valid candidates (source returned nothing, or all failed HiFi validation), the task was marked not_found and the fallback was never reached, even though the quarantine already held N version-mismatch entries. Fix: add try_version_mismatch_fallback to TaskWorkerDeps; in the "no valid candidates" path of task_worker, invoke it before marking not_found when is_quarantine_retry. Wired in _build_task_worker_deps via a new helper (_try_version_mismatch_fallback_for_worker) that calls try_accept_version_mismatch_fallback directly with the track's title and artist and a reprocess lambda over _post_process_matched_download_with_verification. Co-Authored-By: Claude Sonnet 4.6 --- core/downloads/task_worker.py | 10 ++++++++ web_server.py | 48 +++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/core/downloads/task_worker.py b/core/downloads/task_worker.py index 3850dd2d..1e27cbc4 100644 --- a/core/downloads/task_worker.py +++ b/core/downloads/task_worker.py @@ -151,6 +151,7 @@ class TaskWorkerDeps: attempt_download_with_candidates: Callable # (task_id, candidates, track, batch_id) -> bool on_download_completed: Callable # (batch_id, task_id, success) -> None recover_worker_slot: Callable # (batch_id, task_id) -> None + try_version_mismatch_fallback: Optional[Callable] = None # (title, artist, task_id, batch_id) -> bool def download_track_worker(task_id: str, batch_id: Optional[str], deps: TaskWorkerDeps) -> None: @@ -589,6 +590,15 @@ def download_track_worker(task_id: str, batch_id: Optional[str], deps: TaskWorke # If we get here, all search queries and hybrid fallbacks failed logger.warning(f"[Modal Worker] No valid candidates found for '{track.name}' after trying all {len(search_queries)} queries.") + + # Last-resort: quarantine retry with no new candidates — the retry search + # exhausted all sources. If the setting is enabled, accept the best + # already-quarantined candidate rather than leaving the track missing. + if is_quarantine_retry and deps.try_version_mismatch_fallback: + _fallback_artist = track.artists[0] if track.artists else '' + if deps.try_version_mismatch_fallback(track.name, _fallback_artist, task_id, batch_id): + return # fallback re-dispatched; batch completion handled by reprocess thread + with tasks_lock: if task_id in download_tasks: download_tasks[task_id]['status'] = 'not_found' diff --git a/web_server.py b/web_server.py index 24fdf547..d274e182 100644 --- a/web_server.py +++ b/web_server.py @@ -16896,6 +16896,53 @@ def _run_post_processing_worker(task_id, batch_id): from core.downloads import task_worker as _downloads_task_worker +def _try_version_mismatch_fallback_for_worker(expected_title, expected_artist, task_id, batch_id): + """Called by the download worker when a quarantine-retry search finds no + candidates. Delegates to version_mismatch_fallback so the best already- + quarantined version-mismatch candidate is accepted rather than giving up.""" + import threading + import time + from core.imports.version_mismatch_fallback import try_accept_version_mismatch_fallback + from core.imports.quarantine import approve_quarantine_entry, list_quarantine_entries + from config.settings import config_manager + from core.imports.paths import docker_resolve_path + import os + try: + download_path = docker_resolve_path( + config_manager.get('soulseek.download_path', './downloads') + ) + quarantine_dir = os.path.join(download_path, 'ss_quarantine') + restore_dir = os.path.join(download_path, 'Transfer') + + def _reprocess(restored_path, ctx, tid, bid): + new_key = f"vmfallback_{tid}_{int(time.time())}" + threading.Thread( + target=lambda: _post_process_matched_download_with_verification( + new_key, ctx, restored_path, tid, bid + ), + daemon=True, + ).start() + + return try_accept_version_mismatch_fallback( + quarantine_dir=quarantine_dir, + restore_dir=restore_dir, + expected_title=expected_title, + expected_artist=expected_artist, + task_id=task_id, + batch_id=batch_id, + config_get=config_manager.get, + list_entries=list_quarantine_entries, + approve_entry=approve_quarantine_entry, + reprocess=_reprocess, + ) + except Exception as exc: + import logging + logging.getLogger(__name__).debug( + "[Version-Mismatch Fallback] worker-path skipped due to error: %s", exc + ) + return False + + def _build_task_worker_deps(): """Build TaskWorkerDeps bundle from web_server.py globals on each call.""" return _downloads_task_worker.TaskWorkerDeps( @@ -16909,6 +16956,7 @@ def _build_task_worker_deps(): attempt_download_with_candidates=_attempt_download_with_candidates, on_download_completed=lambda b, t, success: _on_download_completed(b, t, success=success), recover_worker_slot=_recover_worker_slot, + try_version_mismatch_fallback=_try_version_mismatch_fallback_for_worker, )