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:
|
||||
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
|
||||
|
|
|
|||
37
main.py
37
main.py
|
|
@ -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}")
|
||||
|
||||
|
|
|
|||
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)
|
||||
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()
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Reference in a new issue