|
|
|
|
@ -0,0 +1,512 @@
|
|
|
|
|
# Cancel Button Implementation Guide
|
|
|
|
|
|
|
|
|
|
This document details the exact changes made to implement cancel functionality in the download modals.
|
|
|
|
|
|
|
|
|
|
## Overview
|
|
|
|
|
|
|
|
|
|
Added intelligent individual track cancellation functionality to sync.py that:
|
|
|
|
|
- **Smart button visibility**: Only shows cancel buttons for tracks missing from Plex
|
|
|
|
|
- **Dynamic UI updates**: Buttons appear during analysis and disappear when downloaded
|
|
|
|
|
- **Works at any phase**: Database check, downloads, post-download
|
|
|
|
|
- **Clean interface**: No unnecessary buttons on tracks that don't need downloading
|
|
|
|
|
- **Proper integration**: Seamlessly works with existing worker system
|
|
|
|
|
- **Defensive programming**: Prevents crashes in all scenarios
|
|
|
|
|
|
|
|
|
|
## Files Modified
|
|
|
|
|
|
|
|
|
|
### 1. `/ui/pages/sync.py` - DownloadMissingTracksModal
|
|
|
|
|
|
|
|
|
|
## Detailed Changes
|
|
|
|
|
|
|
|
|
|
### Change 1: Table Structure Extension
|
|
|
|
|
**Location**: `create_track_table()` method, lines ~4105-4113
|
|
|
|
|
|
|
|
|
|
**Before**:
|
|
|
|
|
```python
|
|
|
|
|
self.track_table.setColumnCount(5)
|
|
|
|
|
self.track_table.setHorizontalHeaderLabels(["Track", "Artist", "Duration", "Matched", "Status"])
|
|
|
|
|
self.track_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
|
|
|
|
self.track_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Interactive)
|
|
|
|
|
self.track_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Interactive)
|
|
|
|
|
self.track_table.setColumnWidth(2, 90)
|
|
|
|
|
self.track_table.setColumnWidth(3, 140)
|
|
|
|
|
self.track_table.verticalHeader().setDefaultSectionSize(35)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**After**:
|
|
|
|
|
```python
|
|
|
|
|
self.track_table.setColumnCount(6)
|
|
|
|
|
self.track_table.setHorizontalHeaderLabels(["Track", "Artist", "Duration", "Matched", "Status", "Cancel"])
|
|
|
|
|
self.track_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
|
|
|
|
self.track_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Interactive)
|
|
|
|
|
self.track_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Interactive)
|
|
|
|
|
self.track_table.horizontalHeader().setSectionResizeMode(5, QHeaderView.ResizeMode.Fixed)
|
|
|
|
|
self.track_table.setColumnWidth(2, 90)
|
|
|
|
|
self.track_table.setColumnWidth(3, 140)
|
|
|
|
|
self.track_table.setColumnWidth(5, 70)
|
|
|
|
|
self.track_table.verticalHeader().setDefaultSectionSize(50)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Purpose**: Extends table from 5 to 6 columns, adds "Cancel" header, sets fixed 70px width for cancel column, increases row height to 50px to accommodate centered buttons.
|
|
|
|
|
|
|
|
|
|
### Change 2: State Tracking Addition
|
|
|
|
|
**Location**: `__init__()` method, after line ~3817
|
|
|
|
|
|
|
|
|
|
**Added**:
|
|
|
|
|
```python
|
|
|
|
|
self.cancelled_tracks = set() # Track indices of cancelled tracks
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Purpose**: Simple set to track which row indices have been cancelled.
|
|
|
|
|
|
|
|
|
|
### Change 3: Smart Cancel Button Container Creation
|
|
|
|
|
**Location**: `populate_track_table()` method, after status item creation
|
|
|
|
|
|
|
|
|
|
**Added**:
|
|
|
|
|
```python
|
|
|
|
|
# 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, 5, container)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Purpose**: Creates empty containers that will only get cancel buttons for tracks that are missing from Plex, keeping the UI clean.
|
|
|
|
|
|
|
|
|
|
### Change 4: Conditional Cancel Button Addition
|
|
|
|
|
**Location**: `on_track_analyzed()` method
|
|
|
|
|
|
|
|
|
|
**Before**:
|
|
|
|
|
```python
|
|
|
|
|
def on_track_analyzed(self, track_index, result):
|
|
|
|
|
"""Handle individual track analysis completion with live UI updates"""
|
|
|
|
|
self.analysis_progress.setValue(track_index)
|
|
|
|
|
if result.exists_in_plex:
|
|
|
|
|
matched_text = f"✅ Found ({result.confidence:.1f})"
|
|
|
|
|
self.matched_tracks_count += 1
|
|
|
|
|
self.matched_count_label.setText(str(self.matched_tracks_count))
|
|
|
|
|
else:
|
|
|
|
|
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, 3, QTableWidgetItem(matched_text))
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**After**:
|
|
|
|
|
```python
|
|
|
|
|
def on_track_analyzed(self, track_index, result):
|
|
|
|
|
"""Handle individual track analysis completion with live UI updates"""
|
|
|
|
|
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
|
|
|
|
|
self.matched_count_label.setText(str(self.matched_tracks_count))
|
|
|
|
|
else:
|
|
|
|
|
matched_text = "❌ Missing"
|
|
|
|
|
self.tracks_to_download_count += 1
|
|
|
|
|
self.download_count_label.setText(str(self.tracks_to_download_count))
|
|
|
|
|
# Add cancel button for missing tracks only
|
|
|
|
|
self.add_cancel_button_to_row(row_index)
|
|
|
|
|
self.track_table.setItem(row_index, 3, QTableWidgetItem(matched_text))
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Purpose**: Only adds cancel buttons to tracks that are missing from Plex during real-time analysis.
|
|
|
|
|
|
|
|
|
|
### Change 5: Dynamic Cancel Button Creation Method
|
|
|
|
|
**Location**: After `format_duration()` method
|
|
|
|
|
|
|
|
|
|
**Added Complete Method**:
|
|
|
|
|
```python
|
|
|
|
|
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, 5)
|
|
|
|
|
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)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Purpose**: Creates perfect 20x20px square red "×" buttons only for missing tracks, properly styled and connected.
|
|
|
|
|
|
|
|
|
|
### Change 6: Cancel Button Hiding for Downloaded Tracks
|
|
|
|
|
**Location**: After `add_cancel_button_to_row()` method
|
|
|
|
|
|
|
|
|
|
**Added Complete Method**:
|
|
|
|
|
```python
|
|
|
|
|
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, 5)
|
|
|
|
|
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}")
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Purpose**: Hides cancel buttons when tracks are successfully downloaded since cancellation is no longer relevant.
|
|
|
|
|
|
|
|
|
|
### Change 7: Download Queue Integration
|
|
|
|
|
**Location**: `start_next_batch_of_downloads()` method, inside the while loop
|
|
|
|
|
|
|
|
|
|
**Added**:
|
|
|
|
|
```python
|
|
|
|
|
# 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
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Purpose**: Prevents cancelled tracks from entering the download queue, automatically skips them.
|
|
|
|
|
|
|
|
|
|
### Change 8: Integration with Download Completion
|
|
|
|
|
**Location**: `on_parallel_track_completed()` method, success section
|
|
|
|
|
|
|
|
|
|
**Before**:
|
|
|
|
|
```python
|
|
|
|
|
if success:
|
|
|
|
|
print(f"🔧 Track {download_index} completed successfully - updating table index {track_info['table_index']} to '✅ Downloaded'")
|
|
|
|
|
self.track_table.setItem(track_info['table_index'], 4, QTableWidgetItem("✅ Downloaded"))
|
|
|
|
|
self.downloaded_tracks_count += 1
|
|
|
|
|
self.downloaded_count_label.setText(str(self.downloaded_tracks_count))
|
|
|
|
|
self.successful_downloads += 1
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**After**:
|
|
|
|
|
```python
|
|
|
|
|
if success:
|
|
|
|
|
print(f"🔧 Track {download_index} completed successfully - updating table index {track_info['table_index']} to '✅ Downloaded'")
|
|
|
|
|
self.track_table.setItem(track_info['table_index'], 4, 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
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Purpose**: Automatically hides cancel buttons when tracks are successfully downloaded.
|
|
|
|
|
|
|
|
|
|
### Change 9: Cancel Track Method
|
|
|
|
|
**Location**: After `format_duration()` method
|
|
|
|
|
|
|
|
|
|
**Added Complete Method**:
|
|
|
|
|
```python
|
|
|
|
|
def cancel_track(self, row):
|
|
|
|
|
"""Cancel a specific track - works at any phase"""
|
|
|
|
|
# Get cancel button and disable it
|
|
|
|
|
cancel_button = self.track_table.cellWidget(row, 5)
|
|
|
|
|
if cancel_button:
|
|
|
|
|
cancel_button.setEnabled(False)
|
|
|
|
|
cancel_button.setText("✓")
|
|
|
|
|
cancel_button.setStyleSheet("""
|
|
|
|
|
QPushButton {
|
|
|
|
|
background-color: #28a745; color: white; border: none;
|
|
|
|
|
border-radius: 8px; font-size: 10px; font-weight: bold;
|
|
|
|
|
}
|
|
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
# Update status to cancelled
|
|
|
|
|
self.track_table.setItem(row, 4, QTableWidgetItem("🚫 Cancelled"))
|
|
|
|
|
|
|
|
|
|
# Add to cancelled tracks set
|
|
|
|
|
if not hasattr(self, 'cancelled_tracks'):
|
|
|
|
|
self.cancelled_tracks = set()
|
|
|
|
|
self.cancelled_tracks.add(row)
|
|
|
|
|
|
|
|
|
|
track = self.playlist.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)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Purpose**: Main cancellation logic that works at any phase, handles UI updates, state management, and **properly cancels active workers**.
|
|
|
|
|
|
|
|
|
|
### Change 6: Defensive Programming for Completion Handler
|
|
|
|
|
**Location**: `on_parallel_track_completed()` method, beginning
|
|
|
|
|
|
|
|
|
|
**Added**:
|
|
|
|
|
```python
|
|
|
|
|
if not hasattr(self, 'parallel_search_tracking'):
|
|
|
|
|
print(f"⚠️ parallel_search_tracking not initialized yet, skipping completion for download {download_index}")
|
|
|
|
|
return
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Purpose**: Prevents AttributeError when cancel is called before downloads start.
|
|
|
|
|
|
|
|
|
|
### Change 7: Preserve Cancelled Status in Completion Handler
|
|
|
|
|
**Location**: `on_parallel_track_completed()` method, failure handling section
|
|
|
|
|
|
|
|
|
|
**Before**:
|
|
|
|
|
```python
|
|
|
|
|
else:
|
|
|
|
|
print(f"🔧 Track {download_index} failed - updating table index {track_info['table_index']} to '❌ Failed'")
|
|
|
|
|
self.track_table.setItem(track_info['table_index'], 4, QTableWidgetItem("❌ Failed"))
|
|
|
|
|
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()
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**After**:
|
|
|
|
|
```python
|
|
|
|
|
else:
|
|
|
|
|
# Check if track was cancelled (don't overwrite cancelled status)
|
|
|
|
|
table_index = track_info['table_index']
|
|
|
|
|
current_status = self.track_table.item(table_index, 4)
|
|
|
|
|
if current_status and "🚫 Cancelled" in current_status.text():
|
|
|
|
|
print(f"🔧 Track {download_index} was cancelled - preserving cancelled status")
|
|
|
|
|
else:
|
|
|
|
|
print(f"🔧 Track {download_index} failed - updating table index {table_index} to '❌ Failed'")
|
|
|
|
|
self.track_table.setItem(table_index, 4, QTableWidgetItem("❌ Failed"))
|
|
|
|
|
if track_info not in self.permanently_failed_tracks:
|
|
|
|
|
self.permanently_failed_tracks.append(track_info)
|
|
|
|
|
self.update_failed_matches_button()
|
|
|
|
|
self.failed_downloads += 1
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Purpose**: **CRITICAL FIX** - Prevents cancelled tracks from having their status overwritten with "❌ Failed" when completion handler is triggered.
|
|
|
|
|
|
|
|
|
|
### Change 8: Smart Wishlist Integration for Cancelled Tracks
|
|
|
|
|
**Location**: `on_all_downloads_complete()` method, before wishlist processing
|
|
|
|
|
|
|
|
|
|
**Added**:
|
|
|
|
|
```python
|
|
|
|
|
# Add cancelled tracks that were missing from Plex to permanently_failed_tracks for wishlist inclusion
|
|
|
|
|
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.playlist.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 add downloaded tracks to wishlist)
|
|
|
|
|
status_item = self.track_table.item(cancelled_row, 4)
|
|
|
|
|
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 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")
|
|
|
|
|
else:
|
|
|
|
|
print(f"🚫 Cancelled track {cancelled_track.name} was not missing from Plex, skipping wishlist addition")
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Purpose**: **COMPLETE LOGIC** - Ensures only cancelled tracks that were missing from Plex AND not successfully downloaded get added to wishlist. Prevents downloaded tracks from being added to wishlist even if cancelled after download.
|
|
|
|
|
|
|
|
|
|
## Key Design Decisions
|
|
|
|
|
|
|
|
|
|
### 1. **Simple State Tracking**
|
|
|
|
|
- Uses `set()` for cancelled track indices instead of complex tracking
|
|
|
|
|
- Integrates at queue entry point for immediate effect
|
|
|
|
|
- No need for complex worker cancellation logic
|
|
|
|
|
|
|
|
|
|
### 2. **Timeline-Agnostic Design**
|
|
|
|
|
- Works whether called during database check, download phase, or after
|
|
|
|
|
- Uses defensive `hasattr()` checks throughout
|
|
|
|
|
- Gracefully handles missing attributes
|
|
|
|
|
|
|
|
|
|
### 3. **Visual Feedback**
|
|
|
|
|
- Button changes from red "×" to green "✓" when clicked
|
|
|
|
|
- Status updates to "🚫 Cancelled" immediately
|
|
|
|
|
- Button becomes disabled to prevent double-clicking
|
|
|
|
|
|
|
|
|
|
### 4. **Smart Button Visibility**
|
|
|
|
|
- Buttons only appear for tracks missing from Plex ("❌ Missing")
|
|
|
|
|
- Buttons automatically hide when tracks are downloaded ("✅ Downloaded")
|
|
|
|
|
- Clean UI with no unnecessary buttons on found tracks
|
|
|
|
|
- Dynamic updates during real-time analysis
|
|
|
|
|
|
|
|
|
|
### 5. **Size Optimization**
|
|
|
|
|
- 20x20px buttons with proper centering
|
|
|
|
|
- 70px column width for Cancel column
|
|
|
|
|
- 50px row height to accommodate centered buttons
|
|
|
|
|
- Fixed column sizing to prevent layout issues
|
|
|
|
|
|
|
|
|
|
## Expected Behavior
|
|
|
|
|
|
|
|
|
|
### **Button Visibility Logic**
|
|
|
|
|
- ✅ **Found tracks**: No cancel button (already exists in Plex)
|
|
|
|
|
- ❌ **Missing tracks**: Cancel button appears during analysis
|
|
|
|
|
- ⏳ **Downloading tracks**: Cancel button remains visible and functional
|
|
|
|
|
- ✅ **Downloaded tracks**: Cancel button automatically disappears
|
|
|
|
|
- 🚫 **Cancelled tracks**: Button changes to green "✓" and becomes disabled
|
|
|
|
|
- ❌ **Failed tracks**: Cancel button remains for potential retry cancellation
|
|
|
|
|
|
|
|
|
|
### **Cancellation Behavior**
|
|
|
|
|
1. **Before downloads start**: Track added to cancelled set, skipped when queue processes
|
|
|
|
|
2. **During downloads**: Active download cancelled, worker immediately moves to next track, cancelled status preserved
|
|
|
|
|
3. **After downloads**: Visual cancellation only (track already processed)
|
|
|
|
|
4. **When downloaded**: Cancel button automatically hidden (nothing left to cancel)
|
|
|
|
|
|
|
|
|
|
## Critical Issue Resolution
|
|
|
|
|
|
|
|
|
|
### **Problem 1**: Active Worker Cancellation
|
|
|
|
|
- Initial implementation skipped future tracks but didn't cancel active workers
|
|
|
|
|
- Workers would continue processing cancelled tracks instead of moving to next available track
|
|
|
|
|
- Cancelled status would be overwritten with "❌ Failed"
|
|
|
|
|
|
|
|
|
|
### **Solution 1**: Completion Flow Integration
|
|
|
|
|
- Enhanced download detection to find active `download_index`
|
|
|
|
|
- Trigger `on_parallel_track_completed(download_index, success=False)` to:
|
|
|
|
|
- Decrement `active_parallel_downloads` counter
|
|
|
|
|
- Call `start_next_batch_of_downloads()` to continue queue
|
|
|
|
|
- Preserve "🚫 Cancelled" status instead of overwriting with "❌ Failed"
|
|
|
|
|
|
|
|
|
|
### **Problem 2**: Cancelled Tracks Not Added to Wishlist
|
|
|
|
|
- Cancelled tracks were being skipped completely and not added to wishlist for future retry
|
|
|
|
|
- Only tracks that actually failed during download processing were being added to wishlist
|
|
|
|
|
- Users expected cancelled tracks that were missing from Plex to be retryable later
|
|
|
|
|
|
|
|
|
|
### **Solution 2**: Smart Wishlist Integration
|
|
|
|
|
- At completion time, cross-reference `cancelled_tracks` with `missing_tracks`
|
|
|
|
|
- Check track status to determine if it was successfully downloaded
|
|
|
|
|
- Only add cancelled tracks that were missing from Plex AND not successfully downloaded
|
|
|
|
|
- Skip cancelled tracks that already exist in Plex (no point retrying those)
|
|
|
|
|
- Skip cancelled tracks that were successfully downloaded (already have the file)
|
|
|
|
|
- Include proper `spotify_track` reference needed by wishlist system
|
|
|
|
|
- Existing wishlist logic then processes all failed tracks (including cancelled ones)
|
|
|
|
|
|
|
|
|
|
## No AttributeErrors
|
|
|
|
|
|
|
|
|
|
All potential crashes prevented by:
|
|
|
|
|
- `hasattr()` checks before accessing attributes
|
|
|
|
|
- Defensive initialization in cancel_track method
|
|
|
|
|
- Early returns in completion handler
|
|
|
|
|
|
|
|
|
|
## Testing Scenarios - All Working ✅
|
|
|
|
|
|
|
|
|
|
1. ✅ **Smart button visibility** → Only missing tracks get cancel buttons
|
|
|
|
|
2. ✅ **Button appears during analysis** → Real-time button addition for missing tracks
|
|
|
|
|
3. ✅ **Button hidden when downloaded** → Automatic removal when status = "✅ Downloaded"
|
|
|
|
|
4. ✅ **No buttons on found tracks** → Clean UI for tracks already in Plex
|
|
|
|
|
5. ✅ **Cancel during database check** → Track skipped when downloads start
|
|
|
|
|
6. ✅ **Cancel during active download** → Worker immediately moves to next track
|
|
|
|
|
7. ✅ **Cancel multiple tracks rapidly** → All handled correctly
|
|
|
|
|
8. ✅ **Workers continue after cancellation** → Queue proceeds automatically
|
|
|
|
|
9. ✅ **Button states and UI updates** → Proper visual feedback
|
|
|
|
|
10. ✅ **No crashes in any scenario** → Defensive programming prevents errors
|
|
|
|
|
11. ✅ **Status preservation** → "🚫 Cancelled" status maintained, not overwritten
|
|
|
|
|
12. ✅ **Wishlist integration** → Cancelled tracks missing from Plex added to wishlist
|
|
|
|
|
13. ✅ **Smart wishlist filtering** → Downloaded tracks not added to wishlist
|
|
|
|
|
|
|
|
|
|
### **Debug Output for Active Cancellation**:
|
|
|
|
|
```
|
|
|
|
|
🚫 Track cancelled: [Track Name] (row X)
|
|
|
|
|
🚫 Found parallel tracking Y for cancelled track
|
|
|
|
|
🚫 Triggering completion for active download Y
|
|
|
|
|
🔧 Track Y was cancelled - preserving cancelled status
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### **Debug Output for Smart Button Management**:
|
|
|
|
|
```
|
|
|
|
|
🫥 Hidden cancel button for downloaded track at row 3
|
|
|
|
|
🫥 Hidden cancel button for downloaded track at row 7
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### **Debug Output for Wishlist Integration**:
|
|
|
|
|
```
|
|
|
|
|
🚫 Added cancelled missing track Summer Rain to failed list for wishlist
|
|
|
|
|
🚫 Cancelled track Already Have This was not missing from Plex, skipping wishlist addition
|
|
|
|
|
🚫 Cancelled track Downloaded Song was already downloaded, skipping wishlist addition
|
|
|
|
|
✨ Added 3 failed tracks to wishlist for automatic retry.
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Replication Instructions
|
|
|
|
|
|
|
|
|
|
To replicate this exact implementation:
|
|
|
|
|
|
|
|
|
|
1. Follow changes in order listed above
|
|
|
|
|
2. Use exact code snippets provided
|
|
|
|
|
3. Test after each major change
|
|
|
|
|
4. Verify Python syntax with `python3 -m py_compile ui/pages/sync.py`
|
|
|
|
|
|
|
|
|
|
## Extension to Other Modals
|
|
|
|
|
|
|
|
|
|
This same pattern can be applied to:
|
|
|
|
|
- `artists.py` - DownloadMissingAlbumTracksModal
|
|
|
|
|
- `dashboard.py` - DownloadMissingWishlistTracksModal
|
|
|
|
|
|
|
|
|
|
Adjust column indices and track access patterns as needed for each modal's structure.
|