diff --git a/core/wishlist_service.py b/core/wishlist_service.py
index 62d97cd0..c6d102f0 100644
--- a/core/wishlist_service.py
+++ b/core/wishlist_service.py
@@ -165,6 +165,52 @@ class WishlistService:
"""Clear all tracks from the wishlist"""
return self.database.clear_wishlist()
+ def check_track_in_wishlist(self, spotify_track_id: str) -> bool:
+ """Check if a track exists in the wishlist by Spotify track ID"""
+ try:
+ wishlist_tracks = self.get_wishlist_tracks_for_download()
+ for track in wishlist_tracks:
+ if track.get('spotify_track_id') == spotify_track_id or track.get('id') == spotify_track_id:
+ return True
+ return False
+ except Exception as e:
+ logger.error(f"Error checking track in wishlist: {e}")
+ return False
+
+ def find_matching_wishlist_track(self, track_name: str, artist_name: str) -> Optional[Dict[str, Any]]:
+ """
+ Find a matching track in the wishlist using fuzzy matching on name and artist.
+ Returns the first matching wishlist track or None if no match found.
+ """
+ try:
+ wishlist_tracks = self.get_wishlist_tracks_for_download()
+
+ # Normalize input for comparison
+ normalized_track_name = track_name.lower().strip()
+ normalized_artist_name = artist_name.lower().strip()
+
+ for wl_track in wishlist_tracks:
+ wl_name = wl_track.get('name', '').lower().strip()
+ wl_artists = wl_track.get('artists', [])
+
+ # Extract artist name from wishlist track
+ wl_artist_name = ''
+ if wl_artists:
+ if isinstance(wl_artists[0], dict):
+ wl_artist_name = wl_artists[0].get('name', '').lower().strip()
+ else:
+ wl_artist_name = str(wl_artists[0]).lower().strip()
+
+ # Simple exact matching (could be enhanced with fuzzy matching algorithms)
+ if wl_name == normalized_track_name and wl_artist_name == normalized_artist_name:
+ return wl_track
+
+ return None
+
+ except Exception as e:
+ logger.error(f"Error finding matching wishlist track: {e}")
+ return None
+
def get_wishlist_summary(self) -> Dict[str, Any]:
"""Get a summary of the wishlist for dashboard display"""
try:
diff --git a/web_server.py b/web_server.py
index 1d79b064..ac2ef73f 100644
--- a/web_server.py
+++ b/web_server.py
@@ -4525,6 +4525,12 @@ def _post_process_matched_download(context_key, context, file_path):
print(f"โ
Post-processing complete for: {final_path}")
+ # WISHLIST REMOVAL: Check if this track should be removed from wishlist after successful download
+ try:
+ _check_and_remove_from_wishlist(context)
+ except Exception as wishlist_error:
+ print(f"โ ๏ธ [Post-Process] Error checking wishlist removal: {wishlist_error}")
+
# Call completion callback for missing downloads tasks to start next batch
task_id = context.get('task_id')
batch_id = context.get('batch_id')
@@ -4559,6 +4565,232 @@ def _post_process_matched_download(context_key, context, file_path):
# Keep track of processed downloads to avoid re-processing
_processed_download_ids = set()
+def _check_and_remove_from_wishlist(context):
+ """
+ Check if a successfully downloaded track should be removed from wishlist.
+ Extracts Spotify track data from download context and removes from wishlist if found.
+ """
+ try:
+ from core.wishlist_service import get_wishlist_service
+ wishlist_service = get_wishlist_service()
+
+ # Try to extract Spotify track ID from various sources in the context
+ spotify_track_id = None
+
+ # Method 1: Direct track_info with id
+ track_info = context.get('track_info', {})
+ if track_info.get('id'):
+ spotify_track_id = track_info['id']
+ print(f"๐ [Wishlist] Found Spotify ID from track_info: {spotify_track_id}")
+
+ # Method 2: From original search result
+ elif context.get('original_search_result', {}).get('id'):
+ spotify_track_id = context['original_search_result']['id']
+ print(f"๐ [Wishlist] Found Spotify ID from original_search_result: {spotify_track_id}")
+
+ # Method 3: Check if this is a wishlist download (context has wishlist_id)
+ elif 'wishlist_id' in track_info:
+ wishlist_id = track_info['wishlist_id']
+ print(f"๐ [Wishlist] Found wishlist_id in context: {wishlist_id}")
+
+ # Get the Spotify track ID from the wishlist entry
+ wishlist_tracks = wishlist_service.get_wishlist_tracks_for_download()
+ for wl_track in wishlist_tracks:
+ if wl_track.get('wishlist_id') == wishlist_id:
+ spotify_track_id = wl_track.get('spotify_track_id') or wl_track.get('id')
+ print(f"๐ [Wishlist] Found Spotify ID from wishlist entry: {spotify_track_id}")
+ break
+
+ # Method 4: Try to construct ID from track metadata for fuzzy matching
+ if not spotify_track_id:
+ track_name = track_info.get('name') or context.get('original_search_result', {}).get('title', '')
+ artist_name = _get_track_artist_name(track_info) or _get_track_artist_name(context.get('original_search_result', {}))
+
+ if track_name and artist_name:
+ print(f"๐ [Wishlist] No Spotify ID found, checking for fuzzy match: '{track_name}' by '{artist_name}'")
+
+ # Get all wishlist tracks and find potential matches
+ wishlist_tracks = wishlist_service.get_wishlist_tracks_for_download()
+ for wl_track in wishlist_tracks:
+ wl_name = wl_track.get('name', '').lower()
+ wl_artists = wl_track.get('artists', [])
+ wl_artist_name = ''
+
+ # Extract artist name from wishlist track
+ if wl_artists:
+ if isinstance(wl_artists[0], dict):
+ wl_artist_name = wl_artists[0].get('name', '').lower()
+ else:
+ wl_artist_name = str(wl_artists[0]).lower()
+
+ # Simple fuzzy matching
+ if (wl_name == track_name.lower() and wl_artist_name == artist_name.lower()):
+ spotify_track_id = wl_track.get('spotify_track_id') or wl_track.get('id')
+ print(f"๐ [Wishlist] Found fuzzy match - Spotify ID: {spotify_track_id}")
+ break
+
+ # If we found a Spotify track ID, remove it from wishlist
+ if spotify_track_id:
+ print(f"๐ [Wishlist] Attempting to remove track from wishlist: {spotify_track_id}")
+ removed = wishlist_service.mark_track_download_result(spotify_track_id, success=True)
+ if removed:
+ print(f"โ
[Wishlist] Successfully removed track from wishlist: {spotify_track_id}")
+ else:
+ print(f"โน๏ธ [Wishlist] Track not found in wishlist or already removed: {spotify_track_id}")
+ else:
+ print(f"โน๏ธ [Wishlist] No Spotify track ID found for wishlist removal check")
+
+ except Exception as e:
+ print(f"โ [Wishlist] Error in wishlist removal check: {e}")
+ import traceback
+ traceback.print_exc()
+
+def _check_and_remove_track_from_wishlist_by_metadata(track_data):
+ """
+ Check if a track found during database analysis should be removed from wishlist.
+ Uses track metadata (name, artists, id) to find and remove from wishlist.
+ """
+ try:
+ from core.wishlist_service import get_wishlist_service
+ wishlist_service = get_wishlist_service()
+
+ # Extract track info
+ track_name = track_data.get('name', '')
+ track_id = track_data.get('id', '')
+ artists = track_data.get('artists', [])
+
+ print(f"๐ [Analysis] Checking if track should be removed from wishlist: '{track_name}' (ID: {track_id})")
+
+ # Method 1: Direct Spotify ID match
+ if track_id:
+ removed = wishlist_service.mark_track_download_result(track_id, success=True)
+ if removed:
+ print(f"โ
[Analysis] Removed track from wishlist via direct ID match: {track_id}")
+ return True
+
+ # Method 2: Fuzzy matching by name and artist if no direct ID match
+ if track_name and artists:
+ # Extract primary artist name
+ primary_artist = ''
+ if isinstance(artists[0], dict) and 'name' in artists[0]:
+ primary_artist = artists[0]['name']
+ elif isinstance(artists[0], str):
+ primary_artist = artists[0]
+ else:
+ primary_artist = str(artists[0])
+
+ print(f"๐ [Analysis] No direct ID match, trying fuzzy match: '{track_name}' by '{primary_artist}'")
+
+ # Get all wishlist tracks and find matches
+ wishlist_tracks = wishlist_service.get_wishlist_tracks_for_download()
+ for wl_track in wishlist_tracks:
+ wl_name = wl_track.get('name', '').lower()
+ wl_artists = wl_track.get('artists', [])
+ wl_artist_name = ''
+
+ # Extract artist name from wishlist track
+ if wl_artists:
+ if isinstance(wl_artists[0], dict):
+ wl_artist_name = wl_artists[0].get('name', '').lower()
+ else:
+ wl_artist_name = str(wl_artists[0]).lower()
+
+ # Fuzzy matching - normalize strings for comparison
+ if (wl_name == track_name.lower() and wl_artist_name == primary_artist.lower()):
+ spotify_track_id = wl_track.get('spotify_track_id') or wl_track.get('id')
+ if spotify_track_id:
+ removed = wishlist_service.mark_track_download_result(spotify_track_id, success=True)
+ if removed:
+ print(f"โ
[Analysis] Removed track from wishlist via fuzzy match: {spotify_track_id}")
+ return True
+
+ print(f"โน๏ธ [Analysis] Track not found in wishlist or already removed: '{track_name}'")
+ return False
+
+ except Exception as e:
+ print(f"โ [Analysis] Error checking wishlist removal by metadata: {e}")
+ import traceback
+ traceback.print_exc()
+ return False
+
+def _automatic_wishlist_cleanup_after_db_update():
+ """
+ Automatic wishlist cleanup that runs after database updates.
+ This is a simplified version of the cleanup API endpoint designed for background execution.
+ """
+ try:
+ from core.wishlist_service import get_wishlist_service
+ from database.music_database import MusicDatabase
+
+ wishlist_service = get_wishlist_service()
+ db = MusicDatabase()
+ active_server = config_manager.get_active_media_server()
+
+ print("๐ [Auto Cleanup] Starting automatic wishlist cleanup after database update...")
+
+ # Get all wishlist tracks
+ wishlist_tracks = wishlist_service.get_wishlist_tracks_for_download()
+ if not wishlist_tracks:
+ print("๐ [Auto Cleanup] No tracks in wishlist to clean up")
+ return
+
+ print(f"๐ [Auto Cleanup] Found {len(wishlist_tracks)} tracks in wishlist")
+
+ removed_count = 0
+
+ for track in wishlist_tracks:
+ track_name = track.get('name', '')
+ artists = track.get('artists', [])
+ spotify_track_id = track.get('spotify_track_id') or track.get('id')
+
+ # Skip if no essential data
+ if not track_name or not artists or not spotify_track_id:
+ continue
+
+ # Check each artist
+ found_in_db = False
+ for artist in artists:
+ # Handle both string format and dict format
+ if isinstance(artist, str):
+ artist_name = artist
+ elif isinstance(artist, dict) and 'name' in artist:
+ artist_name = artist['name']
+ else:
+ artist_name = str(artist)
+
+ try:
+ db_track, confidence = db.check_track_exists(
+ track_name, artist_name,
+ confidence_threshold=0.7,
+ server_source=active_server
+ )
+
+ if db_track and confidence >= 0.7:
+ found_in_db = True
+ print(f"๐ [Auto Cleanup] Track found in database: '{track_name}' by {artist_name} (confidence: {confidence:.2f})")
+ break
+
+ except Exception as db_error:
+ print(f"โ ๏ธ [Auto Cleanup] Error checking database for track '{track_name}': {db_error}")
+ continue
+
+ # If found in database, remove from wishlist
+ if found_in_db:
+ try:
+ removed = wishlist_service.mark_track_download_result(spotify_track_id, success=True)
+ if removed:
+ removed_count += 1
+ print(f"โ
[Auto Cleanup] Removed track from wishlist: '{track_name}' ({spotify_track_id})")
+ except Exception as remove_error:
+ print(f"โ [Auto Cleanup] Error removing track from wishlist: {remove_error}")
+
+ print(f"๐ [Auto Cleanup] Completed automatic cleanup: {removed_count} tracks removed from wishlist")
+
+ except Exception as e:
+ print(f"โ [Auto Cleanup] Error in automatic wishlist cleanup: {e}")
+ import traceback
+ traceback.print_exc()
+
@app.route('/api/version-info', methods=['GET'])
def get_version_info():
"""
@@ -4846,6 +5078,14 @@ def _db_update_finished_callback(total_artists, total_albums, total_tracks, succ
with db_update_lock:
db_update_state["status"] = "finished"
db_update_state["phase"] = f"Completed: {successful} successful, {failed} failed."
+
+ # WISHLIST CLEANUP: Automatically clean up wishlist after database update
+ try:
+ print("๐ [DB Update] Database update completed, starting automatic wishlist cleanup...")
+ # Run cleanup in background to avoid blocking the UI
+ missing_download_executor.submit(_automatic_wishlist_cleanup_after_db_update)
+ except Exception as cleanup_error:
+ print(f"โ ๏ธ [DB Update] Error starting automatic wishlist cleanup: {cleanup_error}")
def _db_update_error_callback(error_message):
with db_update_lock:
@@ -5002,6 +5242,95 @@ def clear_wishlist():
print(f"Error clearing wishlist: {e}")
return jsonify({"success": False, "error": str(e)}), 500
+@app.route('/api/wishlist/cleanup', methods=['POST'])
+def cleanup_wishlist():
+ """Endpoint to remove tracks from wishlist that already exist in the database."""
+ try:
+ from core.wishlist_service import get_wishlist_service
+ from database.music_database import MusicDatabase
+
+ wishlist_service = get_wishlist_service()
+ db = MusicDatabase()
+ active_server = config_manager.get_active_media_server()
+
+ print("๐ [Wishlist Cleanup] Starting wishlist cleanup process...")
+
+ # Get all wishlist tracks
+ wishlist_tracks = wishlist_service.get_wishlist_tracks_for_download()
+ if not wishlist_tracks:
+ return jsonify({"success": True, "message": "No tracks in wishlist to clean up", "removed_count": 0})
+
+ print(f"๐ [Wishlist Cleanup] Found {len(wishlist_tracks)} tracks in wishlist")
+
+ removed_count = 0
+ processed_count = 0
+
+ for track in wishlist_tracks:
+ processed_count += 1
+ track_name = track.get('name', '')
+ artists = track.get('artists', [])
+ spotify_track_id = track.get('spotify_track_id') or track.get('id')
+
+ # Skip if no essential data
+ if not track_name or not artists or not spotify_track_id:
+ continue
+
+ print(f"๐ [Wishlist Cleanup] Checking track {processed_count}/{len(wishlist_tracks)}: '{track_name}'")
+
+ # Check each artist
+ found_in_db = False
+ for artist in artists:
+ # Handle both string format and dict format
+ if isinstance(artist, str):
+ artist_name = artist
+ elif isinstance(artist, dict) and 'name' in artist:
+ artist_name = artist['name']
+ else:
+ artist_name = str(artist)
+
+ try:
+ db_track, confidence = db.check_track_exists(
+ track_name, artist_name,
+ confidence_threshold=0.7,
+ server_source=active_server
+ )
+
+ if db_track and confidence >= 0.7:
+ found_in_db = True
+ print(f"๐ [Wishlist Cleanup] Track found in database: '{track_name}' by {artist_name} (confidence: {confidence:.2f})")
+ break
+
+ except Exception as db_error:
+ print(f"โ ๏ธ [Wishlist Cleanup] Error checking database for track '{track_name}': {db_error}")
+ continue
+
+ # If found in database, remove from wishlist
+ if found_in_db:
+ try:
+ removed = wishlist_service.mark_track_download_result(spotify_track_id, success=True)
+ if removed:
+ removed_count += 1
+ print(f"โ
[Wishlist Cleanup] Removed track from wishlist: '{track_name}' ({spotify_track_id})")
+ else:
+ print(f"โ ๏ธ [Wishlist Cleanup] Failed to remove track from wishlist: '{track_name}' ({spotify_track_id})")
+ except Exception as remove_error:
+ print(f"โ [Wishlist Cleanup] Error removing track from wishlist: {remove_error}")
+
+ print(f"๐ [Wishlist Cleanup] Completed cleanup: {removed_count} tracks removed from wishlist")
+
+ return jsonify({
+ "success": True,
+ "message": f"Wishlist cleanup completed: {removed_count} tracks removed",
+ "removed_count": removed_count,
+ "processed_count": processed_count
+ })
+
+ except Exception as e:
+ print(f"Error in wishlist cleanup: {e}")
+ import traceback
+ traceback.print_exc()
+ return jsonify({"success": False, "error": str(e)}), 500
+
@app.route('/api/database/update', methods=['POST'])
def start_database_update():
"""Endpoint to start the database update process."""
@@ -5510,6 +5839,24 @@ def _on_download_completed(batch_id, task_id, success=True):
download_batches[batch_id]['permanently_failed_tracks'].append(track_info)
print(f"โ [Batch Manager] Added failed track to batch tracking: {track_info['track_name']}")
+ # WISHLIST REMOVAL: Handle successful downloads for wishlist removal
+ if success and task_id in download_tasks:
+ try:
+ task = download_tasks[task_id]
+ track_info = task.get('track_info', {})
+ print(f"๐ [Batch Manager] Successful download - checking wishlist removal for task {task_id}")
+
+ # Try to remove from wishlist using track info
+ if track_info:
+ # Create a context-like structure for the wishlist removal function
+ context = {
+ 'track_info': track_info,
+ 'original_search_result': track_info # fallback
+ }
+ _check_and_remove_from_wishlist(context)
+ except Exception as wishlist_error:
+ print(f"โ ๏ธ [Batch Manager] Error checking wishlist removal for successful download: {wishlist_error}")
+
# Decrement active count
old_active = download_batches[batch_id]['active_count']
download_batches[batch_id]['active_count'] -= 1
@@ -5628,6 +5975,13 @@ def _run_full_missing_tracks_process(batch_id, playlist_id, tracks_json):
analysis_results.append({
'track_index': i, 'track': track_data, 'found': found, 'confidence': confidence
})
+
+ # WISHLIST REMOVAL: If track is found in database, check if it should be removed from wishlist
+ if found and confidence >= 0.7:
+ try:
+ _check_and_remove_track_from_wishlist_by_metadata(track_data)
+ except Exception as wishlist_error:
+ print(f"โ ๏ธ [Analysis] Error checking wishlist removal for found track: {wishlist_error}")
with tasks_lock:
if batch_id in download_batches:
diff --git a/webui/static/script.js b/webui/static/script.js
index e05ff9df..255e1d78 100644
--- a/webui/static/script.js
+++ b/webui/static/script.js
@@ -3405,6 +3405,9 @@ async function openDownloadMissingWishlistModal() {
+
@@ -8488,6 +8491,71 @@ async function handleWishlistButtonClick() {
}
}
+async function cleanupWishlist(playlistId) {
+ try {
+ // Show information dialog
+ const confirmed = confirm(
+ "Cleanup Wishlist\n\n" +
+ "This will check all wishlist tracks against your music library and automatically remove " +
+ "any tracks that already exist in your database.\n\n" +
+ "This is a safe operation that only removes tracks you already have. " +
+ "Continue with cleanup?"
+ );
+
+ if (!confirmed) {
+ return;
+ }
+
+ // Disable the cleanup button during the operation
+ const cleanupBtn = document.getElementById(`cleanup-wishlist-btn-${playlistId}`);
+ if (cleanupBtn) {
+ cleanupBtn.disabled = true;
+ cleanupBtn.textContent = '๐งน Cleaning...';
+ }
+
+ const response = await fetch('/api/wishlist/cleanup', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ });
+
+ const result = await response.json();
+
+ if (result.success) {
+ const removedCount = result.removed_count || 0;
+ const processedCount = result.processed_count || 0;
+
+ if (removedCount > 0) {
+ showToast(`Wishlist cleanup completed: ${removedCount} tracks removed (${processedCount} checked)`, 'success');
+
+ // Refresh the modal content to show updated state
+ setTimeout(() => {
+ openDownloadMissingWishlistModal();
+ }, 500);
+
+ // Update the wishlist count in the main dashboard
+ await updateWishlistCount();
+ } else {
+ showToast(`Wishlist cleanup completed: No tracks to remove (${processedCount} checked)`, 'info');
+ }
+ } else {
+ showToast(`Error cleaning wishlist: ${result.error}`, 'error');
+ }
+
+ } catch (error) {
+ console.error('Error cleaning wishlist:', error);
+ showToast(`Error cleaning wishlist: ${error.message}`, 'error');
+ } finally {
+ // Re-enable the cleanup button
+ const cleanupBtn = document.getElementById(`cleanup-wishlist-btn-${playlistId}`);
+ if (cleanupBtn) {
+ cleanupBtn.disabled = false;
+ cleanupBtn.textContent = '๐งน Cleanup Wishlist';
+ }
+ }
+}
+
async function clearWishlist(playlistId) {
try {
// Show confirmation dialog