matching improvements and bug fixes
This commit is contained in:
parent
d9e41bc12f
commit
3bbb635848
20 changed files with 121 additions and 29 deletions
BIN
__pycache__/main.cpython-310.pyc
Normal file
BIN
__pycache__/main.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
core/__pycache__/soulseek_client.cpython-310.pyc
Normal file
BIN
core/__pycache__/soulseek_client.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
37
main.py
|
|
@ -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}")
|
||||||
|
|
||||||
|
|
|
||||||
BIN
ui/pages/__pycache__/artists.cpython-310.pyc
Normal file
BIN
ui/pages/__pycache__/artists.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
ui/pages/__pycache__/downloads.cpython-310.pyc
Normal file
BIN
ui/pages/__pycache__/downloads.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
ui/pages/__pycache__/sync.cpython-310.pyc
Normal file
BIN
ui/pages/__pycache__/sync.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
Loading…
Reference in a new issue