From 1af9e7d3d316eca8e235147347633ddcac45897e Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Thu, 24 Jul 2025 18:17:50 -0700 Subject: [PATCH] good --- core/plex_client.py | 24 ++++++++++++------ ui/pages/sync.py | 59 ++++++++++++++++++++++----------------------- 2 files changed, 45 insertions(+), 38 deletions(-) diff --git a/core/plex_client.py b/core/plex_client.py index d3430f92..ee852a10 100644 --- a/core/plex_client.py +++ b/core/plex_client.py @@ -250,8 +250,8 @@ class PlexClient: def search_tracks(self, title: str, artist: str, limit: int = 15) -> List[PlexTrackInfo]: """ - Searches for tracks using a robust, multi-stage fallback strategy to find - the best possible candidates for matching. + Searches for tracks using an efficient, multi-stage "early exit" strategy. + It stops and returns results as soon as candidates are found. """ if not self.music_library: logger.warning("Plex music library not found. Cannot perform search.") @@ -259,7 +259,6 @@ class PlexClient: try: candidate_tracks = [] - # Use a set to keep track of found track keys to avoid duplicates found_track_keys = set() def add_candidates(tracks): @@ -277,25 +276,31 @@ class PlexClient: plex_artist = artist_results[0] all_artist_tracks = plex_artist.tracks() lower_title = title.lower() - # Find all tracks by this artist that contain the title text stage1_results = [track for track in all_artist_tracks if lower_title in track.title.lower()] add_candidates(stage1_results) logger.debug(f"Stage 1 found {len(stage1_results)} candidates.") + + # --- Early Exit: If Stage 1 found results, stop here --- + if candidate_tracks: + logger.info(f"Found {len(candidate_tracks)} candidates in Stage 1. Exiting early.") + return [PlexTrackInfo.from_plex_track(track) for track in candidate_tracks[:limit]] # --- Stage 2: Flexible Keyword Search (Artist + Title combined) --- - # This is good for artist variations like "The Dare" vs "Dare" search_query = f"{artist} {title}".strip() logger.debug(f"Stage 2: Performing keyword search for '{search_query}'") stage2_results = self.music_library.search(title=search_query, libtype='track', limit=limit) add_candidates(stage2_results) - + + # --- Early Exit: If Stage 2 found results, stop here --- + if candidate_tracks: + logger.info(f"Found {len(candidate_tracks)} candidates in Stage 2. Exiting early.") + return [PlexTrackInfo.from_plex_track(track) for track in candidate_tracks[:limit]] + # --- Stage 3: Title-Only Fallback Search --- - # This is the broadest search for cases where artist metadata is wrong or missing. logger.debug(f"Stage 3: Performing title-only search for '{title}'") stage3_results = self.music_library.searchTracks(title=title, limit=limit) add_candidates(stage3_results) - # Convert the raw Plex track objects to our simplified PlexTrackInfo dataclass tracks = [PlexTrackInfo.from_plex_track(track) for track in candidate_tracks[:limit]] if tracks: @@ -309,6 +314,9 @@ class PlexClient: traceback.print_exc() return [] + + + def get_library_stats(self) -> Dict[str, int]: if not self.music_library: return {} diff --git a/ui/pages/sync.py b/ui/pages/sync.py index 9d3bdeb9..a4dedb23 100644 --- a/ui/pages/sync.py +++ b/ui/pages/sync.py @@ -120,16 +120,14 @@ class PlaylistTrackAnalysisWorker(QRunnable): def _check_track_in_plex(self, spotify_track): """ - Check if a Spotify track exists in Plex by trying a multi-stage search strategy: - 1. Search using Artist + Title variations. - 2. Fallback to searching with only the Title variations across the entire library. + Check if a Spotify track exists in Plex by searching for each artist and + stopping as soon as a confident match is found. """ try: original_title = spotify_track.name # --- Generate a list of title variations --- - title_variations = [] - title_variations.append(original_title) + title_variations = [original_title] if " - " in original_title: title_variations.append(original_title.split(' - ')[0].strip()) @@ -143,11 +141,10 @@ class PlaylistTrackAnalysisWorker(QRunnable): unique_title_variations = list(dict.fromkeys(title_variations)) - # --- Execute searches and collect all potential matches --- all_potential_matches = [] found_match_ids = set() - - # STAGE 1: Search with Artist + Title + + # --- Search for each artist, but exit early if a good match is found --- artists_to_search = spotify_track.artists if spotify_track.artists else [""] for artist_name in artists_to_search: if self._cancelled: return None, 0.0 @@ -165,36 +162,39 @@ class PlaylistTrackAnalysisWorker(QRunnable): if track.id not in found_match_ids: all_potential_matches.append(track) found_match_ids.add(track.id) + + # --- Early Exit Check --- + # After searching for an artist, check if we have a confident match. + if all_potential_matches: + match_result = self.matching_engine.find_best_match(spotify_track, all_potential_matches) + if match_result.is_match: + print(f"✔️ Confident match found early for '{original_title}'. Stopping search.") + return match_result.plex_track, match_result.confidence - # STAGE 2: Fallback search with Title Only (as suggested) - print(f"🎤 Performing title-only fallback search for '{original_title}'") - for query_title in unique_title_variations: - if self._cancelled: return None, 0.0 - # Pass an empty artist to trigger title-only search logic in plex_client - title_only_matches = self.plex_client.search_tracks( - title=query_title, - artist="", - limit=10 - ) - for track in title_only_matches: - if track.id not in found_match_ids: - all_potential_matches.append(track) - found_match_ids.add(track.id) + # --- Final Fallback: Title-only search if no artist-based match was found --- + if not all_potential_matches: + print(f"🎤 No artist-based matches found. Performing final title-only fallback for '{original_title}'") + for query_title in unique_title_variations: + title_only_matches = self.plex_client.search_tracks(title=query_title, artist="", limit=10) + for track in title_only_matches: + if track.id not in found_match_ids: + all_potential_matches.append(track) + found_match_ids.add(track.id) if not all_potential_matches: - print(f"❌ No Plex candidates found for '{original_title}' after all search strategies.") + print(f"❌ No Plex candidates found for '{original_title}' after all strategies.") return None, 0.0 - # --- Use the matching engine to find the best match among ALL candidates --- + # --- Final Scoring --- print(f"✅ Found {len(all_potential_matches)} total potential Plex matches for '{original_title}'. Scoring now...") - match_result = self.matching_engine.find_best_match(spotify_track, all_potential_matches) + final_match_result = self.matching_engine.find_best_match(spotify_track, all_potential_matches) - if match_result.is_match: - print(f"✔️ Best match for '{original_title}': '{match_result.plex_track.title}' with confidence {match_result.confidence:.2f}") + if final_match_result.is_match: + print(f"✔️ Best match for '{original_title}': '{final_match_result.plex_track.title}' with confidence {final_match_result.confidence:.2f}") else: - print(f"⚠️ No confident match found for '{original_title}'. Best attempt scored {match_result.confidence:.2f}.") + print(f"⚠️ No confident match found for '{original_title}'. Best attempt scored {final_match_result.confidence:.2f}.") - return match_result.plex_track, match_result.confidence + return final_match_result.plex_track, final_match_result.confidence except Exception as e: import traceback @@ -203,7 +203,6 @@ class PlaylistTrackAnalysisWorker(QRunnable): return None, 0.0 - class TrackDownloadWorkerSignals(QObject): """Signals for track download worker""" download_started = pyqtSignal(int, int, str) # download_index, track_index, download_id