pull/2/head
Broque Thomas 10 months ago
parent 4405e33aef
commit 39ca1e369a

@ -117,9 +117,10 @@ class MusicMatchingEngine:
artist_score = self.similarity_score(spotify_artist, plex_artist)
album_score = self.similarity_score(spotify_album, plex_album)
# CORRECTED: Plex duration is already in milliseconds.
duration_score = self.duration_similarity(
spotify_track.duration_ms,
plex_track.duration * 1000 if plex_track.duration else 0
plex_track.duration if plex_track.duration else 0
)
if title_score >= 0.9 and artist_score >= 0.9 and album_score >= 0.8:
@ -220,4 +221,4 @@ class MusicMatchingEngine:
return f"{main_artist} {clean_title}"
matching_engine = MusicMatchingEngine()
matching_engine = MusicMatchingEngine()

@ -24,11 +24,22 @@ class PlexTrackInfo:
@classmethod
def from_plex_track(cls, track: PlexTrack) -> 'PlexTrackInfo':
# Gracefully handle tracks that might be missing artist or album metadata in Plex
try:
artist_title = track.artist().title if track.artist() else "Unknown Artist"
except (NotFound, AttributeError):
artist_title = "Unknown Artist"
try:
album_title = track.album().title if track.album() else "Unknown Album"
except (NotFound, AttributeError):
album_title = "Unknown Album"
return cls(
id=str(track.ratingKey),
title=track.title,
artist=track.artist().title if track.artist() else "Unknown Artist",
album=track.album().title if track.album() else "Unknown Album",
artist=artist_title,
album=album_title,
duration=track.duration,
track_number=track.trackNumber,
year=track.year,
@ -92,8 +103,8 @@ class PlexClient:
try:
if config.get('token'):
# Use shorter timeout (5 seconds) to prevent app freezing
self.server = PlexServer(config['base_url'], config['token'], timeout=5)
# Use a longer timeout (15 seconds) to prevent read timeouts on slow servers
self.server = PlexServer(config['base_url'], config['token'], timeout=15)
else:
logger.error("Plex token not configured")
return
@ -237,24 +248,50 @@ class PlexClient:
logger.error(f"Error searching for track '{title}' by '{artist}': {e}")
return None
def search_tracks(self, query: str, limit: int = 20) -> List[PlexTrackInfo]:
def search_tracks(self, title: str, artist: str, limit: int = 10) -> List[PlexTrackInfo]:
"""
Searches for tracks in the Plex music library using a more robust, two-step method
that is more compatible with different Plex server versions.
"""
if not self.music_library:
return []
try:
results = self.music_library.search(query, limit=limit)
tracks = []
# Step 1: Search for the artist first. This is generally reliable.
artist_results = self.music_library.searchArtists(title=artist, limit=1)
for result in results:
if isinstance(result, PlexTrack):
tracks.append(PlexTrackInfo.from_plex_track(result))
results = []
if artist_results:
# If artist is found, get all their tracks and filter by title in Python.
# This avoids the API filter issue entirely.
plex_artist = artist_results[0]
all_artist_tracks = plex_artist.tracks()
# Use a case-insensitive substring match to find potential tracks.
# The matching engine will do the final, more precise comparison.
lower_title = title.lower()
for track in all_artist_tracks:
if lower_title in track.title.lower():
results.append(track)
else:
# Fallback: If the artist wasn't found, search for the track title
# across the entire library. This is less precise but better than nothing.
logger.debug(f"Artist '{artist}' not found. Falling back to title search for '{title}'.")
results = self.music_library.searchTracks(title=title, limit=limit)
tracks = []
for result in results[:limit]: # Apply limit after filtering
tracks.append(PlexTrackInfo.from_plex_track(result))
if tracks:
logger.debug(f"Plex search for '{title}' by '{artist}' found {len(tracks)} potential matches.")
return tracks
except Exception as e:
logger.error(f"Error searching tracks: {e}")
logger.error(f"Error searching Plex tracks for '{title}': {e}")
return []
def get_library_stats(self) -> Dict[str, int]:
if not self.music_library:
return {}
@ -295,4 +332,4 @@ class PlexClient:
except Exception as e:
logger.error(f"Error updating track metadata: {e}")
return False
return False

@ -9,6 +9,7 @@ from dataclasses import dataclass
from typing import List, Optional
from core.soulseek_client import TrackResult
import re
from core.matching_engine import MusicMatchingEngine
def clean_track_name_for_search(track_name):
"""
@ -59,6 +60,8 @@ class PlaylistTrackAnalysisWorker(QRunnable):
self.plex_client = plex_client
self.signals = PlaylistTrackAnalysisWorkerSignals()
self._cancelled = False
# Instantiate the matching engine once per worker for efficiency
self.matching_engine = MusicMatchingEngine()
def cancel(self):
"""Cancel the analysis operation"""
@ -95,7 +98,8 @@ class PlaylistTrackAnalysisWorker(QRunnable):
# Check if track exists in Plex
try:
plex_match, confidence = self._check_track_in_plex(track)
if plex_match and confidence >= 0.8: # High confidence threshold
# Use the 0.8 confidence threshold
if plex_match and confidence >= 0.8:
result.exists_in_plex = True
result.plex_match = plex_match
result.confidence = confidence
@ -110,77 +114,43 @@ class PlaylistTrackAnalysisWorker(QRunnable):
except Exception as e:
if not self._cancelled:
import traceback
traceback.print_exc()
self.signals.analysis_failed.emit(str(e))
def _check_track_in_plex(self, spotify_track):
"""Check if a Spotify track exists in Plex with confidence scoring"""
"""
Check if a Spotify track exists in Plex by searching Plex and using the
MusicMatchingEngine to find the best match.
"""
try:
# Search Plex for similar tracks
# Use first artist for search
# Use the first artist for the primary search query
artist_name = spotify_track.artists[0] if spotify_track.artists else ""
search_query = f"{artist_name} {spotify_track.name}".strip()
# Get potential matches from Plex
plex_tracks = self.plex_client.search_tracks(search_query, limit=10)
# Use the cleaned track name to get a broader set of potential matches from Plex
search_title = clean_track_name_for_search(spotify_track.name)
if not plex_tracks:
return None, 0.0
# Call the updated search_tracks with separate artist and title
potential_plex_matches = self.plex_client.search_tracks(
title=search_title,
artist=artist_name,
limit=10
)
# Find best match using confidence scoring
best_match = None
best_confidence = 0.0
if not potential_plex_matches:
return None, 0.0
for plex_track in plex_tracks:
confidence = self._calculate_track_confidence(spotify_track, plex_track)
if confidence > best_confidence:
best_confidence = confidence
best_match = plex_track
# Use the matching engine to find the best match among the candidates.
match_result = self.matching_engine.find_best_match(spotify_track, potential_plex_matches)
return best_match, best_confidence
# Return the best Plex track found and its confidence score.
return match_result.plex_track, match_result.confidence
except Exception as e:
import traceback
print(f"Error checking track in Plex: {e}")
traceback.print_exc()
return None, 0.0
def _calculate_track_confidence(self, spotify_track, plex_track):
"""Calculate confidence score between Spotify and Plex tracks"""
try:
# Basic string similarity for now (can be enhanced with existing matching engine)
import re
def normalize_string(s):
return re.sub(r'[^a-zA-Z0-9\s]', '', s.lower()).strip()
# Normalize track titles
spotify_title = normalize_string(spotify_track.name)
plex_title = normalize_string(plex_track.title)
# Normalize artist names
spotify_artist = normalize_string(spotify_track.artists[0]) if spotify_track.artists else ""
plex_artist = normalize_string(plex_track.artist)
# Simple similarity scoring
title_similarity = 1.0 if spotify_title == plex_title else 0.0
artist_similarity = 1.0 if spotify_artist == plex_artist else 0.0
# Weight title more heavily
confidence = (title_similarity * 0.7) + (artist_similarity * 0.3)
# Duration check (allow 10% variance)
if hasattr(spotify_track, 'duration_ms') and hasattr(plex_track, 'duration'):
spotify_duration = spotify_track.duration_ms / 1000
plex_duration = plex_track.duration / 1000 if plex_track.duration else 0
if plex_duration > 0:
duration_diff = abs(spotify_duration - plex_duration) / max(spotify_duration, plex_duration)
if duration_diff <= 0.1: # Within 10%
confidence += 0.1 # Bonus for duration match
return min(confidence, 1.0) # Cap at 1.0
except Exception as e:
print(f"Error calculating track confidence: {e}")
return 0.0
class TrackDownloadWorkerSignals(QObject):
"""Signals for track download worker"""

Loading…
Cancel
Save