diff --git a/core/matching_engine.py b/core/matching_engine.py index b532a640..eaa21002 100644 --- a/core/matching_engine.py +++ b/core/matching_engine.py @@ -634,8 +634,20 @@ class MusicMatchingEngine: title_words = spotify_cleaned_title.split() is_short_title = len(spotify_cleaned_title) <= 5 or len(title_words) == 1 + # --- Minimum Title Gate --- + # Reject matches where the title has almost no resemblance to the target. + # Without this, artist + album bonus alone can push completely wrong tracks + # past the confidence threshold (e.g. "West End Girl" matching "Tennis" + # just because they're by the same artist on the same album). + if not is_youtube and title_score < 0.30 and not has_word_boundary: + logger.debug( + f"Title gate reject: '{spotify_track.name}' vs '{slskd_track.filename[:60]}' " + f"(title_score={title_score:.2f} < 0.30)" + ) + return 0.0 + # --- Final Weighted Score --- - + if is_youtube: # For YouTube, rely more on Title and Duration since Artist is often missing from video titles # and the search query already filtered by artist to some extent. diff --git a/web_server.py b/web_server.py index 5ebc5d58..28b864a0 100644 --- a/web_server.py +++ b/web_server.py @@ -13450,6 +13450,19 @@ def _post_process_matched_download_with_verification(context_key, context, file_ if original_batch_id: context['batch_id'] = original_batch_id + # Check if race guard detected the source file was already gone + if context.get('_race_guard_failed'): + _pp.info(f"Race guard: source file gone for task {task_id} — marking as failed") + with tasks_lock: + if task_id in download_tasks: + download_tasks[task_id]['status'] = 'failed' + download_tasks[task_id]['error_message'] = 'Source file was already processed or removed by another task' + with matched_context_lock: + if context_key in matched_downloads_context: + del matched_downloads_context[context_key] + _on_download_completed(batch_id, task_id, success=False) + return + # Check if AcoustID quarantined the file — no further processing needed if context.get('_acoustid_quarantined'): failure_msg = context.get('_acoustid_failure_msg', 'AcoustID verification failed') @@ -13827,7 +13840,8 @@ def _post_process_matched_download(context_key, context, file_path): if existing_final and os.path.exists(existing_final): print(f"✅ [Race Guard] Source gone but destination exists — already processed by another thread: {os.path.basename(existing_final)}") return - print(f"⚠️ [Race Guard] Source file gone and no known destination — likely already processed: {os.path.basename(file_path)}") + print(f"⚠️ [Race Guard] Source file gone and no known destination — marking as failed: {os.path.basename(file_path)}") + context['_race_guard_failed'] = True return # --- END RACE CONDITION GUARD ---