@@ -2243,36 +2243,64 @@ def get_quality_preset(self, preset_name: str) -> dict:
22432243
22442244 # Wishlist management methods
22452245
2246- def add_to_wishlist (self , spotify_track_data : Dict [str , Any ], failure_reason : str = "Download failed" ,
2246+ def add_to_wishlist (self , spotify_track_data : Dict [str , Any ], failure_reason : str = "Download failed" ,
22472247 source_type : str = "unknown" , source_info : Dict [str , Any ] = None ) -> bool :
22482248 """Add a failed track to the wishlist for retry"""
22492249 try :
22502250 with self ._get_connection () as conn :
22512251 cursor = conn .cursor ()
2252-
2252+
22532253 # Use Spotify track ID as unique identifier
22542254 track_id = spotify_track_data .get ('id' )
22552255 if not track_id :
22562256 logger .error ("Cannot add track to wishlist: missing Spotify track ID" )
22572257 return False
2258-
2258+
2259+ track_name = spotify_track_data .get ('name' , 'Unknown Track' )
2260+ artists = spotify_track_data .get ('artists' , [])
2261+ artist_name = artists [0 ].get ('name' , 'Unknown Artist' ) if artists else 'Unknown Artist'
2262+
2263+ # Check for duplicates by track name + artist (not just Spotify ID)
2264+ # This prevents adding the same track multiple times with different IDs or edge cases
2265+ cursor .execute ("""
2266+ SELECT id, spotify_track_id, spotify_data FROM wishlist_tracks
2267+ """ )
2268+
2269+ existing_tracks = cursor .fetchall ()
2270+
2271+ # Check if any existing track has matching name AND artist
2272+ for existing in existing_tracks :
2273+ try :
2274+ existing_data = json .loads (existing ['spotify_data' ])
2275+ existing_name = existing_data .get ('name' , '' )
2276+ existing_artists = existing_data .get ('artists' , [])
2277+ existing_artist = existing_artists [0 ].get ('name' , '' ) if existing_artists else ''
2278+
2279+ # Case-insensitive comparison of track name and primary artist
2280+ if (existing_name .lower () == track_name .lower () and
2281+ existing_artist .lower () == artist_name .lower ()):
2282+ logger .info (f"Skipping duplicate wishlist entry: '{ track_name } ' by { artist_name } (already exists as ID: { existing ['id' ]} )" )
2283+ return False # Already exists, don't add duplicate
2284+ except Exception as parse_error :
2285+ logger .warning (f"Error parsing existing wishlist track data: { parse_error } " )
2286+ continue
2287+
22592288 # Convert data to JSON strings
22602289 spotify_json = json .dumps (spotify_track_data )
22612290 source_json = json .dumps (source_info or {})
2262-
2291+
2292+ # No duplicate found, insert the track
22632293 cursor .execute ("""
2264- INSERT OR REPLACE INTO wishlist_tracks
2294+ INSERT OR REPLACE INTO wishlist_tracks
22652295 (spotify_track_id, spotify_data, failure_reason, source_type, source_info, date_added)
22662296 VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
22672297 """ , (track_id , spotify_json , failure_reason , source_type , source_json ))
2268-
2298+
22692299 conn .commit ()
2270-
2271- track_name = spotify_track_data .get ('name' , 'Unknown Track' )
2272- artist_name = spotify_track_data .get ('artists' , [{}])[0 ].get ('name' , 'Unknown Artist' )
2300+
22732301 logger .info (f"Added track to wishlist: '{ track_name } ' by { artist_name } " )
22742302 return True
2275-
2303+
22762304 except Exception as e :
22772305 logger .error (f"Error adding track to wishlist: { e } " )
22782306 return False
@@ -2393,6 +2421,61 @@ def clear_wishlist(self) -> bool:
23932421 logger .error (f"Error clearing wishlist: { e } " )
23942422 return False
23952423
2424+ def remove_wishlist_duplicates (self ) -> int :
2425+ """Remove duplicate tracks from wishlist based on track name + artist.
2426+ Keeps the oldest entry (by date_added) for each duplicate set.
2427+ Returns the number of duplicates removed."""
2428+ try :
2429+ with self ._get_connection () as conn :
2430+ cursor = conn .cursor ()
2431+
2432+ # Get all wishlist tracks
2433+ cursor .execute ("""
2434+ SELECT id, spotify_track_id, spotify_data, date_added
2435+ FROM wishlist_tracks
2436+ ORDER BY date_added ASC
2437+ """ )
2438+ all_tracks = cursor .fetchall ()
2439+
2440+ # Track seen tracks and duplicates to remove
2441+ seen_tracks = {} # Key: (track_name, artist_name), Value: track_id to keep
2442+ duplicates_to_remove = []
2443+
2444+ for track in all_tracks :
2445+ try :
2446+ track_data = json .loads (track ['spotify_data' ])
2447+ track_name = track_data .get ('name' , '' ).lower ()
2448+ artists = track_data .get ('artists' , [])
2449+ artist_name = artists [0 ].get ('name' , '' ).lower () if artists else 'unknown'
2450+
2451+ key = (track_name , artist_name )
2452+
2453+ if key in seen_tracks :
2454+ # Duplicate found - mark for removal
2455+ duplicates_to_remove .append (track ['id' ])
2456+ logger .info (f"Found duplicate: '{ track_name } ' by { artist_name } (ID: { track ['id' ]} , keeping ID: { seen_tracks [key ]} )" )
2457+ else :
2458+ # First occurrence - keep this one
2459+ seen_tracks [key ] = track ['id' ]
2460+
2461+ except Exception as parse_error :
2462+ logger .warning (f"Error parsing wishlist track { track ['id' ]} : { parse_error } " )
2463+ continue
2464+
2465+ # Remove all duplicates
2466+ removed_count = 0
2467+ for duplicate_id in duplicates_to_remove :
2468+ cursor .execute ("DELETE FROM wishlist_tracks WHERE id = ?" , (duplicate_id ,))
2469+ removed_count += 1
2470+
2471+ conn .commit ()
2472+ logger .info (f"Removed { removed_count } duplicate tracks from wishlist" )
2473+ return removed_count
2474+
2475+ except Exception as e :
2476+ logger .error (f"Error removing wishlist duplicates: { e } " )
2477+ return 0
2478+
23962479 # Watchlist operations
23972480 def add_artist_to_watchlist (self , spotify_artist_id : str , artist_name : str ) -> bool :
23982481 """Add an artist to the watchlist for monitoring new releases"""
0 commit comments