matching improvements and bug fixes

This commit is contained in:
Broque Thomas 2025-08-09 13:47:00 -07:00
parent d9e41bc12f
commit 3bbb635848
20 changed files with 121 additions and 29 deletions

Binary file not shown.

Binary file not shown.

View file

@ -52,7 +52,8 @@ class MusicMatchingEngine:
""" """
if not text: if not text:
return "" return ""
text = re.sub(r'koяn', 'korn', text, flags=re.IGNORECASE) # Handle Korn/KoЯn variations - both uppercase Я (U+042F) and lowercase я (U+044F)
text = re.sub(r'ko[яЯ]n', 'korn', text, flags=re.IGNORECASE)
text = unidecode(text) text = unidecode(text)
text = text.lower() text = text.lower()

View file

@ -203,6 +203,7 @@ class SoulseekClient:
self.base_url: Optional[str] = None self.base_url: Optional[str] = None
self.api_key: Optional[str] = None self.api_key: Optional[str] = None
self.download_path: Path = Path("./downloads") self.download_path: Path = Path("./downloads")
self.active_searches: Dict[str, bool] = {} # search_id -> still_active
self._setup_client() self._setup_client()
def _setup_client(self): def _setup_client(self):
@ -566,6 +567,9 @@ class SoulseekClient:
logger.info(f"Search initiated with ID: {search_id}") logger.info(f"Search initiated with ID: {search_id}")
# Track this search as active
self.active_searches[search_id] = True
# Poll for results - process and emit results immediately when found # Poll for results - process and emit results immediately when found
all_responses = [] all_responses = []
all_tracks = [] all_tracks = []
@ -574,6 +578,11 @@ class SoulseekClient:
max_polls = int(timeout / poll_interval) # 20 attempts over 30 seconds max_polls = int(timeout / poll_interval) # 20 attempts over 30 seconds
for poll_count in range(max_polls): for poll_count in range(max_polls):
# Check if search was cancelled
if search_id not in self.active_searches:
logger.info(f"Search {search_id} was cancelled, stopping")
return [], []
logger.debug(f"Polling for results (attempt {poll_count + 1}/{max_polls}) - elapsed: {poll_count * poll_interval:.1f}s") logger.debug(f"Polling for results (attempt {poll_count + 1}/{max_polls}) - elapsed: {poll_count * poll_interval:.1f}s")
# Get current search responses # Get current search responses
@ -627,6 +636,10 @@ class SoulseekClient:
except Exception as e: except Exception as e:
logger.error(f"Error searching: {e}") logger.error(f"Error searching: {e}")
return [], [] return [], []
finally:
# Remove from active searches when done
if 'search_id' in locals() and search_id in self.active_searches:
del self.active_searches[search_id]
async def download(self, username: str, filename: str, file_size: int = 0) -> Optional[str]: async def download(self, username: str, filename: str, file_size: int = 0) -> Optional[str]:
if not self.base_url: if not self.base_url:
@ -1338,9 +1351,26 @@ class SoulseekClient:
"""Check if slskd is configured (has base_url)""" """Check if slskd is configured (has base_url)"""
return self.base_url is not None return self.base_url is not None
async def cancel_all_searches(self):
"""Cancel all active searches"""
if not self.active_searches:
return
logger.info(f"Cancelling {len(self.active_searches)} active searches...")
for search_id in list(self.active_searches.keys()):
try:
# Delete the search via API
await self._make_request('DELETE', f'searches/{search_id}')
logger.debug(f"Cancelled search {search_id}")
except Exception as e:
logger.warning(f"Could not cancel search {search_id}: {e}")
# Mark all searches as cancelled
self.active_searches.clear()
async def close(self): async def close(self):
# No persistent session to close - each request creates its own session # Cancel any active searches before closing
pass await self.cancel_all_searches()
def __del__(self): def __del__(self):
# No persistent session to clean up # No persistent session to clean up

37
main.py
View file

@ -5,7 +5,7 @@ import asyncio
import time import time
from pathlib import Path from pathlib import Path
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, QStackedWidget from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QWidget, QStackedWidget
from PyQt6.QtCore import QThread, pyqtSignal, QTimer from PyQt6.QtCore import QThread, pyqtSignal, QTimer, QThreadPool
from PyQt6.QtGui import QFont, QPalette, QColor from PyQt6.QtGui import QFont, QPalette, QColor
from config.settings import config_manager from config.settings import config_manager
@ -58,7 +58,7 @@ class ServiceStatusThread(QThread):
def stop(self): def stop(self):
self.running = False self.running = False
self.quit() self.quit()
self.wait() self.wait(2000) # Wait max 2 seconds
class MainWindow(QMainWindow): class MainWindow(QMainWindow):
def __init__(self): def __init__(self):
@ -321,6 +321,27 @@ class MainWindow(QMainWindow):
logger.info("Cleaning up Dashboard page threads...") logger.info("Cleaning up Dashboard page threads...")
self.dashboard_page.cleanup_threads() self.dashboard_page.cleanup_threads()
# Stop other page threads and background tasks
if hasattr(self, 'artists_page') and self.artists_page:
logger.info("Cleaning up Artists page threads...")
if hasattr(self.artists_page, 'cleanup_threads'):
self.artists_page.cleanup_threads()
if hasattr(self, 'sync_page') and self.sync_page:
logger.info("Cleaning up Sync page threads...")
if hasattr(self.sync_page, 'cleanup_threads'):
self.sync_page.cleanup_threads()
if hasattr(self, 'downloads_page') and self.downloads_page:
logger.info("Cleaning up Downloads page threads...")
if hasattr(self.downloads_page, 'cleanup_threads'):
self.downloads_page.cleanup_threads()
# Stop all QThreadPool tasks
logger.info("Stopping global thread pool...")
QThreadPool.globalInstance().clear()
QThreadPool.globalInstance().waitForDone(1000) # Wait max 1 second
# Stop status monitoring thread # Stop status monitoring thread
if self.status_thread: if self.status_thread:
logger.info("Stopping status monitoring thread...") logger.info("Stopping status monitoring thread...")
@ -334,8 +355,16 @@ class MainWindow(QMainWindow):
# Close Soulseek client # Close Soulseek client
try: try:
logger.info("Closing Soulseek client...") logger.info("Closing Soulseek client...")
loop = asyncio.get_event_loop() # Use modern asyncio approach instead of deprecated get_event_loop
loop.run_until_complete(self.soulseek_client.close()) try:
loop = asyncio.get_running_loop()
# Create a new task to close the client
task = asyncio.create_task(self.soulseek_client.close())
# Wait for it to complete
asyncio.run_coroutine_threadsafe(self.soulseek_client.close(), loop).result(timeout=3.0)
except RuntimeError:
# No running loop, create new one
asyncio.run(self.soulseek_client.close())
except Exception as e: except Exception as e:
logger.error(f"Error closing Soulseek client: {e}") logger.error(f"Error closing Soulseek client: {e}")

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -2884,9 +2884,20 @@ class DownloadMissingAlbumTracksModal(QDialog):
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
search_result = loop.run_until_complete(self.soulseek_client.search(self.query)) search_result = loop.run_until_complete(self.soulseek_client.search(self.query))
results_list = search_result[0] if isinstance(search_result, tuple) and search_result else [] results_list = search_result[0] if isinstance(search_result, tuple) and search_result else []
self.signals.search_completed.emit(results_list, self.query)
# Check if signals object is still valid before emitting
try:
self.signals.search_completed.emit(results_list, self.query)
except RuntimeError:
# Qt objects deleted during shutdown, ignore
logger.debug(f"Search completed for '{self.query}' but UI already closed")
except Exception as e: except Exception as e:
self.signals.search_failed.emit(self.query, str(e)) try:
self.signals.search_failed.emit(self.query, str(e))
except RuntimeError:
# Qt objects deleted during shutdown, ignore
logger.debug(f"Search failed for '{self.query}' but UI already closed: {e}")
finally: finally:
if loop: loop.close() if loop: loop.close()

View file

@ -970,9 +970,20 @@ class DownloadMissingWishlistTracksModal(QDialog):
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
search_result = loop.run_until_complete(self.soulseek_client.search(self.query)) search_result = loop.run_until_complete(self.soulseek_client.search(self.query))
results_list = search_result[0] if isinstance(search_result, tuple) and search_result else [] results_list = search_result[0] if isinstance(search_result, tuple) and search_result else []
self.signals.search_completed.emit(results_list, self.query)
# Check if signals object is still valid before emitting
try:
self.signals.search_completed.emit(results_list, self.query)
except RuntimeError:
# Qt objects deleted during shutdown, ignore
logger.debug(f"Search completed for '{self.query}' but UI already closed")
except Exception as e: except Exception as e:
self.signals.search_failed.emit(self.query, str(e)) try:
self.signals.search_failed.emit(self.query, str(e))
except RuntimeError:
# Qt objects deleted during shutdown, ignore
logger.debug(f"Search failed for '{self.query}' but UI already closed: {e}")
finally: finally:
if loop: loop.close() if loop: loop.close()

View file

@ -564,7 +564,7 @@ class SpotifyMatchingModal(QDialog):
self.skipped_matching = False self.skipped_matching = False
# Clean up the image download pool # Clean up the image download pool
self.image_download_pool.clear() self.image_download_pool.clear()
self.image_download_pool.waitForDone(-1) # Wait indefinitely for tasks to finish self.image_download_pool.waitForDone(5000) # Wait max 5 seconds for tasks to finish
self.cancelled.emit() self.cancelled.emit()
super().reject() super().reject()
@ -9085,21 +9085,20 @@ class DownloadsPage(QWidget):
try: try:
# Try to get existing event loop first # Try to get existing event loop first
try: try:
loop = asyncio.get_event_loop() loop = asyncio.get_running_loop()
if loop.is_running(): # Loop is already running, we need to run in a thread
# If loop is running, we need to run in a thread def run_in_thread():
def run_in_thread(): new_loop = asyncio.new_event_loop()
new_loop = asyncio.new_event_loop() asyncio.set_event_loop(new_loop)
asyncio.set_event_loop(new_loop) try:
try: new_loop.run_until_complete(self._cancel_current_streaming_download())
new_loop.run_until_complete(self._cancel_current_streaming_download()) finally:
finally: new_loop.close()
new_loop.close()
thread = threading.Thread(target=run_in_thread)
thread = threading.Thread(target=run_in_thread) thread.start()
thread.start() thread.join(timeout=5.0) # Wait max 5 seconds
thread.join(timeout=5.0) # Wait max 5 seconds return
return
except RuntimeError: except RuntimeError:
# No event loop in current thread # No event loop in current thread
pass pass

View file

@ -4814,9 +4814,20 @@ class DownloadMissingTracksModal(QDialog):
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
search_result = loop.run_until_complete(self.soulseek_client.search(self.query)) search_result = loop.run_until_complete(self.soulseek_client.search(self.query))
results_list = search_result[0] if isinstance(search_result, tuple) and search_result else [] results_list = search_result[0] if isinstance(search_result, tuple) and search_result else []
self.signals.search_completed.emit(results_list, self.query)
# Check if signals object is still valid before emitting
try:
self.signals.search_completed.emit(results_list, self.query)
except RuntimeError:
# Qt objects deleted during shutdown, ignore
logger.debug(f"Search completed for '{self.query}' but UI already closed")
except Exception as e: except Exception as e:
self.signals.search_failed.emit(self.query, str(e)) try:
self.signals.search_failed.emit(self.query, str(e))
except RuntimeError:
# Qt objects deleted during shutdown, ignore
logger.debug(f"Search failed for '{self.query}' but UI already closed: {e}")
finally: finally:
if loop: loop.close() if loop: loop.close()