|
|
|
|
@ -250,9 +250,8 @@ class PlexClient:
|
|
|
|
|
|
|
|
|
|
def search_tracks(self, title: str, artist: str, limit: int = 15) -> List[PlexTrackInfo]:
|
|
|
|
|
"""
|
|
|
|
|
Searches for tracks in the Plex music library. If an artist is provided, it
|
|
|
|
|
searches within that artist's scope. If the artist is empty or not found,
|
|
|
|
|
it falls back to a library-wide search for the title.
|
|
|
|
|
Searches for tracks using a robust, multi-stage fallback strategy to find
|
|
|
|
|
the best possible candidates for matching.
|
|
|
|
|
"""
|
|
|
|
|
if not self.music_library:
|
|
|
|
|
logger.warning("Plex music library not found. Cannot perform search.")
|
|
|
|
|
@ -260,41 +259,56 @@ class PlexClient:
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
candidate_tracks = []
|
|
|
|
|
|
|
|
|
|
# If an artist is provided, perform a targeted search.
|
|
|
|
|
# Use a set to keep track of found track keys to avoid duplicates
|
|
|
|
|
found_track_keys = set()
|
|
|
|
|
|
|
|
|
|
def add_candidates(tracks):
|
|
|
|
|
"""Helper function to add unique tracks to the main candidate list."""
|
|
|
|
|
for track in tracks:
|
|
|
|
|
if track.ratingKey not in found_track_keys:
|
|
|
|
|
candidate_tracks.append(track)
|
|
|
|
|
found_track_keys.add(track.ratingKey)
|
|
|
|
|
|
|
|
|
|
# --- Stage 1: High-Precision Search (Artist -> then filter by Title) ---
|
|
|
|
|
if artist:
|
|
|
|
|
logger.debug(f"Stage 1: Searching for artist '{artist}'")
|
|
|
|
|
artist_results = self.music_library.searchArtists(title=artist, limit=1)
|
|
|
|
|
if artist_results:
|
|
|
|
|
plex_artist = artist_results[0]
|
|
|
|
|
all_artist_tracks = plex_artist.tracks()
|
|
|
|
|
lower_title = title.lower()
|
|
|
|
|
for track in all_artist_tracks:
|
|
|
|
|
if lower_title in track.title.lower():
|
|
|
|
|
candidate_tracks.append(track)
|
|
|
|
|
else:
|
|
|
|
|
# If artist not found, still fall back to a general title search
|
|
|
|
|
logger.debug(f"Artist '{artist}' not found. Falling back to title search for '{title}'.")
|
|
|
|
|
candidate_tracks = self.music_library.searchTracks(title=title, limit=limit)
|
|
|
|
|
|
|
|
|
|
# If no artist is provided, perform a library-wide title search directly.
|
|
|
|
|
else:
|
|
|
|
|
logger.debug(f"Performing title-only search for '{title}'.")
|
|
|
|
|
candidate_tracks = self.music_library.searchTracks(title=title, limit=limit)
|
|
|
|
|
# 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.")
|
|
|
|
|
|
|
|
|
|
# --- 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)
|
|
|
|
|
|
|
|
|
|
# --- 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:
|
|
|
|
|
logger.debug(f"Plex search for title='{title}', artist='{artist or 'N/A'}' found {len(tracks)} potential matches.")
|
|
|
|
|
logger.info(f"Found {len(tracks)} total potential matches for '{title}' by '{artist}' after all stages.")
|
|
|
|
|
|
|
|
|
|
return tracks
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"Error searching Plex tracks for title='{title}', artist='{artist}': {e}")
|
|
|
|
|
logger.error(f"Error during multi-stage search for title='{title}', artist='{artist}': {e}")
|
|
|
|
|
import traceback
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_library_stats(self) -> Dict[str, int]:
|
|
|
|
|
if not self.music_library:
|
|
|
|
|
return {}
|
|
|
|
|
|