diff --git a/core/__pycache__/database_update_worker.cpython-312.pyc b/core/__pycache__/database_update_worker.cpython-312.pyc
index 0372f5ce..0b310c70 100644
Binary files a/core/__pycache__/database_update_worker.cpython-312.pyc and b/core/__pycache__/database_update_worker.cpython-312.pyc differ
diff --git a/database/__pycache__/music_database.cpython-312.pyc b/database/__pycache__/music_database.cpython-312.pyc
index c8c0ed3d..2c391fc7 100644
Binary files a/database/__pycache__/music_database.cpython-312.pyc and b/database/__pycache__/music_database.cpython-312.pyc differ
diff --git a/database/music_database.py b/database/music_database.py
index 0ac752a9..1866ffd9 100644
--- a/database/music_database.py
+++ b/database/music_database.py
@@ -1377,13 +1377,14 @@ class MusicDatabase:
cleaned = re.sub(r'\s*[\]\)]\s*', ' ', cleaned) # Convert closing brackets/parens to space
cleaned = re.sub(r'\s*-\s*', ' ', cleaned) # Convert dashes to spaces too
- # STEP 2: Remove clear noise patterns
+ # STEP 2: Remove clear noise patterns - very conservative approach
patterns_to_remove = [
r'\s*explicit\s*', # Remove explicit markers (now without brackets)
r'\s*clean\s*', # Remove clean markers (now without brackets)
r'\s*feat\..*', # Remove featuring (now without brackets)
r'\s*featuring.*', # Remove featuring (now without brackets)
r'\s*ft\..*', # Remove ft. (now without brackets)
+ r'\s*edit\s*$', # Remove "- edit" suffix only (specific case: "Reborn - edit" → "Reborn")
]
for pattern in patterns_to_remove:
diff --git a/ui/pages/__pycache__/sync.cpython-312.pyc b/ui/pages/__pycache__/sync.cpython-312.pyc
index 097819a4..350bf7f4 100644
Binary files a/ui/pages/__pycache__/sync.cpython-312.pyc and b/ui/pages/__pycache__/sync.cpython-312.pyc differ
diff --git a/ui/pages/sync.py b/ui/pages/sync.py
index 56965c44..b1adeb48 100644
--- a/ui/pages/sync.py
+++ b/ui/pages/sync.py
@@ -3141,8 +3141,33 @@ class ManualMatchModal(QDialog):
"""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
+
+ # Handle different parent modal types with flexible attribute access
+ try:
+ # Try the standard structure first (DownloadMissingTracksModal, DownloadMissingAlbumTracksModal)
+ self.soulseek_client = parent_modal.parent_page.soulseek_client
+ self.downloads_page = parent_modal.downloads_page
+ except AttributeError:
+ # Fallback for dashboard wishlist modal or other structures
+ try:
+ # Dashboard wishlist modal might have soulseek_client directly
+ self.soulseek_client = getattr(parent_modal, 'soulseek_client', None)
+ self.downloads_page = getattr(parent_modal, 'downloads_page', None)
+
+ # If still not found, try to get from parent widget hierarchy
+ if not self.soulseek_client:
+ current_widget = parent_modal.parent()
+ while current_widget and not self.soulseek_client:
+ self.soulseek_client = getattr(current_widget, 'soulseek_client', None)
+ self.downloads_page = getattr(current_widget, 'downloads_page', None)
+ current_widget = current_widget.parent()
+
+ except AttributeError:
+ pass
+
+ # Validate we have the required clients
+ if not self.soulseek_client:
+ raise RuntimeError("Could not find soulseek_client in parent modal or widget hierarchy")
self.failed_tracks = []
self.current_track_index = 0
@@ -3312,14 +3337,19 @@ class ManualMatchModal(QDialog):
preserving the user's current position.
"""
live_failed_tracks = self.parent_modal.permanently_failed_tracks
+ old_count = len(self.failed_tracks) if hasattr(self, 'failed_tracks') else 0
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)
+ new_count = len(self.failed_tracks)
+
+ print(f"🔄 Track list sync: {old_count} → {new_count} failed tracks, current_track_id={current_track_id}")
if not self.failed_tracks:
+ print("⚠️ No failed tracks remaining")
return
new_index = -1
@@ -3329,6 +3359,7 @@ class ManualMatchModal(QDialog):
new_index = i
break
+ old_index = self.current_track_index
if new_index != -1:
self.current_track_index = new_index
else:
@@ -3339,26 +3370,39 @@ class ManualMatchModal(QDialog):
if self.current_track_index < 0:
self.current_track_index = 0
+
+ if old_index != self.current_track_index:
+ print(f"📍 Index changed: {old_index} → {self.current_track_index}")
def load_current_track(self):
"""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()
+ # Only sync track list if we don't already have the current track loaded
+ # This prevents the index from being reset when navigating
+ if not hasattr(self, 'failed_tracks') or len(self.failed_tracks) == 0:
+ self._update_track_list()
if not self.failed_tracks:
QMessageBox.information(self, "Complete", "All failed tracks have been addressed.")
self.accept()
return
+ # Ensure current_track_index is still valid after any potential sync
+ 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
+
self.update_navigation_state()
self.current_track_info = self.failed_tracks[self.current_track_index]
spotify_track = self.current_track_info['spotify_track']
artist = spotify_track.artists[0] if spotify_track.artists else "Unknown"
+ print(f"📍 Loading track at index {self.current_track_index}: {spotify_track.name} by {artist}")
+
# Use the original track name for the info label
self.info_label.setText(f"Could not find: {spotify_track.name}
by {artist}")
@@ -3369,19 +3413,31 @@ class ManualMatchModal(QDialog):
def load_next_track(self):
"""Navigate to the next failed track."""
- if self.current_track_index < len(self.parent_modal.permanently_failed_tracks) - 1:
+ # Sync the track list first to handle any resolved tracks
+ self._update_track_list()
+
+ print(f"🔄 Next clicked: current_index={self.current_track_index}, failed_tracks_count={len(self.failed_tracks)}")
+
+ if self.current_track_index < len(self.failed_tracks) - 1:
self.current_track_index += 1
+ print(f"✅ Moving to next track: new_index={self.current_track_index}")
self.load_current_track()
+ else:
+ print(f"⚠️ Already at last track (index {self.current_track_index} of {len(self.failed_tracks)})")
def load_previous_track(self):
"""Navigate to the previous failed track."""
+ # Sync the track list first to handle any resolved tracks
+ self._update_track_list()
+
if self.current_track_index > 0:
self.current_track_index -= 1
self.load_current_track()
def update_navigation_state(self):
"""Update the 'Track X of Y' label and enable/disable nav buttons."""
- total_tracks = len(self.parent_modal.permanently_failed_tracks)
+ # Use the internal synchronized list for consistency
+ total_tracks = len(self.failed_tracks)
# Ensure current_track_index is valid even if list shrinks
if self.current_track_index >= total_tracks:
@@ -3505,6 +3561,30 @@ class ManualMatchModal(QDialog):
self.track_resolved.emit(self.current_track_info)
+ # Auto-advance to the next failed track after successful selection
+ # Use a small delay to allow the parent modal to update the failed tracks list
+ QTimer.singleShot(100, self._advance_to_next_track_after_resolution)
+
+ def _advance_to_next_track_after_resolution(self):
+ """
+ Advances to the next failed track after a successful manual resolution.
+ If no more tracks remain, closes the modal with a success message.
+ """
+ # Sync the track list to reflect the resolved track being removed
+ self._update_track_list()
+
+ if not self.failed_tracks:
+ # No more failed tracks - show success and close
+ QMessageBox.information(self, "Complete", "All failed tracks have been resolved! 🎉")
+ self.accept()
+ return
+
+ # Check if we need to adjust the current index after removal
+ if self.current_track_index >= len(self.failed_tracks):
+ self.current_track_index = len(self.failed_tracks) - 1
+
+ # Load the next track (which might be at the same index if current was removed)
+ print(f"🔄 Auto-advancing after resolution: index {self.current_track_index} of {len(self.failed_tracks)} remaining")
self.load_current_track()
def clear_results(self):
@@ -4086,7 +4166,11 @@ class DownloadMissingTracksModal(QDialog):
self.active_parallel_downloads += 1
self.download_queue_index += 1
- if (self.download_queue_index >= len(self.missing_tracks) and self.active_parallel_downloads == 0):
+ # Check if we're done: either all downloads completed OR all remaining work is done
+ downloads_complete = (self.download_queue_index >= len(self.missing_tracks) and self.active_parallel_downloads == 0)
+ all_work_complete = (self.completed_downloads >= len(self.missing_tracks))
+
+ if downloads_complete or all_work_complete:
self.on_all_downloads_complete()
def search_and_download_track_parallel(self, spotify_track, download_index, track_index):
@@ -4443,6 +4527,18 @@ class DownloadMissingTracksModal(QDialog):
if original_failed_track:
self.permanently_failed_tracks.remove(original_failed_track)
print(f"✅ Removed track from permanently_failed_tracks - remaining: {len(self.permanently_failed_tracks)}")
+
+ # Update progress bar to account for manually resolved track
+ # The track was manually resolved, so we need to count it as "completed"
+ self.successful_downloads += 1
+ self.completed_downloads += 1
+ # Update the progress bar maximum to reflect the actual remaining work
+ total_remaining_work = len(self.missing_tracks) - (self.successful_downloads - len(self.permanently_failed_tracks))
+ if total_remaining_work > 0:
+ # Recalculate progress: completed work / total original work
+ progress_value = self.completed_downloads
+ self.download_progress.setValue(progress_value)
+ print(f"📊 Updated progress: {progress_value}/{self.download_progress.maximum()} (manual fix)")
else:
print("⚠️ Could not find original failed track to remove")
self.update_failed_matches_button()