Skip to content

Commit 69ea705

Browse files
committed
fix wishlist duplicates
1 parent aef53f7 commit 69ea705

File tree

3 files changed

+115
-13
lines changed

3 files changed

+115
-13
lines changed

database/music_database.py

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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"""

web_server.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7823,15 +7823,23 @@ def get_wishlist_tracks():
78237823
"""Endpoint to get wishlist tracks for display in modal."""
78247824
try:
78257825
from core.wishlist_service import get_wishlist_service
7826+
from database.music_database import MusicDatabase
7827+
7828+
# Clean duplicates before fetching (runs automatically on every fetch)
7829+
db = MusicDatabase()
7830+
duplicates_removed = db.remove_wishlist_duplicates()
7831+
if duplicates_removed > 0:
7832+
print(f"🧹 Cleaned {duplicates_removed} duplicate tracks from wishlist")
7833+
78267834
wishlist_service = get_wishlist_service()
78277835
raw_tracks = wishlist_service.get_wishlist_tracks_for_download()
7828-
7836+
78297837
# SANITIZE: Ensure consistent data format for frontend
78307838
sanitized_tracks = []
78317839
for track in raw_tracks:
78327840
sanitized_track = _sanitize_track_data_for_processing(track)
78337841
sanitized_tracks.append(sanitized_track)
7834-
7842+
78357843
return jsonify({"tracks": sanitized_tracks})
78367844
except Exception as e:
78377845
print(f"Error getting wishlist tracks: {e}")

webui/static/script.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5343,14 +5343,25 @@ function processModalStatusUpdate(playlistId, data) {
53435343
const missingCount = missingTracks.length;
53445344
let completedCount = 0;
53455345
let failedOrCancelledCount = 0;
5346-
5346+
53475347
// Verify modal exists before processing tasks
53485348
const modal = document.getElementById(`download-missing-modal-${playlistId}`);
53495349
if (!modal) {
53505350
console.error(`❌ [Status Update] Modal not found: download-missing-modal-${playlistId}`);
53515351
return;
53525352
}
53535353

5354+
// Update download progress text immediately when entering downloading phase
5355+
// This handles the case where tasks array is empty or still being populated
5356+
const downloadProgressText = document.getElementById(`download-progress-text-${playlistId}`);
5357+
if (data.phase === 'downloading' && missingCount > 0 && (!data.tasks || data.tasks.length === 0)) {
5358+
// No tasks yet, but we're in downloading phase with missing tracks
5359+
if (downloadProgressText) {
5360+
downloadProgressText.textContent = 'Preparing downloads...';
5361+
console.log(`📥 [Download Phase] Preparing ${missingCount} downloads...`);
5362+
}
5363+
}
5364+
53545365
(data.tasks || []).forEach(task => {
53555366
const row = document.querySelector(`#download-missing-modal-${playlistId} tr[data-track-index="${task.track_index}"]`);
53565367
if (!row) {

0 commit comments

Comments
 (0)