diff --git a/ui/pages/__pycache__/dashboard.cpython-310.pyc b/ui/pages/__pycache__/dashboard.cpython-310.pyc index 5733fff..e27f866 100644 Binary files a/ui/pages/__pycache__/dashboard.cpython-310.pyc and b/ui/pages/__pycache__/dashboard.cpython-310.pyc differ diff --git a/ui/pages/__pycache__/dashboard.cpython-312.pyc b/ui/pages/__pycache__/dashboard.cpython-312.pyc index 7636d62..b761b68 100644 Binary files a/ui/pages/__pycache__/dashboard.cpython-312.pyc and b/ui/pages/__pycache__/dashboard.cpython-312.pyc differ diff --git a/ui/pages/dashboard.py b/ui/pages/dashboard.py index 607eb1b..c047fb8 100644 --- a/ui/pages/dashboard.py +++ b/ui/pages/dashboard.py @@ -296,6 +296,7 @@ class DownloadMissingWishlistTracksModal(QDialog): self.download_in_progress = False self.cancel_requested = False self.permanently_failed_tracks = [] + self.cancelled_tracks = set() # Track indices of cancelled tracks self.analysis_results = [] self.missing_tracks = [] self.active_workers = [] @@ -481,19 +482,22 @@ class DownloadMissingWishlistTracksModal(QDialog): layout.setContentsMargins(15, 15, 15, 15) self.track_table = QTableWidget() - # Change column count from 5 to 4 - self.track_table.setColumnCount(4) - # Remove "Duration" from the labels - self.track_table.setHorizontalHeaderLabels(["Track", "Artist", "Matched", "Status"]) + # Change column count from 4 to 5 for Cancel column + self.track_table.setColumnCount(5) + # Add "Cancel" column (no Duration column) + self.track_table.setHorizontalHeaderLabels(["Track", "Artist", "Matched", "Status", "Cancel"]) - # Adjust resize modes for new column indices + # Adjust resize modes for column indices self.track_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) - self.track_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Interactive) # "Matched" is now column 2 + self.track_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Interactive) # "Matched" is column 2 + self.track_table.horizontalHeader().setSectionResizeMode(4, QHeaderView.ResizeMode.Fixed) # "Cancel" is column 4 self.track_table.setColumnWidth(2, 140) # Set width for "Matched" column + self.track_table.setColumnWidth(4, 70) # Set width for "Cancel" column self.track_table.setStyleSheet("QTableWidget { background-color: #3a3a3a; alternate-background-color: #424242; selection-background-color: #1db954; gridline-color: #555; color: #fff; border: 1px solid #555; font-size: 12px; } QHeaderView::section { background-color: #1db954; color: #000; font-weight: bold; font-size: 13px; padding: 12px 8px; border: none; } QTableWidget::item { padding: 12px 8px; border-bottom: 1px solid #4a4a4a; }") self.track_table.setAlternatingRowColors(True) self.track_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.track_table.verticalHeader().setDefaultSectionSize(50) self.track_table.verticalHeader().setVisible(False) layout.addWidget(self.track_table) @@ -519,7 +523,16 @@ class DownloadMissingWishlistTracksModal(QDialog): status_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.track_table.setItem(i, 3, status_item) - # Loop over 4 columns instead of 5 + # Create empty container for cancel button (will be populated later for missing tracks only) + container = QWidget() + container.setStyleSheet("background: transparent;") + layout = QVBoxLayout(container) + layout.setContentsMargins(5, 5, 5, 5) + layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.track_table.setCellWidget(i, 4, container) + + # Loop over 4 columns instead of 5 (don't include cancel column) for col in range(4): item = self.track_table.item(i, col) if item: @@ -529,6 +542,107 @@ class DownloadMissingWishlistTracksModal(QDialog): if not duration_ms: return "0:00" seconds = duration_ms // 1000 return f"{seconds // 60}:{seconds % 60:02d}" + + def add_cancel_button_to_row(self, row): + """Add cancel button to a specific row (only for missing tracks)""" + container = self.track_table.cellWidget(row, 4) + if container and container.layout().count() == 0: # Only add if container is empty + cancel_button = QPushButton("×") + cancel_button.setFixedSize(20, 20) + cancel_button.setMinimumSize(20, 20) + cancel_button.setMaximumSize(20, 20) + cancel_button.setStyleSheet(""" + QPushButton { + background-color: #dc3545; + color: white; + border: 1px solid #c82333; + border-radius: 3px; + font-size: 14px; + font-weight: bold; + padding: 0px; + margin: 0px; + text-align: center; + min-width: 20px; + max-width: 20px; + width: 20px; + } + QPushButton:hover { + background-color: #c82333; + border-color: #bd2130; + } + QPushButton:pressed { + background-color: #bd2130; + border-color: #b21f2d; + } + QPushButton:disabled { + background-color: #28a745; + color: white; + border-color: #1e7e34; + } + """) + cancel_button.setFocusPolicy(Qt.FocusPolicy.NoFocus) + cancel_button.clicked.connect(lambda checked, row_idx=row: self.cancel_track(row_idx)) + + layout = container.layout() + layout.addWidget(cancel_button) + + def hide_cancel_button_for_row(self, row): + """Hide cancel button for a specific row (when track is downloaded)""" + container = self.track_table.cellWidget(row, 4) + if container: + layout = container.layout() + if layout and layout.count() > 0: + cancel_button = layout.itemAt(0).widget() + if cancel_button: + cancel_button.setVisible(False) + print(f"🫥 Hidden cancel button for downloaded track at row {row}") + + def cancel_track(self, row): + """Cancel a specific track - works at any phase""" + # Get cancel button and disable it + container = self.track_table.cellWidget(row, 4) + if container: + layout = container.layout() + if layout and layout.count() > 0: + cancel_button = layout.itemAt(0).widget() + if cancel_button: + cancel_button.setEnabled(False) + cancel_button.setText("✓") + + # Update status to cancelled (column 3 for dashboard) + self.track_table.setItem(row, 3, QTableWidgetItem("🚫 Cancelled")) + + # Add to cancelled tracks set + if not hasattr(self, 'cancelled_tracks'): + self.cancelled_tracks = set() + self.cancelled_tracks.add(row) + + track = self.wishlist_tracks[row] + print(f"🚫 Track cancelled: {track.name} (row {row})") + + # If downloads are active, also handle active download cancellation + download_index = None + + # Check active_downloads list + if hasattr(self, 'active_downloads'): + for download in self.active_downloads: + if download.get('table_index') == row: + download_index = download.get('download_index', row) + print(f"🚫 Found active download {download_index} for cancelled track") + break + + # Check parallel_search_tracking for download index + if download_index is None and hasattr(self, 'parallel_search_tracking'): + for idx, track_info in self.parallel_search_tracking.items(): + if track_info.get('table_index') == row: + download_index = idx + print(f"🚫 Found parallel tracking {download_index} for cancelled track") + break + + # If we found an active download, trigger completion to free up the worker + if download_index is not None and hasattr(self, 'on_parallel_track_completed'): + print(f"🚫 Triggering completion for active download {download_index}") + self.on_parallel_track_completed(download_index, success=False) def create_buttons(self): button_frame = QFrame(styleSheet="background-color: transparent; padding: 10px;") @@ -617,6 +731,7 @@ class DownloadMissingWishlistTracksModal(QDialog): def on_track_analyzed(self, track_index, result): self.analysis_progress.setValue(track_index) + row_index = track_index - 1 if result.exists_in_plex: matched_text = f"✅ Found ({result.confidence:.1f})" self.matched_tracks_count += 1 @@ -633,7 +748,9 @@ class DownloadMissingWishlistTracksModal(QDialog): matched_text = "❌ Missing" self.tracks_to_download_count += 1 self.download_count_label.setText(str(self.tracks_to_download_count)) - self.track_table.setItem(track_index - 1, 2, QTableWidgetItem(matched_text)) + # Add cancel button for missing tracks only + self.add_cancel_button_to_row(row_index) + self.track_table.setItem(row_index, 2, QTableWidgetItem(matched_text)) def on_analysis_completed(self, results): @@ -677,6 +794,13 @@ class DownloadMissingWishlistTracksModal(QDialog): track = track_result.spotify_track track_index = self.find_track_index_in_playlist(track) if track_index != -1: + # Skip if track was cancelled + if hasattr(self, 'cancelled_tracks') and track_index in self.cancelled_tracks: + print(f"🚫 Skipping cancelled track at index {track_index}: {track.name}") + self.download_queue_index += 1 + self.completed_downloads += 1 + continue + # FIX: Changed column index from 4 to 3 to target the "Status" column. self.track_table.setItem(track_index, 3, QTableWidgetItem("🔍 Searching...")) self.search_and_download_track_parallel(track, self.download_queue_index, track_index) @@ -852,11 +976,16 @@ class DownloadMissingWishlistTracksModal(QDialog): self.start_validated_download_parallel(next_candidate, track_info['spotify_track'], track_info['track_index'], track_info['table_index'], download_index) def on_parallel_track_completed(self, download_index, success): + if not hasattr(self, 'parallel_search_tracking'): + print(f"⚠️ parallel_search_tracking not initialized yet, skipping completion for download {download_index}") + return track_info = self.parallel_search_tracking.get(download_index) if not track_info or track_info.get('completed', False): return track_info['completed'] = True if success: self.track_table.setItem(track_info['table_index'], 3, QTableWidgetItem("✅ Downloaded")) + # Hide cancel button since track is now downloaded + self.hide_cancel_button_for_row(track_info['table_index']) self.downloaded_tracks_count += 1 self.downloaded_count_label.setText(str(self.downloaded_tracks_count)) self.successful_downloads += 1 @@ -866,10 +995,16 @@ class DownloadMissingWishlistTracksModal(QDialog): logger.info(f"Successfully downloaded and removed '{track_info['spotify_track'].name}' from wishlist.") else: - self.track_table.setItem(track_info['table_index'], 3, QTableWidgetItem("❌ Failed")) + # Check if track was cancelled (don't overwrite cancelled status) + table_index = track_info['table_index'] + current_status = self.track_table.item(table_index, 3) + if current_status and "🚫 Cancelled" in current_status.text(): + print(f"🔧 Track {download_index} was cancelled - preserving cancelled status") + else: + self.track_table.setItem(table_index, 3, QTableWidgetItem("❌ Failed")) + if track_info not in self.permanently_failed_tracks: + self.permanently_failed_tracks.append(track_info) self.failed_downloads += 1 - if track_info not in self.permanently_failed_tracks: - self.permanently_failed_tracks.append(track_info) self.update_failed_matches_button() self.completed_downloads += 1 self.active_parallel_downloads -= 1 @@ -911,6 +1046,44 @@ class DownloadMissingWishlistTracksModal(QDialog): if self.successful_downloads > 0 and hasattr(self.parent_dashboard, 'scan_manager') and self.parent_dashboard.scan_manager: self.parent_dashboard.scan_manager.request_scan(f"Wishlist download completed ({self.successful_downloads} tracks)") + # Add cancelled tracks that were missing from Plex to permanently_failed_tracks for wishlist re-addition + if hasattr(self, 'cancelled_tracks') and hasattr(self, 'missing_tracks'): + for cancelled_row in self.cancelled_tracks: + # Check if this cancelled track was actually missing from Plex + cancelled_track = self.wishlist_tracks[cancelled_row] + missing_track_result = None + + # Find the corresponding missing track result + for missing_result in self.missing_tracks: + if missing_result.spotify_track.id == cancelled_track.id: + missing_track_result = missing_result + break + + # Only add to wishlist if track was actually missing from Plex AND not successfully downloaded + if missing_track_result: + # Check if track was successfully downloaded (don't re-add downloaded tracks to wishlist) + status_item = self.track_table.item(cancelled_row, 3) + current_status = status_item.text() if status_item else "" + + if "✅ Downloaded" in current_status: + print(f"🚫 Cancelled track {cancelled_track.name} was already downloaded, skipping wishlist re-addition") + else: + cancelled_track_info = { + 'download_index': cancelled_row, + 'table_index': cancelled_row, + 'track': cancelled_track, + 'track_name': cancelled_track.name, + 'artist_name': cancelled_track.artists[0] if cancelled_track.artists else "Unknown", + 'retry_count': 0, + 'spotify_track': missing_track_result.spotify_track # Include the spotify track for wishlist + } + # Check if not already in permanently_failed_tracks + if not any(t.get('table_index') == cancelled_row for t in self.permanently_failed_tracks): + self.permanently_failed_tracks.append(cancelled_track_info) + print(f"🚫 Added cancelled missing track {cancelled_track.name} to failed list for wishlist re-addition") + else: + print(f"🚫 Cancelled track {cancelled_track.name} was not missing from Plex, skipping wishlist re-addition") + wishlist_added_count = 0 if self.permanently_failed_tracks: source_context = {'added_from': 'wishlist_modal', 'timestamp': datetime.now().isoformat()}