This commit is contained in:
Broque Thomas 2025-07-24 18:17:50 -07:00
parent 8e0e77f4d2
commit 1af9e7d3d3
2 changed files with 45 additions and 38 deletions

View file

@ -250,8 +250,8 @@ class PlexClient:
def search_tracks(self, title: str, artist: str, limit: int = 15) -> List[PlexTrackInfo]: 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 Searches for tracks using an efficient, multi-stage "early exit" strategy.
the best possible candidates for matching. It stops and returns results as soon as candidates are found.
""" """
if not self.music_library: if not self.music_library:
logger.warning("Plex music library not found. Cannot perform search.") logger.warning("Plex music library not found. Cannot perform search.")
@ -259,7 +259,6 @@ class PlexClient:
try: try:
candidate_tracks = [] candidate_tracks = []
# Use a set to keep track of found track keys to avoid duplicates
found_track_keys = set() found_track_keys = set()
def add_candidates(tracks): def add_candidates(tracks):
@ -277,25 +276,31 @@ class PlexClient:
plex_artist = artist_results[0] plex_artist = artist_results[0]
all_artist_tracks = plex_artist.tracks() all_artist_tracks = plex_artist.tracks()
lower_title = title.lower() 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()] stage1_results = [track for track in all_artist_tracks if lower_title in track.title.lower()]
add_candidates(stage1_results) add_candidates(stage1_results)
logger.debug(f"Stage 1 found {len(stage1_results)} candidates.") 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) --- # --- 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() search_query = f"{artist} {title}".strip()
logger.debug(f"Stage 2: Performing keyword search for '{search_query}'") logger.debug(f"Stage 2: Performing keyword search for '{search_query}'")
stage2_results = self.music_library.search(title=search_query, libtype='track', limit=limit) stage2_results = self.music_library.search(title=search_query, libtype='track', limit=limit)
add_candidates(stage2_results) 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 --- # --- 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}'") logger.debug(f"Stage 3: Performing title-only search for '{title}'")
stage3_results = self.music_library.searchTracks(title=title, limit=limit) stage3_results = self.music_library.searchTracks(title=title, limit=limit)
add_candidates(stage3_results) 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]] tracks = [PlexTrackInfo.from_plex_track(track) for track in candidate_tracks[:limit]]
if tracks: if tracks:
@ -309,6 +314,9 @@ class PlexClient:
traceback.print_exc() traceback.print_exc()
return [] return []
def get_library_stats(self) -> Dict[str, int]: def get_library_stats(self) -> Dict[str, int]:
if not self.music_library: if not self.music_library:
return {} return {}

View file

@ -120,16 +120,14 @@ class PlaylistTrackAnalysisWorker(QRunnable):
def _check_track_in_plex(self, spotify_track): def _check_track_in_plex(self, spotify_track):
""" """
Check if a Spotify track exists in Plex by trying a multi-stage search strategy: Check if a Spotify track exists in Plex by searching for each artist and
1. Search using Artist + Title variations. stopping as soon as a confident match is found.
2. Fallback to searching with only the Title variations across the entire library.
""" """
try: try:
original_title = spotify_track.name original_title = spotify_track.name
# --- Generate a list of title variations --- # --- Generate a list of title variations ---
title_variations = [] title_variations = [original_title]
title_variations.append(original_title)
if " - " in original_title: if " - " in original_title:
title_variations.append(original_title.split(' - ')[0].strip()) title_variations.append(original_title.split(' - ')[0].strip())
@ -143,11 +141,10 @@ class PlaylistTrackAnalysisWorker(QRunnable):
unique_title_variations = list(dict.fromkeys(title_variations)) unique_title_variations = list(dict.fromkeys(title_variations))
# --- Execute searches and collect all potential matches ---
all_potential_matches = [] all_potential_matches = []
found_match_ids = set() 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 [""] artists_to_search = spotify_track.artists if spotify_track.artists else [""]
for artist_name in artists_to_search: for artist_name in artists_to_search:
if self._cancelled: return None, 0.0 if self._cancelled: return None, 0.0
@ -166,35 +163,38 @@ class PlaylistTrackAnalysisWorker(QRunnable):
all_potential_matches.append(track) all_potential_matches.append(track)
found_match_ids.add(track.id) found_match_ids.add(track.id)
# STAGE 2: Fallback search with Title Only (as suggested) # --- Early Exit Check ---
print(f"🎤 Performing title-only fallback search for '{original_title}'") # After searching for an artist, check if we have a confident match.
for query_title in unique_title_variations: if all_potential_matches:
if self._cancelled: return None, 0.0 match_result = self.matching_engine.find_best_match(spotify_track, all_potential_matches)
# Pass an empty artist to trigger title-only search logic in plex_client if match_result.is_match:
title_only_matches = self.plex_client.search_tracks( print(f"✔️ Confident match found early for '{original_title}'. Stopping search.")
title=query_title, return match_result.plex_track, match_result.confidence
artist="",
limit=10 # --- Final Fallback: Title-only search if no artist-based match was found ---
) if not all_potential_matches:
for track in title_only_matches: print(f"🎤 No artist-based matches found. Performing final title-only fallback for '{original_title}'")
if track.id not in found_match_ids: for query_title in unique_title_variations:
all_potential_matches.append(track) title_only_matches = self.plex_client.search_tracks(title=query_title, artist="", limit=10)
found_match_ids.add(track.id) 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: 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 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...") 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: if final_match_result.is_match:
print(f"✔️ Best match for '{original_title}': '{match_result.plex_track.title}' with confidence {match_result.confidence:.2f}") print(f"✔️ Best match for '{original_title}': '{final_match_result.plex_track.title}' with confidence {final_match_result.confidence:.2f}")
else: 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: except Exception as e:
import traceback import traceback
@ -203,7 +203,6 @@ class PlaylistTrackAnalysisWorker(QRunnable):
return None, 0.0 return None, 0.0
class TrackDownloadWorkerSignals(QObject): class TrackDownloadWorkerSignals(QObject):
"""Signals for track download worker""" """Signals for track download worker"""
download_started = pyqtSignal(int, int, str) # download_index, track_index, download_id download_started = pyqtSignal(int, int, str) # download_index, track_index, download_id