From 69ea705f5d038c4ef47749cfcd572c73816f3c24 Mon Sep 17 00:00:00 2001 From: Broque Thomas Date: Wed, 19 Nov 2025 09:47:56 -0800 Subject: [PATCH] fix wishlist duplicates --- database/music_database.py | 103 +++++++++++++++++++++++++++++++++---- web_server.py | 12 ++++- webui/static/script.js | 13 ++++- 3 files changed, 115 insertions(+), 13 deletions(-) diff --git a/database/music_database.py b/database/music_database.py index d3aa1c55..3c35fb4d 100644 --- a/database/music_database.py +++ b/database/music_database.py @@ -2243,36 +2243,64 @@ class MusicDatabase: # Wishlist management methods - def add_to_wishlist(self, spotify_track_data: Dict[str, Any], failure_reason: str = "Download failed", + def add_to_wishlist(self, spotify_track_data: Dict[str, Any], failure_reason: str = "Download failed", source_type: str = "unknown", source_info: Dict[str, Any] = None) -> bool: """Add a failed track to the wishlist for retry""" try: with self._get_connection() as conn: cursor = conn.cursor() - + # Use Spotify track ID as unique identifier track_id = spotify_track_data.get('id') if not track_id: logger.error("Cannot add track to wishlist: missing Spotify track ID") return False - + + track_name = spotify_track_data.get('name', 'Unknown Track') + artists = spotify_track_data.get('artists', []) + artist_name = artists[0].get('name', 'Unknown Artist') if artists else 'Unknown Artist' + + # Check for duplicates by track name + artist (not just Spotify ID) + # This prevents adding the same track multiple times with different IDs or edge cases + cursor.execute(""" + SELECT id, spotify_track_id, spotify_data FROM wishlist_tracks + """) + + existing_tracks = cursor.fetchall() + + # Check if any existing track has matching name AND artist + for existing in existing_tracks: + try: + existing_data = json.loads(existing['spotify_data']) + existing_name = existing_data.get('name', '') + existing_artists = existing_data.get('artists', []) + existing_artist = existing_artists[0].get('name', '') if existing_artists else '' + + # Case-insensitive comparison of track name and primary artist + if (existing_name.lower() == track_name.lower() and + existing_artist.lower() == artist_name.lower()): + logger.info(f"Skipping duplicate wishlist entry: '{track_name}' by {artist_name} (already exists as ID: {existing['id']})") + return False # Already exists, don't add duplicate + except Exception as parse_error: + logger.warning(f"Error parsing existing wishlist track data: {parse_error}") + continue + # Convert data to JSON strings spotify_json = json.dumps(spotify_track_data) source_json = json.dumps(source_info or {}) - + + # No duplicate found, insert the track cursor.execute(""" - INSERT OR REPLACE INTO wishlist_tracks + INSERT OR REPLACE INTO wishlist_tracks (spotify_track_id, spotify_data, failure_reason, source_type, source_info, date_added) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP) """, (track_id, spotify_json, failure_reason, source_type, source_json)) - + conn.commit() - - track_name = spotify_track_data.get('name', 'Unknown Track') - artist_name = spotify_track_data.get('artists', [{}])[0].get('name', 'Unknown Artist') + logger.info(f"Added track to wishlist: '{track_name}' by {artist_name}") return True - + except Exception as e: logger.error(f"Error adding track to wishlist: {e}") return False @@ -2393,6 +2421,61 @@ class MusicDatabase: logger.error(f"Error clearing wishlist: {e}") return False + def remove_wishlist_duplicates(self) -> int: + """Remove duplicate tracks from wishlist based on track name + artist. + Keeps the oldest entry (by date_added) for each duplicate set. + Returns the number of duplicates removed.""" + try: + with self._get_connection() as conn: + cursor = conn.cursor() + + # Get all wishlist tracks + cursor.execute(""" + SELECT id, spotify_track_id, spotify_data, date_added + FROM wishlist_tracks + ORDER BY date_added ASC + """) + all_tracks = cursor.fetchall() + + # Track seen tracks and duplicates to remove + seen_tracks = {} # Key: (track_name, artist_name), Value: track_id to keep + duplicates_to_remove = [] + + for track in all_tracks: + try: + track_data = json.loads(track['spotify_data']) + track_name = track_data.get('name', '').lower() + artists = track_data.get('artists', []) + artist_name = artists[0].get('name', '').lower() if artists else 'unknown' + + key = (track_name, artist_name) + + if key in seen_tracks: + # Duplicate found - mark for removal + duplicates_to_remove.append(track['id']) + logger.info(f"Found duplicate: '{track_name}' by {artist_name} (ID: {track['id']}, keeping ID: {seen_tracks[key]})") + else: + # First occurrence - keep this one + seen_tracks[key] = track['id'] + + except Exception as parse_error: + logger.warning(f"Error parsing wishlist track {track['id']}: {parse_error}") + continue + + # Remove all duplicates + removed_count = 0 + for duplicate_id in duplicates_to_remove: + cursor.execute("DELETE FROM wishlist_tracks WHERE id = ?", (duplicate_id,)) + removed_count += 1 + + conn.commit() + logger.info(f"Removed {removed_count} duplicate tracks from wishlist") + return removed_count + + except Exception as e: + logger.error(f"Error removing wishlist duplicates: {e}") + return 0 + # Watchlist operations def add_artist_to_watchlist(self, spotify_artist_id: str, artist_name: str) -> bool: """Add an artist to the watchlist for monitoring new releases""" diff --git a/web_server.py b/web_server.py index a1c80ee1..4c233f2b 100644 --- a/web_server.py +++ b/web_server.py @@ -7823,15 +7823,23 @@ def get_wishlist_tracks(): """Endpoint to get wishlist tracks for display in modal.""" try: from core.wishlist_service import get_wishlist_service + from database.music_database import MusicDatabase + + # Clean duplicates before fetching (runs automatically on every fetch) + db = MusicDatabase() + duplicates_removed = db.remove_wishlist_duplicates() + if duplicates_removed > 0: + print(f"🧹 Cleaned {duplicates_removed} duplicate tracks from wishlist") + wishlist_service = get_wishlist_service() raw_tracks = wishlist_service.get_wishlist_tracks_for_download() - + # SANITIZE: Ensure consistent data format for frontend sanitized_tracks = [] for track in raw_tracks: sanitized_track = _sanitize_track_data_for_processing(track) sanitized_tracks.append(sanitized_track) - + return jsonify({"tracks": sanitized_tracks}) except Exception as e: print(f"Error getting wishlist tracks: {e}") diff --git a/webui/static/script.js b/webui/static/script.js index dcac7ea3..ae555d39 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -5343,7 +5343,7 @@ function processModalStatusUpdate(playlistId, data) { const missingCount = missingTracks.length; let completedCount = 0; let failedOrCancelledCount = 0; - + // Verify modal exists before processing tasks const modal = document.getElementById(`download-missing-modal-${playlistId}`); if (!modal) { @@ -5351,6 +5351,17 @@ function processModalStatusUpdate(playlistId, data) { return; } + // Update download progress text immediately when entering downloading phase + // This handles the case where tasks array is empty or still being populated + const downloadProgressText = document.getElementById(`download-progress-text-${playlistId}`); + if (data.phase === 'downloading' && missingCount > 0 && (!data.tasks || data.tasks.length === 0)) { + // No tasks yet, but we're in downloading phase with missing tracks + if (downloadProgressText) { + downloadProgressText.textContent = 'Preparing downloads...'; + console.log(`📥 [Download Phase] Preparing ${missingCount} downloads...`); + } + } + (data.tasks || []).forEach(task => { const row = document.querySelector(`#download-missing-modal-${playlistId} tr[data-track-index="${task.track_index}"]`); if (!row) {