better
This commit is contained in:
parent
0021fc52dd
commit
f3dd76feae
4 changed files with 41963 additions and 103 deletions
|
|
@ -1 +1 @@
|
||||||
{"access_token": "BQBHSun7wW8zGHATuxfUrZt4LqhiSjFAP_nZkNLOx4GJjxvcgerqk9j7qGjL2ghTTgFFF6XZyZ8RNfXhjJXNPw9a1ti9CTZaq92uq2FxcAPQo5EdgNGwhIQ96ToJcHG7yKBnkCLJpcn9zb8LA3WlGnO2Q0WaHgjBljEhd8ECsz4h-MueVpDohsr6j-DbE2Wt6Sv87TxrdXaqVTKYOu3Ox4LdAAK1D1UYM46s2Onvzd0hTkoV1p5fd2csHmE7xM9d", "token_type": "Bearer", "expires_in": 3600, "scope": "user-library-read user-read-private playlist-read-private playlist-read-collaborative user-read-email", "expires_at": 1753315041, "refresh_token": "AQDmfQkPCGObfJeTUIbW1hAAwhSqkuHRA3Qh2dqVYMRh0eCkFMQgPNJDDzF8y-BiaVbj80zePkK_XSfYH1aJutMtNbnsqRKWuxP31BTrMc7pdUdbE7Fma4oH8wpDUKdG3MM"}
|
{"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"}
|
||||||
41673
logs/app.log
41673
logs/app.log
File diff suppressed because it is too large
Load diff
Binary file not shown.
385
ui/pages/sync.py
385
ui/pages/sync.py
|
|
@ -1953,76 +1953,131 @@ class SyncPage(QWidget):
|
||||||
|
|
||||||
|
|
||||||
class ManualMatchModal(QDialog):
|
class ManualMatchModal(QDialog):
|
||||||
"""Modal for manually searching and downloading a failed track."""
|
"""
|
||||||
|
A completely redesigned modal for manually searching and resolving a failed track download.
|
||||||
|
Features controlled searching, cancellation, and a UI consistent with the main application.
|
||||||
|
This version dynamically updates its track list from the parent modal and has a live-updating count.
|
||||||
|
"""
|
||||||
track_resolved = pyqtSignal(object)
|
track_resolved = pyqtSignal(object)
|
||||||
|
|
||||||
def __init__(self, failed_tracks, parent_modal):
|
def __init__(self, parent_modal):
|
||||||
|
"""Initializes the modal with a direct reference to the parent."""
|
||||||
super().__init__(parent_modal)
|
super().__init__(parent_modal)
|
||||||
self.parent_modal = parent_modal
|
self.parent_modal = parent_modal
|
||||||
self.soulseek_client = parent_modal.parent_page.soulseek_client
|
self.soulseek_client = parent_modal.parent_page.soulseek_client
|
||||||
self.downloads_page = parent_modal.downloads_page
|
self.downloads_page = parent_modal.downloads_page
|
||||||
|
|
||||||
self.failed_tracks = list(failed_tracks) # Use a copy of the list
|
self.failed_tracks = []
|
||||||
self.current_track_index = 0 # Track current position
|
self.current_track_index = 0
|
||||||
self.current_track_info = None
|
self.current_track_info = None
|
||||||
self.search_worker = None
|
self.search_worker = None
|
||||||
|
self.thread_pool = QThreadPool.globalInstance()
|
||||||
|
|
||||||
|
# Timer to delay automatic search
|
||||||
|
self.search_delay_timer = QTimer(self)
|
||||||
|
self.search_delay_timer.setSingleShot(True)
|
||||||
|
self.search_delay_timer.timeout.connect(self.perform_manual_search)
|
||||||
|
|
||||||
|
# Timer to periodically check for updates to the total failed track count
|
||||||
|
self.live_update_timer = QTimer(self)
|
||||||
|
self.live_update_timer.timeout.connect(self._check_and_update_count)
|
||||||
|
self.live_update_timer.start(1000) # Check every second
|
||||||
|
|
||||||
self.setWindowTitle("Manual Track Correction")
|
|
||||||
self.setMinimumSize(900, 700)
|
|
||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
self.load_current_track()
|
self.load_current_track()
|
||||||
|
|
||||||
def setup_ui(self):
|
def setup_ui(self):
|
||||||
|
"""Set up the visually redesigned UI."""
|
||||||
|
self.setWindowTitle("Manual Track Correction")
|
||||||
|
self.setMinimumSize(900, 700)
|
||||||
self.setStyleSheet("""
|
self.setStyleSheet("""
|
||||||
QDialog { background-color: #1e1e1e; }
|
QDialog { background-color: #1e1e1e; color: #ffffff; }
|
||||||
QLabel { color: #ffffff; font-size: 14px; }
|
QLabel { color: #ffffff; font-size: 14px; }
|
||||||
QPushButton {
|
|
||||||
background-color: #1db954; color: #000000; border: none;
|
|
||||||
border-radius: 6px; font-size: 13px; font-weight: bold;
|
|
||||||
padding: 10px 20px; min-width: 80px;
|
|
||||||
}
|
|
||||||
QPushButton:hover { background-color: #1ed760; }
|
|
||||||
QPushButton:disabled { background-color: #404040; color: #888888; }
|
|
||||||
QLineEdit {
|
QLineEdit {
|
||||||
background: #404040; border: 1px solid #606060; border-radius: 6px;
|
background-color: #3a3a3a;
|
||||||
padding: 10px; color: #ffffff; font-size: 13px;
|
border: 1px solid #555555;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
QScrollArea { border: none; }
|
QScrollArea { border: none; background-color: #2d2d2d; }
|
||||||
|
QWidget#resultsWidget { background-color: #2d2d2d; }
|
||||||
""")
|
""")
|
||||||
self.main_layout = QVBoxLayout(self)
|
|
||||||
self.main_layout.setContentsMargins(20, 20, 20, 20)
|
|
||||||
self.main_layout.setSpacing(15)
|
|
||||||
|
|
||||||
|
main_layout = QVBoxLayout(self)
|
||||||
|
main_layout.setContentsMargins(20, 20, 20, 20)
|
||||||
|
main_layout.setSpacing(15)
|
||||||
|
|
||||||
|
# --- Failed Track Info Card ---
|
||||||
info_frame = QFrame()
|
info_frame = QFrame()
|
||||||
info_frame.setStyleSheet("background-color: #2d2d2d; border-radius: 8px; padding: 15px;")
|
info_frame.setStyleSheet("""
|
||||||
|
QFrame {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
border: 1px solid #444444;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
info_layout = QVBoxLayout(info_frame)
|
info_layout = QVBoxLayout(info_frame)
|
||||||
self.info_label = QLabel("Loading track...")
|
self.info_label = QLabel("Loading track...")
|
||||||
self.info_label.setFont(QFont("Arial", 16, QFont.Weight.Bold))
|
self.info_label.setFont(QFont("Arial", 16, QFont.Weight.Bold))
|
||||||
|
self.info_label.setStyleSheet("color: #ffc107;") # Amber color for warning
|
||||||
self.info_label.setWordWrap(True)
|
self.info_label.setWordWrap(True)
|
||||||
info_layout.addWidget(self.info_label)
|
info_layout.addWidget(self.info_label)
|
||||||
self.main_layout.addWidget(info_frame)
|
main_layout.addWidget(info_frame)
|
||||||
|
|
||||||
|
# --- Search Input and Controls ---
|
||||||
|
search_frame = QFrame()
|
||||||
|
search_layout = QHBoxLayout(search_frame)
|
||||||
|
search_layout.setContentsMargins(0,0,0,0)
|
||||||
|
search_layout.setSpacing(10)
|
||||||
|
|
||||||
search_layout = QHBoxLayout()
|
|
||||||
self.search_input = QLineEdit()
|
self.search_input = QLineEdit()
|
||||||
|
self.search_input.setPlaceholderText("Enter a new search query or use the suggestion...")
|
||||||
self.search_input.returnPressed.connect(self.perform_manual_search)
|
self.search_input.returnPressed.connect(self.perform_manual_search)
|
||||||
|
|
||||||
self.search_btn = QPushButton("Search")
|
self.search_btn = QPushButton("Search")
|
||||||
self.search_btn.clicked.connect(self.perform_manual_search)
|
self.search_btn.clicked.connect(self.perform_manual_search)
|
||||||
search_layout.addWidget(self.search_input)
|
self.search_btn.setStyleSheet("""
|
||||||
search_layout.addWidget(self.search_btn)
|
QPushButton {
|
||||||
self.main_layout.addLayout(search_layout)
|
background-color: #1db954; color: #000000; border: none;
|
||||||
|
border-radius: 6px; font-size: 13px; font-weight: bold;
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
QPushButton:hover { background-color: #1ed760; }
|
||||||
|
""")
|
||||||
|
|
||||||
|
self.cancel_search_btn = QPushButton("Cancel")
|
||||||
|
self.cancel_search_btn.clicked.connect(self.cancel_current_search)
|
||||||
|
self.cancel_search_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: #d32f2f; color: #ffffff; border: none;
|
||||||
|
border-radius: 6px; font-size: 13px; font-weight: bold;
|
||||||
|
padding: 10px 20px;
|
||||||
|
}
|
||||||
|
QPushButton:hover { background-color: #f44336; }
|
||||||
|
""")
|
||||||
|
self.cancel_search_btn.hide() # Initially hidden
|
||||||
|
|
||||||
|
search_layout.addWidget(self.search_input, 1)
|
||||||
|
search_layout.addWidget(self.search_btn)
|
||||||
|
search_layout.addWidget(self.cancel_search_btn)
|
||||||
|
main_layout.addWidget(search_frame)
|
||||||
|
|
||||||
|
# --- Search Results Area ---
|
||||||
self.results_scroll = QScrollArea()
|
self.results_scroll = QScrollArea()
|
||||||
self.results_scroll.setWidgetResizable(True)
|
self.results_scroll.setWidgetResizable(True)
|
||||||
self.results_widget = QWidget()
|
self.results_widget = QWidget()
|
||||||
|
self.results_widget.setObjectName("resultsWidget")
|
||||||
self.results_layout = QVBoxLayout(self.results_widget)
|
self.results_layout = QVBoxLayout(self.results_widget)
|
||||||
self.results_layout.setSpacing(8)
|
self.results_layout.setSpacing(8)
|
||||||
|
self.results_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
||||||
self.results_scroll.setWidget(self.results_widget)
|
self.results_scroll.setWidget(self.results_widget)
|
||||||
self.main_layout.addWidget(self.results_scroll, 1)
|
main_layout.addWidget(self.results_scroll, 1)
|
||||||
|
|
||||||
# Navigation and control buttons
|
# --- Navigation and Close Buttons ---
|
||||||
nav_layout = QHBoxLayout()
|
nav_layout = QHBoxLayout()
|
||||||
|
|
||||||
self.prev_btn = QPushButton("← Previous")
|
self.prev_btn = QPushButton("← Previous")
|
||||||
self.prev_btn.clicked.connect(self.load_previous_track)
|
self.prev_btn.clicked.connect(self.load_previous_track)
|
||||||
|
|
||||||
|
|
@ -2034,15 +2089,14 @@ class ManualMatchModal(QDialog):
|
||||||
|
|
||||||
self.close_btn = QPushButton("Close")
|
self.close_btn = QPushButton("Close")
|
||||||
self.close_btn.setStyleSheet("""
|
self.close_btn.setStyleSheet("""
|
||||||
QPushButton {
|
QPushButton { background-color: #616161; color: #ffffff; }
|
||||||
background-color: #616161; color: #ffffff;
|
QPushButton:hover { background-color: #757575; }
|
||||||
}
|
|
||||||
QPushButton:hover {
|
|
||||||
background-color: #757575;
|
|
||||||
}
|
|
||||||
""")
|
""")
|
||||||
self.close_btn.clicked.connect(self.reject)
|
self.close_btn.clicked.connect(self.reject)
|
||||||
|
|
||||||
|
for btn in [self.prev_btn, self.next_btn, self.close_btn]:
|
||||||
|
btn.setFixedSize(120, 40)
|
||||||
|
|
||||||
nav_layout.addWidget(self.prev_btn)
|
nav_layout.addWidget(self.prev_btn)
|
||||||
nav_layout.addStretch()
|
nav_layout.addStretch()
|
||||||
nav_layout.addWidget(self.track_position_label)
|
nav_layout.addWidget(self.track_position_label)
|
||||||
|
|
@ -2050,17 +2104,78 @@ class ManualMatchModal(QDialog):
|
||||||
nav_layout.addWidget(self.next_btn)
|
nav_layout.addWidget(self.next_btn)
|
||||||
nav_layout.addWidget(self.close_btn)
|
nav_layout.addWidget(self.close_btn)
|
||||||
|
|
||||||
self.main_layout.addLayout(nav_layout)
|
main_layout.addLayout(nav_layout)
|
||||||
|
|
||||||
|
def _check_and_update_count(self):
|
||||||
|
"""
|
||||||
|
Periodically called by a timer to check if the total number of failed
|
||||||
|
tracks has changed and updates the navigation label if needed.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
live_total = len(self.parent_modal.permanently_failed_tracks)
|
||||||
|
|
||||||
|
# Extract the current total from the label text "Track X of Y"
|
||||||
|
parts = self.track_position_label.text().split(' of ')
|
||||||
|
if len(parts) == 2:
|
||||||
|
displayed_total = int(parts[1])
|
||||||
|
if live_total != displayed_total:
|
||||||
|
# If the total has changed, refresh the navigation state
|
||||||
|
self.update_navigation_state()
|
||||||
|
else:
|
||||||
|
# If the label is not in the expected format, update it anyway
|
||||||
|
self.update_navigation_state()
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
# Handle cases where the label text is not yet set or in an unexpected format
|
||||||
|
self.update_navigation_state()
|
||||||
|
|
||||||
|
|
||||||
|
def _update_track_list(self):
|
||||||
|
"""
|
||||||
|
Syncs the modal's internal track list with the parent's live list,
|
||||||
|
preserving the user's current position.
|
||||||
|
"""
|
||||||
|
live_failed_tracks = self.parent_modal.permanently_failed_tracks
|
||||||
|
|
||||||
|
current_track_id = None
|
||||||
|
if self.current_track_info:
|
||||||
|
current_track_id = self.current_track_info.get('download_index')
|
||||||
|
|
||||||
|
self.failed_tracks = list(live_failed_tracks)
|
||||||
|
|
||||||
|
if not self.failed_tracks:
|
||||||
|
return
|
||||||
|
|
||||||
|
new_index = -1
|
||||||
|
if current_track_id is not None:
|
||||||
|
for i, track in enumerate(self.failed_tracks):
|
||||||
|
if track.get('download_index') == current_track_id:
|
||||||
|
new_index = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if new_index != -1:
|
||||||
|
self.current_track_index = new_index
|
||||||
|
else:
|
||||||
|
# If the current track was resolved, stay at the same index
|
||||||
|
# but check bounds against the new list length.
|
||||||
|
if self.current_track_index >= len(self.failed_tracks):
|
||||||
|
self.current_track_index = len(self.failed_tracks) - 1
|
||||||
|
|
||||||
|
if self.current_track_index < 0:
|
||||||
|
self.current_track_index = 0
|
||||||
|
|
||||||
def load_current_track(self):
|
def load_current_track(self):
|
||||||
"""Load the track at current_track_index"""
|
"""Loads the current failed track's info and intelligently triggers a search."""
|
||||||
|
self.cancel_current_search()
|
||||||
self.clear_results()
|
self.clear_results()
|
||||||
|
|
||||||
|
# Sync with the parent modal's live list of failed tracks
|
||||||
|
self._update_track_list()
|
||||||
|
|
||||||
if not self.failed_tracks:
|
if not self.failed_tracks:
|
||||||
QMessageBox.information(self, "Complete", "All failed tracks have been addressed.")
|
QMessageBox.information(self, "Complete", "All failed tracks have been addressed.")
|
||||||
self.accept()
|
self.accept()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Update navigation buttons and position label
|
|
||||||
self.update_navigation_state()
|
self.update_navigation_state()
|
||||||
|
|
||||||
self.current_track_info = self.failed_tracks[self.current_track_index]
|
self.current_track_info = self.failed_tracks[self.current_track_index]
|
||||||
|
|
@ -2070,95 +2185,123 @@ class ManualMatchModal(QDialog):
|
||||||
self.info_label.setText(f"Could not find: <b>{spotify_track.name}</b><br>by {artist}")
|
self.info_label.setText(f"Could not find: <b>{spotify_track.name}</b><br>by {artist}")
|
||||||
self.search_input.setText(f"{artist} {spotify_track.name}")
|
self.search_input.setText(f"{artist} {spotify_track.name}")
|
||||||
|
|
||||||
# Display cached results first, as requested
|
self.search_delay_timer.start(1000)
|
||||||
cached_candidates = self.current_track_info.get('candidates', [])
|
|
||||||
if cached_candidates:
|
|
||||||
self.results_layout.addWidget(QLabel("Showing results from initial search. Or, perform a new search above."))
|
|
||||||
for result in cached_candidates:
|
|
||||||
self.results_layout.addWidget(self.create_result_widget(result))
|
|
||||||
else:
|
|
||||||
self.perform_manual_search() # If no cache, search automatically
|
|
||||||
|
|
||||||
def load_next_track(self):
|
def load_next_track(self):
|
||||||
"""Navigate to next track"""
|
"""Navigate to the next failed track."""
|
||||||
if self.current_track_index < len(self.failed_tracks) - 1:
|
if self.current_track_index < len(self.parent_modal.permanently_failed_tracks) - 1:
|
||||||
self.current_track_index += 1
|
self.current_track_index += 1
|
||||||
self.load_current_track()
|
self.load_current_track()
|
||||||
|
|
||||||
def load_previous_track(self):
|
def load_previous_track(self):
|
||||||
"""Navigate to previous track"""
|
"""Navigate to the previous failed track."""
|
||||||
if self.current_track_index > 0:
|
if self.current_track_index > 0:
|
||||||
self.current_track_index -= 1
|
self.current_track_index -= 1
|
||||||
self.load_current_track()
|
self.load_current_track()
|
||||||
|
|
||||||
def update_navigation_state(self):
|
def update_navigation_state(self):
|
||||||
"""Update navigation buttons and position label"""
|
"""Update the 'Track X of Y' label and enable/disable nav buttons."""
|
||||||
total_tracks = len(self.failed_tracks)
|
total_tracks = len(self.parent_modal.permanently_failed_tracks)
|
||||||
current_pos = self.current_track_index + 1
|
|
||||||
|
# Ensure current_track_index is valid even if list shrinks
|
||||||
|
if self.current_track_index >= total_tracks:
|
||||||
|
self.current_track_index = max(0, total_tracks - 1)
|
||||||
|
|
||||||
|
current_pos = self.current_track_index + 1 if total_tracks > 0 else 0
|
||||||
|
|
||||||
self.track_position_label.setText(f"Track {current_pos} of {total_tracks}")
|
self.track_position_label.setText(f"Track {current_pos} of {total_tracks}")
|
||||||
|
|
||||||
# Enable/disable navigation buttons
|
|
||||||
self.prev_btn.setEnabled(self.current_track_index > 0)
|
self.prev_btn.setEnabled(self.current_track_index > 0)
|
||||||
self.next_btn.setEnabled(self.current_track_index < total_tracks - 1)
|
self.next_btn.setEnabled(self.current_track_index < total_tracks - 1)
|
||||||
|
|
||||||
def perform_manual_search(self):
|
def perform_manual_search(self):
|
||||||
|
"""Initiates a search for the current query, cancelling any existing search."""
|
||||||
|
self.search_delay_timer.stop()
|
||||||
|
self.cancel_current_search()
|
||||||
|
|
||||||
query = self.search_input.text().strip()
|
query = self.search_input.text().strip()
|
||||||
if not query: return
|
if not query: return
|
||||||
|
|
||||||
self.clear_results()
|
self.clear_results()
|
||||||
|
self.results_layout.addWidget(QLabel(f"<h3>Searching for '{query}'...</h3>"))
|
||||||
|
self.search_btn.hide()
|
||||||
|
self.cancel_search_btn.show()
|
||||||
|
|
||||||
self.results_layout.addWidget(QLabel(f"Searching for '{query}'..."))
|
self.search_worker = self.SearchWorker(self.soulseek_client, query)
|
||||||
self.search_btn.setText("Searching...")
|
self.search_worker.signals.completed.connect(self.on_manual_search_completed)
|
||||||
self.search_btn.setEnabled(False)
|
self.search_worker.signals.failed.connect(self.on_manual_search_failed)
|
||||||
|
self.thread_pool.start(self.search_worker)
|
||||||
|
|
||||||
worker = self.parent_modal.start_search_worker_parallel(
|
def cancel_current_search(self):
|
||||||
query, [query], self.current_track_info['spotify_track'],
|
"""Stops the currently running search worker."""
|
||||||
self.current_track_info['track_index'], self.current_track_info['table_index'],
|
if self.search_worker:
|
||||||
0, self.current_track_info['download_index']
|
self.search_worker.cancel()
|
||||||
)
|
self.search_worker = None
|
||||||
if worker:
|
self.search_btn.show()
|
||||||
worker.signals.search_completed.connect(self.on_manual_search_completed)
|
self.cancel_search_btn.hide()
|
||||||
worker.signals.search_failed.connect(self.on_manual_search_failed)
|
|
||||||
else:
|
|
||||||
self.search_btn.setText("Search")
|
|
||||||
self.search_btn.setEnabled(True)
|
|
||||||
self.clear_results()
|
|
||||||
self.results_layout.addWidget(QLabel("Error: Could not start search worker"))
|
|
||||||
|
|
||||||
def on_manual_search_completed(self, results, query):
|
def on_manual_search_completed(self, results):
|
||||||
self.search_btn.setText("Search")
|
"""Handles successful search results."""
|
||||||
self.search_btn.setEnabled(True)
|
if not self.search_worker or self.search_worker.is_cancelled:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.cancel_current_search()
|
||||||
self.clear_results()
|
self.clear_results()
|
||||||
|
|
||||||
if not results:
|
if not results:
|
||||||
self.results_layout.addWidget(QLabel("No results found for this query."))
|
self.results_layout.addWidget(QLabel("<h3>No results found for this query.</h3>"))
|
||||||
return
|
return
|
||||||
|
|
||||||
for result in results:
|
for result in results:
|
||||||
self.results_layout.addWidget(self.create_result_widget(result))
|
self.results_layout.addWidget(self.create_result_widget(result))
|
||||||
|
|
||||||
def on_manual_search_failed(self, query, error):
|
def on_manual_search_failed(self, error):
|
||||||
self.search_btn.setText("Search")
|
"""Handles a failed search attempt."""
|
||||||
self.search_btn.setEnabled(True)
|
if not self.search_worker or self.search_worker.is_cancelled:
|
||||||
self.clear_results()
|
return
|
||||||
self.results_layout.addWidget(QLabel(f"Search failed: {error}"))
|
|
||||||
|
|
||||||
def create_result_widget(self, result):
|
self.cancel_current_search()
|
||||||
|
self.clear_results()
|
||||||
|
self.results_layout.addWidget(QLabel(f"<h3>Search failed:</h3><p>{error}</p>"))
|
||||||
|
|
||||||
|
def create_result_widget(self, result: TrackResult):
|
||||||
|
"""Creates a styled widget for a single search result."""
|
||||||
widget = QFrame()
|
widget = QFrame()
|
||||||
widget.setStyleSheet("background-color: #3a3a3a; border-radius: 6px; padding: 10px;")
|
widget.setStyleSheet("""
|
||||||
|
QFrame {
|
||||||
|
background-color: #3a3a3a;
|
||||||
|
border: 1px solid #555555;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
QFrame:hover {
|
||||||
|
border: 1px solid #1db954;
|
||||||
|
}
|
||||||
|
""")
|
||||||
layout = QHBoxLayout(widget)
|
layout = QHBoxLayout(widget)
|
||||||
|
|
||||||
# Display filename and path structure
|
|
||||||
path_parts = result.filename.replace('\\', '/').split('/')
|
path_parts = result.filename.replace('\\', '/').split('/')
|
||||||
filename = path_parts[-1]
|
filename = path_parts[-1]
|
||||||
path_structure = '/'.join(path_parts[:-1])
|
path_structure = '/'.join(path_parts[:-1])
|
||||||
|
|
||||||
info_text = f"<b>{filename}</b><br><i style='color:#aaaaaa;'>{path_structure}</i><br>Quality: {result.quality.upper()}, Size: {result.size // 1024} KB"
|
size_kb = result.size // 1024
|
||||||
|
info_text = (f"<b>{filename}</b><br>"
|
||||||
|
f"<i style='color:#aaaaaa;'>{path_structure}</i><br>"
|
||||||
|
f"Quality: <b>{result.quality.upper()}</b>, "
|
||||||
|
f"Size: <b>{size_kb:,} KB</b>, "
|
||||||
|
f"User: <b>{result.username}</b>")
|
||||||
info_label = QLabel(info_text)
|
info_label = QLabel(info_text)
|
||||||
info_label.setWordWrap(True)
|
info_label.setWordWrap(True)
|
||||||
|
|
||||||
select_btn = QPushButton("Select")
|
select_btn = QPushButton("Select")
|
||||||
select_btn.setFixedWidth(100)
|
select_btn.setFixedWidth(100)
|
||||||
|
select_btn.setStyleSheet("""
|
||||||
|
QPushButton {
|
||||||
|
background-color: #1db954; color: #000000;
|
||||||
|
}
|
||||||
|
QPushButton:hover {
|
||||||
|
background-color: #1ed760;
|
||||||
|
}
|
||||||
|
""")
|
||||||
select_btn.clicked.connect(lambda: self.on_selection_made(result))
|
select_btn.clicked.connect(lambda: self.on_selection_made(result))
|
||||||
|
|
||||||
layout.addWidget(info_label, 1)
|
layout.addWidget(info_label, 1)
|
||||||
|
|
@ -2166,9 +2309,12 @@ class ManualMatchModal(QDialog):
|
||||||
return widget
|
return widget
|
||||||
|
|
||||||
def on_selection_made(self, slskd_result):
|
def on_selection_made(self, slskd_result):
|
||||||
|
"""
|
||||||
|
Handles user selecting a track. The parent modal removes the track from the
|
||||||
|
live list, and this modal will sync with that change on the next load.
|
||||||
|
"""
|
||||||
print(f"Manual selection made: {slskd_result.filename}")
|
print(f"Manual selection made: {slskd_result.filename}")
|
||||||
|
|
||||||
# This starts the download via the main modal's infrastructure
|
|
||||||
self.parent_modal.start_validated_download_parallel(
|
self.parent_modal.start_validated_download_parallel(
|
||||||
slskd_result,
|
slskd_result,
|
||||||
self.current_track_info['spotify_track'],
|
self.current_track_info['spotify_track'],
|
||||||
|
|
@ -2179,26 +2325,67 @@ class ManualMatchModal(QDialog):
|
||||||
|
|
||||||
self.track_resolved.emit(self.current_track_info)
|
self.track_resolved.emit(self.current_track_info)
|
||||||
|
|
||||||
# Remove the resolved track from the list
|
self.load_current_track()
|
||||||
self.failed_tracks.pop(self.current_track_index)
|
|
||||||
|
|
||||||
# Adjust current index if we removed the last track
|
|
||||||
if self.current_track_index >= len(self.failed_tracks) and self.current_track_index > 0:
|
|
||||||
self.current_track_index -= 1
|
|
||||||
|
|
||||||
# Load the current track (or close if no more tracks)
|
|
||||||
if self.failed_tracks:
|
|
||||||
self.load_current_track()
|
|
||||||
else:
|
|
||||||
QMessageBox.information(self, "Complete", "All failed tracks have been addressed.")
|
|
||||||
self.accept()
|
|
||||||
|
|
||||||
def clear_results(self):
|
def clear_results(self):
|
||||||
|
"""Removes all widgets from the results layout."""
|
||||||
while self.results_layout.count():
|
while self.results_layout.count():
|
||||||
child = self.results_layout.takeAt(0)
|
child = self.results_layout.takeAt(0)
|
||||||
if child.widget():
|
if child.widget():
|
||||||
child.widget().deleteLater()
|
child.widget().deleteLater()
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
"""Ensures any running search is cancelled when the modal is closed."""
|
||||||
|
self.cancel_current_search()
|
||||||
|
self.search_delay_timer.stop()
|
||||||
|
self.live_update_timer.stop() # Stop the live update timer
|
||||||
|
super().closeEvent(event)
|
||||||
|
|
||||||
|
# --- Inner classes for self-contained search worker ---
|
||||||
|
class SearchWorkerSignals(QObject):
|
||||||
|
completed = pyqtSignal(list)
|
||||||
|
failed = pyqtSignal(str)
|
||||||
|
|
||||||
|
class SearchWorker(QRunnable):
|
||||||
|
def __init__(self, soulseek_client, query):
|
||||||
|
super().__init__()
|
||||||
|
self.soulseek_client = soulseek_client
|
||||||
|
self.query = query
|
||||||
|
self.signals = ManualMatchModal.SearchWorkerSignals()
|
||||||
|
self.is_cancelled = False
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
self.is_cancelled = True
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if self.is_cancelled:
|
||||||
|
return
|
||||||
|
|
||||||
|
loop = None
|
||||||
|
try:
|
||||||
|
import asyncio
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
search_result = loop.run_until_complete(self.soulseek_client.search(self.query))
|
||||||
|
|
||||||
|
if self.is_cancelled:
|
||||||
|
return
|
||||||
|
|
||||||
|
if isinstance(search_result, tuple) and len(search_result) >= 1:
|
||||||
|
results_list = search_result[0] if search_result[0] else []
|
||||||
|
else:
|
||||||
|
results_list = []
|
||||||
|
|
||||||
|
self.signals.completed.emit(results_list)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if not self.is_cancelled:
|
||||||
|
self.signals.failed.emit(str(e))
|
||||||
|
finally:
|
||||||
|
if loop:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
|
||||||
class DownloadMissingTracksModal(QDialog):
|
class DownloadMissingTracksModal(QDialog):
|
||||||
"""Enhanced modal for downloading missing tracks with live progress tracking"""
|
"""Enhanced modal for downloading missing tracks with live progress tracking"""
|
||||||
|
|
@ -5991,7 +6178,7 @@ class DownloadMissingTracksModal(QDialog):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create and show the modal
|
# Create and show the modal
|
||||||
manual_modal = ManualMatchModal(self.permanently_failed_tracks, self)
|
manual_modal = ManualMatchModal(self)
|
||||||
manual_modal.track_resolved.connect(self.on_manual_match_resolved)
|
manual_modal.track_resolved.connect(self.on_manual_match_resolved)
|
||||||
manual_modal.exec()
|
manual_modal.exec()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue