diff --git a/core/downloads/post_processing.py b/core/downloads/post_processing.py index 2c17e90f..5dbbae97 100644 --- a/core/downloads/post_processing.py +++ b/core/downloads/post_processing.py @@ -171,6 +171,21 @@ def run_post_processing_worker(task_id: str, batch_id: str, deps: PostProcessDep logger.info(f"[Post-Processing] Task {task_id} already completed by stream processor, skipping verification") return + # RACE GUARD: the monitor sets status -> 'post_processing' immediately + # before submitting this worker. If the status is now anything else, the + # browser-poll post-processor already took ownership of this task — e.g. + # it quarantined the file and requeued the next-best candidate (status + # -> 'searching', source identity cleared). Bail WITHOUT marking failed + # or notifying batch completion: otherwise we clobber that in-flight + # retry with a bogus "missing file or source information" failure while a + # parallel attempt is importing the song. + if task['status'] != 'post_processing': + logger.info( + f"[Post-Processing] Task {task_id} no longer in 'post_processing' " + f"(now '{task['status']}') — another path took over, skipping" + ) + return + # Extract file information for verification track_info = task.get('track_info', {}) task_filename = task.get('filename') or track_info.get('filename') diff --git a/core/downloads/task_worker.py b/core/downloads/task_worker.py index 1b7b2bc1..f615de47 100644 --- a/core/downloads/task_worker.py +++ b/core/downloads/task_worker.py @@ -54,21 +54,34 @@ def _cand_user_file(candidate): return getattr(candidate, 'username', None), getattr(candidate, 'filename', None) -def _best_quality_ordering(): - """Return ``(quality_first, targets)`` for the active search mode. +def _candidate_ordering(): + """Return ``(quality_first, targets)`` for the active search mode + toggle. - In best-quality mode the candidate walk is ordered by the user's profile - quality rank (best→worst) instead of confidence-first. Fails closed to - priority-mode ordering on any error so a profile/DB hiccup never blocks a - download. See docs/superpowers/specs/2026-06-14-best-quality-search-mode-design.md. + The candidate walk is ordered by the user's profile quality rank + (best→worst) instead of confidence-first when EITHER: + - best-quality search mode is active (always quality-first), OR + - priority mode and the ``rank_candidates_by_quality`` toggle is on + (opt-in; default off keeps the byte-for-byte confidence-first walk). + + Quality-first ordering also makes the version-mismatch force-import pick + the highest-quality candidate, because that fallback accepts the + first-tried (= best-ordered) quarantined entry. + + Fails closed to confidence-first ordering on any error so a profile/DB + hiccup never blocks a download. See + docs/superpowers/specs/2026-06-14-best-quality-search-mode-design.md. """ try: - from core.quality.selection import load_search_mode, load_profile_targets - if load_search_mode() == 'best_quality': + from core.quality.selection import ( + load_search_mode, + load_profile_targets, + load_rank_candidates_by_quality, + ) + if load_search_mode() == 'best_quality' or load_rank_candidates_by_quality(): targets, _ = load_profile_targets() return True, targets except Exception as exc: - logger.debug("[Modal Worker] best-quality ordering unavailable: %s", exc) + logger.debug("[Modal Worker] quality ordering unavailable: %s", exc) return False, None @@ -109,7 +122,7 @@ def _try_cached_candidates(task_id, batch_id, track, deps): f"[Modal Worker] Quarantine retry: trying {len(remaining)} cached " f"candidate(s) before re-searching (task {task_id})" ) - _qf, _qt = _best_quality_ordering() + _qf, _qt = _candidate_ordering() return deps.attempt_download_with_candidates( task_id, remaining, track, batch_id, quality_first=_qf, quality_targets=_qt, ) @@ -393,7 +406,7 @@ def download_track_worker(task_id: str, batch_id: Optional[str], deps: TaskWorke # Best-quality search mode: the orchestrator already pooled candidates # across every source for each query, so order the candidate walk by the # user's profile quality rank (best→worst). Computed once per task. - _best_quality, _quality_targets = _best_quality_ordering() + _best_quality, _quality_targets = _candidate_ordering() # 2. Sequential Query Search (matches GUI's start_search_worker_parallel logic) search_diagnostics = [] # Track what happened per query for detailed error messages diff --git a/core/imports/version_mismatch_fallback.py b/core/imports/version_mismatch_fallback.py index 3e0ef92d..2bf37388 100644 --- a/core/imports/version_mismatch_fallback.py +++ b/core/imports/version_mismatch_fallback.py @@ -103,9 +103,12 @@ def select_version_mismatch_fallback( # don't guess which the user wants. return None - # First tried = oldest = highest-confidence (the retry walks candidates - # best-first). The id is a "_