maybe better?
This commit is contained in:
parent
f3dd76feae
commit
e603e8379c
4 changed files with 51365 additions and 133 deletions
|
|
@ -1 +1 @@
|
|||
{"access_token": "BQDQihfVJT0GTVKd5lQGJ16sP88DEFNAEYdZFaU_0UG027cia_VTwdpslWZrchDt_xwdj4mNdCQXc3f-VgbMeSBfFCvWCwqXzbfhh_tEMzigzdiXfK18MvnDrgHEh8K5DOI5k2TOwt--9Asq6O2Q6TpgUqWkBXXNMDNL1Birv_n-8Eh8rlFoZaBt4uvAi6Moqo0T7zjjBbOcFkFFpvIkPYg1lwJGjcjqTCGgZxd6jSPd06taPpyCo53DTx0oA1df", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1753319703, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}
|
||||
{"access_token": "BQC0k9mKpZfqwLpS4a01JeBykWsEGs7JP0n6o_5pb0LMQbq8gZVRZ4nZCqMwM-98xz2Sx3qaUxWWOgPjpO-WbVWclPOSyNlkMNdlgKsQBr4kYjujybvC0qFAIicwnKJphRrzSvI7xHxFfE0Dht2NgQAbqaTVD3xC_Ao9QV5ElBoE__YWW1fN82ojdpJJqHyh9stJeYw2BVUMxbhsELivyRoSvndTnvyoXz11OCxCBOiAyvIsqzWNu7ZSuBnSRjfq", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1753323266, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}
|
||||
51258
logs/app.log
51258
logs/app.log
File diff suppressed because it is too large
Load diff
Binary file not shown.
238
ui/pages/sync.py
238
ui/pages/sync.py
|
|
@ -8,6 +8,31 @@ from PyQt6.QtGui import QFont
|
|||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
from core.soulseek_client import TrackResult
|
||||
import re
|
||||
|
||||
def clean_track_name_for_search(track_name):
|
||||
"""
|
||||
Cleans a track name for searching by removing text in parentheses and brackets.
|
||||
If cleaning the name results in an empty string, the original name is returned.
|
||||
"""
|
||||
if not track_name or not isinstance(track_name, str):
|
||||
return track_name
|
||||
|
||||
# Remove content in parentheses, e.g., (feat. Artist), (Remix)
|
||||
cleaned_name = re.sub(r'\s*\([^)]*\)', '', track_name).strip()
|
||||
# Remove content in square brackets, e.g., [Live], [Explicit]
|
||||
cleaned_name = re.sub(r'\s*\[[^\]]*\]', '', cleaned_name).strip()
|
||||
|
||||
# If cleaning results in an empty string (e.g., track name was only "(Intro)"),
|
||||
# return the original track name to avoid an empty search.
|
||||
if not cleaned_name:
|
||||
return track_name
|
||||
|
||||
# Log cleaning if significant changes were made
|
||||
if cleaned_name != track_name:
|
||||
print(f"🧹 Cleaned track name for search: '{track_name}' -> '{cleaned_name}'")
|
||||
|
||||
return cleaned_name
|
||||
|
||||
@dataclass
|
||||
class TrackAnalysisResult:
|
||||
|
|
@ -2182,7 +2207,10 @@ class ManualMatchModal(QDialog):
|
|||
spotify_track = self.current_track_info['spotify_track']
|
||||
artist = spotify_track.artists[0] if spotify_track.artists else "Unknown"
|
||||
|
||||
# Use the original track name for the info label
|
||||
self.info_label.setText(f"Could not find: <b>{spotify_track.name}</b><br>by {artist}")
|
||||
|
||||
# Use the ORIGINAL, UNCLEANED track name for the initial search query
|
||||
self.search_input.setText(f"{artist} {spotify_track.name}")
|
||||
|
||||
self.search_delay_timer.start(1000)
|
||||
|
|
@ -3352,98 +3380,61 @@ class DownloadMissingTracksModal(QDialog):
|
|||
# Try each search query until we find good results
|
||||
self.try_search_queries(search_queries, spotify_track, track_index, table_index, 0)
|
||||
|
||||
def clean_track_name_for_search(self, track_name):
|
||||
"""Clean track name by removing parentheses content and extra information"""
|
||||
import re
|
||||
|
||||
cleaned_name = track_name
|
||||
|
||||
# Remove content in parentheses - covers feat., remix, remaster, etc.
|
||||
# Examples: "(feat. Artist)", "(Remix)", "(Remastered)", "(Live)", "(Radio Edit)"
|
||||
cleaned_name = re.sub(r'\s*\([^)]*\)', '', cleaned_name)
|
||||
|
||||
# Remove content in square brackets - covers similar extras
|
||||
# Examples: "[feat. Artist]", "[Remix]", "[Explicit]"
|
||||
cleaned_name = re.sub(r'\s*\[[^\]]*\]', '', cleaned_name)
|
||||
|
||||
# Remove common standalone suffixes that appear after dashes
|
||||
# Examples: " - Remix", " - Radio Edit", " - Extended Version"
|
||||
suffixes_to_remove = [
|
||||
r'\s*-\s*remix\s*$',
|
||||
r'\s*-\s*radio\s+edit\s*$',
|
||||
r'\s*-\s*extended\s+version\s*$',
|
||||
r'\s*-\s*remaster\s*$',
|
||||
r'\s*-\s*remastered\s*$',
|
||||
r'\s*-\s*live\s*$',
|
||||
r'\s*-\s*acoustic\s*$',
|
||||
r'\s*-\s*instrumental\s*$'
|
||||
]
|
||||
|
||||
for suffix_pattern in suffixes_to_remove:
|
||||
cleaned_name = re.sub(suffix_pattern, '', cleaned_name, flags=re.IGNORECASE)
|
||||
|
||||
# Clean up extra whitespace
|
||||
cleaned_name = ' '.join(cleaned_name.split())
|
||||
|
||||
# Log cleaning if significant changes were made
|
||||
if cleaned_name != track_name:
|
||||
print(f"🧹 Cleaned track name: '{track_name}' → '{cleaned_name}'")
|
||||
|
||||
return cleaned_name
|
||||
|
||||
def generate_smart_search_queries(self, artist_name, track_name):
|
||||
"""Generate multiple search query variations with special handling for single-word tracks"""
|
||||
# Clean the track name first to remove parentheses content
|
||||
clean_track_name = self.clean_track_name_for_search(track_name)
|
||||
"""
|
||||
Generate multiple search query variations in a specific fallback order.
|
||||
1. Full original query.
|
||||
2. Query with cleaned track name (parentheses removed).
|
||||
3. Query with cleaned track name and a shortened artist name.
|
||||
4. Query with only the cleaned track name.
|
||||
"""
|
||||
import re
|
||||
|
||||
queries = []
|
||||
|
||||
# Check if track name is a single word (no spaces)
|
||||
is_single_word = len(clean_track_name.strip().split()) == 1
|
||||
|
||||
# NEW STRATEGY per user request:
|
||||
# 1. "Virtual Mage Orbit Love" (Artist + Track)
|
||||
# 2. "Orbit Love Virtual M" (Track + Shortened Artist)
|
||||
# 3. "Orbit Love" (Track only)
|
||||
|
||||
# Strategy 1: Artist + Track (recommended format: "Virtual Mage Orbit Love")
|
||||
# --- Attempt 1: Full Original Query ---
|
||||
if artist_name:
|
||||
queries.append(f"{artist_name} {clean_track_name}".strip())
|
||||
queries.append(f"{artist_name} {track_name}".strip())
|
||||
|
||||
# Strategy 2: Track + Shortened Artist (fallback: "Orbit Love Virtual M")
|
||||
# --- Clean the track name for subsequent fallbacks ---
|
||||
cleaned_name = re.sub(r'\s*\([^)]*\)', '', track_name).strip()
|
||||
cleaned_name = re.sub(r'\s*\[[^\]]*\]', '', cleaned_name).strip()
|
||||
|
||||
# If cleaning resulted in an empty string, just use the original name
|
||||
if not cleaned_name:
|
||||
cleaned_name = track_name
|
||||
|
||||
# --- Attempt 2: Artist + Cleaned Track Name ---
|
||||
# Add this only if the cleaning actually changed the name
|
||||
if cleaned_name.lower() != track_name.lower():
|
||||
if artist_name:
|
||||
queries.append(f"{artist_name} {cleaned_name}".strip())
|
||||
|
||||
# --- Attempt 3: Cleaned Track Name + Shortened Artist ---
|
||||
if artist_name:
|
||||
artist_words = artist_name.split()
|
||||
if len(artist_words) > 1:
|
||||
# For multi-word artists: Take first word + first letter of second word
|
||||
# For multi-word artists: "Virtual Mage" -> "Virtual M"
|
||||
short_artist = artist_words[0] + " " + artist_words[1][0] if len(artist_words[1]) > 0 else artist_words[0]
|
||||
else:
|
||||
# For single-word artists: Truncate to ~7 characters
|
||||
short_artist = artist_words[0][:7] if len(artist_words[0]) > 7 else artist_words[0]
|
||||
|
||||
queries.append(f"{clean_track_name} {short_artist}".strip())
|
||||
|
||||
# Strategy 3: Track only (final fallback: "Orbit Love")
|
||||
queries.append(clean_track_name.strip())
|
||||
|
||||
# Legacy single-word handling for backward compatibility
|
||||
if is_single_word:
|
||||
print(f"🎯 Single-word track detected: '{track_name}' - using enhanced search strategy")
|
||||
first_word = artist_name.split()[0] if artist_name else ""
|
||||
if first_word and len(first_word) > 2:
|
||||
queries.append(f"{track_name} {first_word}".strip())
|
||||
|
||||
# Strategy 4: Track name + full artist name (traditional approach)
|
||||
if artist_name:
|
||||
queries.append(f"{track_name} {artist_name}".strip())
|
||||
|
||||
# Remove duplicates while preserving order
|
||||
queries.append(f"{cleaned_name} {short_artist}".strip())
|
||||
|
||||
# --- Attempt 4: Cleaned Track Name Only ---
|
||||
queries.append(cleaned_name.strip())
|
||||
|
||||
# --- Finalize: Remove duplicates while preserving the fallback order ---
|
||||
unique_queries = []
|
||||
for query in queries:
|
||||
if query and query not in unique_queries:
|
||||
unique_queries.append(query)
|
||||
|
||||
print(f"🧠 Generated {len(unique_queries)} smart queries for '{track_name}'. Sequence: {unique_queries}")
|
||||
return unique_queries
|
||||
|
||||
|
||||
def shorten_artist_name(self, artist_name):
|
||||
"""Remove common articles and prefixes that cause search issues"""
|
||||
if not artist_name:
|
||||
|
|
@ -5515,97 +5506,78 @@ class DownloadMissingTracksModal(QDialog):
|
|||
'table_index': table_index,
|
||||
'download_index': download_index,
|
||||
'completed': False,
|
||||
'used_sources': set() # Track sources to avoid retrying same ones
|
||||
'used_sources': set()
|
||||
}
|
||||
|
||||
# Use existing search infrastructure but with parallel completion callback
|
||||
self.start_search_worker_parallel(search_queries[0], search_queries, spotify_track, track_index, table_index, 0, download_index)
|
||||
|
||||
def start_search_worker_parallel(self, query, queries, spotify_track, track_index, table_index, query_index, download_index):
|
||||
"""Start search worker with parallel completion handling"""
|
||||
print(f"🔍 Starting parallel search worker for query: {query}")
|
||||
|
||||
# CORRECTED CALL: Removed the extra 'search_queries[0]' argument.
|
||||
self.start_search_worker_parallel(search_queries, spotify_track, track_index, table_index, 0, download_index)
|
||||
|
||||
def start_search_worker_parallel(self, queries, spotify_track, track_index, table_index, query_index, download_index):
|
||||
"""
|
||||
Start search worker with parallel completion handling.
|
||||
This now iterates through the list of queries.
|
||||
"""
|
||||
# Check if we have exhausted all queries for this track
|
||||
if query_index >= len(queries):
|
||||
print(f"❌ All {len(queries)} search queries failed for download {download_index + 1}")
|
||||
self.on_parallel_track_failed(download_index, "All search strategies failed")
|
||||
return
|
||||
|
||||
query = queries[query_index]
|
||||
print(f"🔍 Starting parallel search worker for query {query_index + 1}/{len(queries)}: '{query}'")
|
||||
|
||||
# Create a simple parallel search worker
|
||||
from PyQt6.QtCore import QRunnable, QObject, pyqtSignal
|
||||
|
||||
|
||||
class ParallelSearchWorkerSignals(QObject):
|
||||
search_completed = pyqtSignal(list, str) # results, query
|
||||
search_failed = pyqtSignal(str, str) # query, error
|
||||
|
||||
|
||||
class ParallelSearchWorker(QRunnable):
|
||||
def __init__(self, soulseek_client, query):
|
||||
super().__init__()
|
||||
self.soulseek_client = soulseek_client
|
||||
self.query = query
|
||||
self.signals = ParallelSearchWorkerSignals()
|
||||
|
||||
|
||||
def run(self):
|
||||
loop = None
|
||||
try:
|
||||
import asyncio
|
||||
# Create a fresh event loop for this thread
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
# Perform search with proper await
|
||||
search_result = loop.run_until_complete(self.soulseek_client.search(self.query))
|
||||
|
||||
# Debug: Check what we actually got
|
||||
print(f"🔍 DEBUG: Search '{self.query}' returned type: {type(search_result)}")
|
||||
|
||||
# Handle tuple result structure (tracks, albums)
|
||||
if isinstance(search_result, tuple) and len(search_result) >= 1:
|
||||
# First element is the tracks list
|
||||
results_list = search_result[0] if search_result[0] else []
|
||||
print(f"🔍 DEBUG: Extracted {len(results_list)} tracks from tuple")
|
||||
elif hasattr(search_result, '__iter__') and not isinstance(search_result, (str, bytes)):
|
||||
results_list = list(search_result)
|
||||
else:
|
||||
results_list = []
|
||||
|
||||
# Debug first actual result
|
||||
if results_list and len(results_list) > 0:
|
||||
first_track = results_list[0]
|
||||
print(f"🔍 DEBUG: First track type: {type(first_track)}")
|
||||
print(f"🔍 DEBUG: First track attributes: {[attr for attr in dir(first_track) if not attr.startswith('_')]}")
|
||||
if hasattr(first_track, 'filename'):
|
||||
print(f"🔍 DEBUG: First track filename: '{first_track.filename}'")
|
||||
|
||||
# Emit completion signal
|
||||
self.signals.search_completed.emit(results_list, self.query)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Parallel search error for '{self.query}': {str(e)}")
|
||||
self.signals.search_failed.emit(self.query, str(e))
|
||||
finally:
|
||||
if loop:
|
||||
try:
|
||||
pending = asyncio.all_tasks(loop)
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
if pending:
|
||||
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
|
||||
loop.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Create and configure worker
|
||||
worker = ParallelSearchWorker(self.parent_page.soulseek_client, query)
|
||||
|
||||
# Connect to parallel completion handler
|
||||
# Connect to the completion handler, passing all necessary context
|
||||
worker.signals.search_completed.connect(
|
||||
lambda results, q, qs=queries, st=spotify_track, ti=track_index,
|
||||
tabi=table_index, qi=query_index, di=download_index:
|
||||
self.on_search_query_completed_parallel(results, qs, st, ti, tabi, qi, q, di)
|
||||
)
|
||||
|
||||
# Handle search failures
|
||||
worker.signals.search_failed.connect(
|
||||
lambda q, error, di=download_index:
|
||||
self.on_parallel_track_failed(di, f"Search failed for '{q}': {error}")
|
||||
lambda q, error, qs=queries, st=spotify_track, ti=track_index,
|
||||
tabi=table_index, qi=query_index, di=download_index:
|
||||
self.on_search_query_completed_parallel([], qs, st, ti, tabi, qi, q, di) # Treat failure as empty result
|
||||
)
|
||||
|
||||
# Submit to thread pool
|
||||
if hasattr(self.parent_page, 'thread_pool'):
|
||||
self.parent_page.thread_pool.start(worker)
|
||||
else:
|
||||
|
|
@ -5614,11 +5586,13 @@ class DownloadMissingTracksModal(QDialog):
|
|||
thread_pool.start(worker)
|
||||
|
||||
return worker
|
||||
|
||||
|
||||
def on_search_query_completed_parallel(self, results, queries, spotify_track, track_index, table_index, query_index, query, download_index):
|
||||
"""Handle completion of a parallel search query"""
|
||||
"""
|
||||
Handle completion of a parallel search query. If it fails, it will now
|
||||
trigger the next query in the list.
|
||||
"""
|
||||
if hasattr(self, 'cancel_requested') and self.cancel_requested:
|
||||
print("🛑 Parallel search query completed after cancellation - ignoring results")
|
||||
self.on_parallel_track_completed(download_index, False)
|
||||
return
|
||||
|
||||
|
|
@ -5632,23 +5606,23 @@ class DownloadMissingTracksModal(QDialog):
|
|||
|
||||
if valid_candidates:
|
||||
print(f"🎯 Found {len(valid_candidates)} valid candidates for parallel download {download_index + 1}")
|
||||
|
||||
# --- NEW CODE: Cache the candidates and the original spotify track ---
|
||||
if hasattr(self, 'parallel_search_tracking') and download_index in self.parallel_search_tracking:
|
||||
self.parallel_search_tracking[download_index]['candidates'] = valid_candidates
|
||||
self.parallel_search_tracking[download_index]['spotify_track'] = spotify_track
|
||||
# --- END OF NEW CODE ---
|
||||
|
||||
best_match = valid_candidates[0]
|
||||
print(f"🎯 Selected best match for parallel download: {best_match.filename}")
|
||||
|
||||
if hasattr(self.parent_page, 'log_area'):
|
||||
self.parent_page.log_area.append(f" 🎯 Best match (parallel #{download_index + 1}): {best_match.filename}")
|
||||
|
||||
self.validate_slskd_result_with_spotify_parallel(best_match, spotify_track, track_index, table_index, valid_candidates, download_index)
|
||||
return
|
||||
|
||||
self.on_parallel_track_failed(download_index, f"No valid results for query: {query}")
|
||||
|
||||
# --- THIS IS THE NEW RETRY LOGIC ---
|
||||
# If we are here, the current query failed. Try the next one.
|
||||
next_query_index = query_index + 1
|
||||
if next_query_index < len(queries):
|
||||
print(f"🔄 No valid results for '{query}'. Trying next query...")
|
||||
if hasattr(self.parent_page, 'log_area'):
|
||||
self.parent_page.log_area.append(f" 🔄 No valid results for '{query}'. Trying next query...")
|
||||
# Call the search worker again with the next query index
|
||||
self.start_search_worker_parallel(queries, spotify_track, track_index, table_index, next_query_index, download_index)
|
||||
else:
|
||||
# All queries have been tried and failed
|
||||
print(f"❌ All {len(queries)} search queries failed for download {download_index + 1}")
|
||||
self.on_parallel_track_failed(download_index, f"No valid results after trying all {len(queries)} queries.")
|
||||
|
||||
def validate_slskd_result_with_spotify_parallel(self, slskd_result, original_spotify_track, track_index, table_index, valid_candidates, download_index):
|
||||
"""Parallel version of Spotify validation - SIMPLIFIED for speed"""
|
||||
|
|
|
|||
Loading…
Reference in a new issue