good
This commit is contained in:
parent
4405e33aef
commit
39ca1e369a
3 changed files with 82 additions and 74 deletions
|
|
@ -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:
|
||||
# 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
|
||||
)
|
||||
|
||||
if not potential_plex_matches:
|
||||
return None, 0.0
|
||||
|
||||
# Find best match using confidence scoring
|
||||
best_match = None
|
||||
best_confidence = 0.0
|
||||
# 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)
|
||||
|
||||
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
|
||||
|
||||
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…
Reference in a new issue