This commit is contained in:
Broque Thomas 2025-07-23 18:00:01 -07:00
parent 0021fc52dd
commit f3dd76feae
4 changed files with 41963 additions and 103 deletions

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -1953,76 +1953,131 @@ class SyncPage(QWidget):
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)
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)
self.parent_modal = parent_modal
self.soulseek_client = parent_modal.parent_page.soulseek_client
self.downloads_page = parent_modal.downloads_page
self.failed_tracks = list(failed_tracks) # Use a copy of the list
self.current_track_index = 0 # Track current position
self.failed_tracks = []
self.current_track_index = 0
self.current_track_info = None
self.search_worker = None
self.setWindowTitle("Manual Track Correction")
self.setMinimumSize(900, 700)
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.setup_ui()
self.load_current_track()
def setup_ui(self):
"""Set up the visually redesigned UI."""
self.setWindowTitle("Manual Track Correction")
self.setMinimumSize(900, 700)
self.setStyleSheet("""
QDialog { background-color: #1e1e1e; }
QDialog { background-color: #1e1e1e; color: #ffffff; }
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 {
background: #404040; border: 1px solid #606060; border-radius: 6px;
padding: 10px; color: #ffffff; font-size: 13px;
background-color: #3a3a3a;
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.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)
self.info_label = QLabel("Loading track...")
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)
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.setPlaceholderText("Enter a new search query or use the suggestion...")
self.search_input.returnPressed.connect(self.perform_manual_search)
self.search_btn = QPushButton("Search")
self.search_btn.clicked.connect(self.perform_manual_search)
search_layout.addWidget(self.search_input)
search_layout.addWidget(self.search_btn)
self.main_layout.addLayout(search_layout)
self.search_btn.setStyleSheet("""
QPushButton {
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.setWidgetResizable(True)
self.results_widget = QWidget()
self.results_widget.setObjectName("resultsWidget")
self.results_layout = QVBoxLayout(self.results_widget)
self.results_layout.setSpacing(8)
self.results_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
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()
self.prev_btn = QPushButton("← Previous")
self.prev_btn.clicked.connect(self.load_previous_track)
@ -2034,15 +2089,14 @@ class ManualMatchModal(QDialog):
self.close_btn = QPushButton("Close")
self.close_btn.setStyleSheet("""
QPushButton {
background-color: #616161; color: #ffffff;
}
QPushButton:hover {
background-color: #757575;
}
QPushButton { background-color: #616161; color: #ffffff; }
QPushButton:hover { background-color: #757575; }
""")
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.addStretch()
nav_layout.addWidget(self.track_position_label)
@ -2050,17 +2104,78 @@ class ManualMatchModal(QDialog):
nav_layout.addWidget(self.next_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):
"""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()
# Sync with the parent modal's live list of failed tracks
self._update_track_list()
if not self.failed_tracks:
QMessageBox.information(self, "Complete", "All failed tracks have been addressed.")
self.accept()
return
# Update navigation buttons and position label
self.update_navigation_state()
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.search_input.setText(f"{artist} {spotify_track.name}")
# Display cached results first, as requested
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
self.search_delay_timer.start(1000)
def load_next_track(self):
"""Navigate to next track"""
if self.current_track_index < len(self.failed_tracks) - 1:
"""Navigate to the next failed track."""
if self.current_track_index < len(self.parent_modal.permanently_failed_tracks) - 1:
self.current_track_index += 1
self.load_current_track()
def load_previous_track(self):
"""Navigate to previous track"""
"""Navigate to the previous failed track."""
if self.current_track_index > 0:
self.current_track_index -= 1
self.load_current_track()
def update_navigation_state(self):
"""Update navigation buttons and position label"""
total_tracks = len(self.failed_tracks)
current_pos = self.current_track_index + 1
"""Update the 'Track X of Y' label and enable/disable nav buttons."""
total_tracks = len(self.parent_modal.permanently_failed_tracks)
# 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}")
# Enable/disable navigation buttons
self.prev_btn.setEnabled(self.current_track_index > 0)
self.next_btn.setEnabled(self.current_track_index < total_tracks - 1)
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()
if not query: return
self.clear_results()
self.results_layout.addWidget(QLabel(f"Searching for '{query}'..."))
self.search_btn.setText("Searching...")
self.search_btn.setEnabled(False)
self.results_layout.addWidget(QLabel(f"<h3>Searching for '{query}'...</h3>"))
self.search_btn.hide()
self.cancel_search_btn.show()
worker = self.parent_modal.start_search_worker_parallel(
query, [query], self.current_track_info['spotify_track'],
self.current_track_info['track_index'], self.current_track_info['table_index'],
0, self.current_track_info['download_index']
)
if worker:
worker.signals.search_completed.connect(self.on_manual_search_completed)
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"))
self.search_worker = self.SearchWorker(self.soulseek_client, query)
self.search_worker.signals.completed.connect(self.on_manual_search_completed)
self.search_worker.signals.failed.connect(self.on_manual_search_failed)
self.thread_pool.start(self.search_worker)
def on_manual_search_completed(self, results, query):
self.search_btn.setText("Search")
self.search_btn.setEnabled(True)
def cancel_current_search(self):
"""Stops the currently running search worker."""
if self.search_worker:
self.search_worker.cancel()
self.search_worker = None
self.search_btn.show()
self.cancel_search_btn.hide()
def on_manual_search_completed(self, results):
"""Handles successful search results."""
if not self.search_worker or self.search_worker.is_cancelled:
return
self.cancel_current_search()
self.clear_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
for result in results:
self.results_layout.addWidget(self.create_result_widget(result))
def on_manual_search_failed(self, query, error):
self.search_btn.setText("Search")
self.search_btn.setEnabled(True)
self.clear_results()
self.results_layout.addWidget(QLabel(f"Search failed: {error}"))
def on_manual_search_failed(self, error):
"""Handles a failed search attempt."""
if not self.search_worker or self.search_worker.is_cancelled:
return
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.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)
# Display filename and path structure
path_parts = result.filename.replace('\\', '/').split('/')
filename = 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.setWordWrap(True)
select_btn = QPushButton("Select")
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))
layout.addWidget(info_label, 1)
@ -2166,9 +2309,12 @@ class ManualMatchModal(QDialog):
return widget
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}")
# This starts the download via the main modal's infrastructure
self.parent_modal.start_validated_download_parallel(
slskd_result,
self.current_track_info['spotify_track'],
@ -2179,26 +2325,67 @@ class ManualMatchModal(QDialog):
self.track_resolved.emit(self.current_track_info)
# Remove the resolved track from the list
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()
self.load_current_track()
def clear_results(self):
"""Removes all widgets from the results layout."""
while self.results_layout.count():
child = self.results_layout.takeAt(0)
if child.widget():
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):
"""Enhanced modal for downloading missing tracks with live progress tracking"""
@ -5991,7 +6178,7 @@ class DownloadMissingTracksModal(QDialog):
return
# 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.exec()