matching improvements and bug fixes

pull/8/head
Broque Thomas 9 months ago
parent d9e41bc12f
commit 3bbb635848

Binary file not shown.

@ -52,7 +52,8 @@ class MusicMatchingEngine:
"""
if not text:
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 = text.lower()

@ -203,6 +203,7 @@ class SoulseekClient:
self.base_url: Optional[str] = None
self.api_key: Optional[str] = None
self.download_path: Path = Path("./downloads")
self.active_searches: Dict[str, bool] = {} # search_id -> still_active
self._setup_client()
def _setup_client(self):
@ -566,6 +567,9 @@ class SoulseekClient:
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
all_responses = []
all_tracks = []
@ -574,6 +578,11 @@ class SoulseekClient:
max_polls = int(timeout / poll_interval) # 20 attempts over 30 seconds
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")
# Get current search responses
@ -627,6 +636,10 @@ class SoulseekClient:
except Exception as e:
logger.error(f"Error searching: {e}")
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]:
if not self.base_url:
@ -1338,9 +1351,26 @@ class SoulseekClient:
"""Check if slskd is configured (has base_url)"""
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):
# No persistent session to close - each request creates its own session
pass
# Cancel any active searches before closing
await self.cancel_all_searches()
def __del__(self):
# No persistent session to clean up

@ -5,7 +5,7 @@ import asyncio
import time
from pathlib import Path
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 config.settings import config_manager
@ -58,7 +58,7 @@ class ServiceStatusThread(QThread):
def stop(self):
self.running = False
self.quit()
self.wait()
self.wait(2000) # Wait max 2 seconds
class MainWindow(QMainWindow):
def __init__(self):
@ -321,6 +321,27 @@ class MainWindow(QMainWindow):
logger.info("Cleaning up Dashboard page 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
if self.status_thread:
logger.info("Stopping status monitoring thread...")
@ -334,8 +355,16 @@ class MainWindow(QMainWindow):
# Close Soulseek client
try:
logger.info("Closing Soulseek client...")
loop = asyncio.get_event_loop()
loop.run_until_complete(self.soulseek_client.close())
# Use modern asyncio approach instead of deprecated get_event_loop
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:
logger.error(f"Error closing Soulseek client: {e}")

@ -2884,9 +2884,20 @@ class DownloadMissingAlbumTracksModal(QDialog):
asyncio.set_event_loop(loop)
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 []
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:
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:
if loop: loop.close()

@ -970,9 +970,20 @@ class DownloadMissingWishlistTracksModal(QDialog):
asyncio.set_event_loop(loop)
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 []
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:
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:
if loop: loop.close()

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

@ -4814,9 +4814,20 @@ class DownloadMissingTracksModal(QDialog):
asyncio.set_event_loop(loop)
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 []
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:
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:
if loop: loop.close()

Loading…
Cancel
Save