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]:
"""
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 {}

View file

@ -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