good
This commit is contained in:
parent
77383f0178
commit
55c36241dd
6 changed files with 4985 additions and 5 deletions
|
|
@ -1 +1 @@
|
||||||
{"access_token": "BQBvbg3WwYrukruP6c8SrF_hNNq8RpE81IiDnj1pJB6smBlF5SVT7VO4Zd2jdurQtwk41A-6U8fBORnLO4_hfxYkMBfxnL4ifzsO9gQfvQLUR585hq4iL03Nrh30LhO8h9wnhWwSWDNPd9W-RakrqdfY8p23ZDMNAaGqGp0ZsvbkiKIDBMMP9Y91tye7QSSJxYRSlBr64yiJoWu2Ybwe-OA_xD9OqxZxSpTNgEKNyOuozqQ-TUWJNPSe7n_cSrPs", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1753462488, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}
|
{"access_token": "BQDlZYRG1PkTF9MyEIWlv6Stz7KVj9ljTaG0vqjb1mIQGx6C7j2Gs79hjulYLuHv_FZ7w_jr0J4GC49bSGCYjV3o30sx58NqnlHMoEcbHmQGmFO6PIvZslmm5WEUXFhCtyW3AIoTCL_MDgxGty3fHstHxqJ8yWWsbgu8DaLxcgtju4_YIv53LlnbkCsGzdoHt4y6ExoFnOcTjU5ZguhdyYULkgEXVd2mvW4yMZE-SRpEaHGcFgu_V_lqTirm7ZHp", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1753469090, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}
|
||||||
Binary file not shown.
|
|
@ -6,6 +6,8 @@ from unidecode import unidecode
|
||||||
from utils.logging_config import get_logger
|
from utils.logging_config import get_logger
|
||||||
from core.spotify_client import Track as SpotifyTrack
|
from core.spotify_client import Track as SpotifyTrack
|
||||||
from core.plex_client import PlexTrackInfo
|
from core.plex_client import PlexTrackInfo
|
||||||
|
from core.soulseek_client import TrackResult
|
||||||
|
|
||||||
|
|
||||||
logger = get_logger("matching_engine")
|
logger = get_logger("matching_engine")
|
||||||
|
|
||||||
|
|
@ -181,3 +183,80 @@ class MusicMatchingEngine:
|
||||||
else:
|
else:
|
||||||
# Fallback to just track name if no artist
|
# Fallback to just track name if no artist
|
||||||
return self.clean_title(spotify_track.name)
|
return self.clean_title(spotify_track.name)
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_slskd_match_confidence(self, spotify_track: SpotifyTrack, slskd_track: TrackResult) -> float:
|
||||||
|
"""
|
||||||
|
Calculates a confidence score for a Soulseek track against a Spotify track.
|
||||||
|
This is the core of the new matching logic.
|
||||||
|
"""
|
||||||
|
# Normalize the Spotify track info once for efficiency
|
||||||
|
spotify_title_norm = self.normalize_string(spotify_track.name)
|
||||||
|
spotify_artists_norm = [self.normalize_string(a) for a in spotify_track.artists]
|
||||||
|
|
||||||
|
# The slskd filename is our primary source of truth, so normalize it
|
||||||
|
slskd_filename_norm = self.normalize_string(slskd_track.filename)
|
||||||
|
|
||||||
|
# 1. Title Score: How well does the Spotify title appear in the filename?
|
||||||
|
# We use the cleaned, core title for a strict check. This avoids matching remixes.
|
||||||
|
spotify_cleaned_title = self.clean_title(spotify_track.name)
|
||||||
|
title_score = 0.0
|
||||||
|
if spotify_cleaned_title in slskd_filename_norm:
|
||||||
|
title_score = 0.9 # High score for direct inclusion
|
||||||
|
# Bonus for being a standalone word/phrase, penalizing partial matches like 'in' in 'finland'
|
||||||
|
if re.search(r'\b' + re.escape(spotify_cleaned_title) + r'\b', slskd_filename_norm):
|
||||||
|
title_score = 1.0
|
||||||
|
|
||||||
|
# 2. Artist Score: How well do the Spotify artists appear in the filename?
|
||||||
|
artist_score = 0.0
|
||||||
|
for artist in spotify_artists_norm:
|
||||||
|
if artist in slskd_filename_norm:
|
||||||
|
artist_score = 1.0 # Perfect match if any artist is found
|
||||||
|
break
|
||||||
|
|
||||||
|
# 3. Duration Score: How similar are the track lengths?
|
||||||
|
# We give this a lower weight as slskd duration data can be unreliable.
|
||||||
|
duration_score = self.duration_similarity(spotify_track.duration_ms, slskd_track.duration if slskd_track.duration else 0)
|
||||||
|
|
||||||
|
# 4. Quality Bonus: Add a small bonus for higher quality formats
|
||||||
|
quality_bonus = 0.0
|
||||||
|
if slskd_track.quality:
|
||||||
|
if slskd_track.quality.lower() == 'flac':
|
||||||
|
quality_bonus = 0.1
|
||||||
|
elif slskd_track.quality.lower() == 'mp3' and (slskd_track.bitrate or 0) >= 320:
|
||||||
|
quality_bonus = 0.05
|
||||||
|
|
||||||
|
# --- Final Weighted Score ---
|
||||||
|
# Title and Artist are the most important factors for an accurate match.
|
||||||
|
final_confidence = (title_score * 0.60) + (artist_score * 0.35) + (duration_score * 0.05)
|
||||||
|
|
||||||
|
# Add the quality bonus to the final score
|
||||||
|
final_confidence += quality_bonus
|
||||||
|
|
||||||
|
# Ensure the final score doesn't exceed 1.0
|
||||||
|
return min(final_confidence, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def find_best_slskd_matches(self, spotify_track: SpotifyTrack, slskd_results: List[TrackResult]) -> List[TrackResult]:
|
||||||
|
"""
|
||||||
|
Scores and sorts a list of Soulseek results against a Spotify track.
|
||||||
|
Returns the list of candidates sorted from best to worst match.
|
||||||
|
"""
|
||||||
|
if not slskd_results:
|
||||||
|
return []
|
||||||
|
|
||||||
|
scored_results = []
|
||||||
|
for slskd_track in slskd_results:
|
||||||
|
confidence = self.calculate_slskd_match_confidence(spotify_track, slskd_track)
|
||||||
|
# We temporarily store the confidence score on the object itself for sorting
|
||||||
|
slskd_track.confidence = confidence
|
||||||
|
scored_results.append(slskd_track)
|
||||||
|
|
||||||
|
# Sort by confidence score (descending), and then by size as a tie-breaker
|
||||||
|
sorted_results = sorted(scored_results, key=lambda r: (r.confidence, r.size), reverse=True)
|
||||||
|
|
||||||
|
# Filter out very low-confidence results to avoid bad matches.
|
||||||
|
# A threshold of 0.6 means the title and artist had to have some reasonable similarity.
|
||||||
|
confident_results = [r for r in sorted_results if r.confidence > 0.6]
|
||||||
|
|
||||||
|
return confident_results
|
||||||
4885
logs/app.log
4885
logs/app.log
File diff suppressed because it is too large
Load diff
Binary file not shown.
|
|
@ -3443,7 +3443,7 @@ class DownloadMissingTracksModal(QDialog):
|
||||||
self.playlist_item = playlist_item
|
self.playlist_item = playlist_item
|
||||||
self.parent_page = parent_page
|
self.parent_page = parent_page
|
||||||
self.downloads_page = downloads_page
|
self.downloads_page = downloads_page
|
||||||
|
self.matching_engine = MusicMatchingEngine()
|
||||||
# State tracking
|
# State tracking
|
||||||
self.total_tracks = len(playlist.tracks)
|
self.total_tracks = len(playlist.tracks)
|
||||||
self.matched_tracks_count = 0
|
self.matched_tracks_count = 0
|
||||||
|
|
@ -4374,9 +4374,25 @@ class DownloadMissingTracksModal(QDialog):
|
||||||
if loop: loop.close()
|
if loop: loop.close()
|
||||||
|
|
||||||
def get_valid_candidates(self, results, spotify_track, query):
|
def get_valid_candidates(self, results, spotify_track, query):
|
||||||
"""Get all valid candidates sorted by score (for retry mechanism)"""
|
"""
|
||||||
# This is a simplified version for brevity. The full scoring logic should be here.
|
Scores and filters search results using the MusicMatchingEngine to find the best candidates.
|
||||||
return sorted(results, key=lambda r: r.size, reverse=True)
|
This replaces the simple size-based sorting with intelligent, confidence-based scoring.
|
||||||
|
"""
|
||||||
|
if not results:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Use the new matching engine function to score, filter, and sort the results.
|
||||||
|
# This returns a list of SlskdTrack objects with a 'confidence' attribute,
|
||||||
|
# already sorted from best to worst and filtered by our confidence threshold.
|
||||||
|
confident_matches = self.matching_engine.find_best_slskd_matches(spotify_track, results)
|
||||||
|
|
||||||
|
if confident_matches:
|
||||||
|
best_confidence = confident_matches[0].confidence
|
||||||
|
print(f"✅ Found {len(confident_matches)} confident matches for '{spotify_track.name}'. Best score: {best_confidence:.2f} from query '{query}'")
|
||||||
|
else:
|
||||||
|
print(f"⚠️ No confident matches found for '{spotify_track.name}' from query '{query}'.")
|
||||||
|
|
||||||
|
return confident_matches
|
||||||
|
|
||||||
def create_spotify_based_search_result_from_validation(self, slskd_result, spotify_metadata):
|
def create_spotify_based_search_result_from_validation(self, slskd_result, spotify_metadata):
|
||||||
"""Create SpotifyBasedSearchResult from validation results"""
|
"""Create SpotifyBasedSearchResult from validation results"""
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue