fix wishlist duplicates
This commit is contained in:
parent
aef53f7b4d
commit
69ea705f5d
3 changed files with 115 additions and 13 deletions
|
|
@ -2243,36 +2243,64 @@ class MusicDatabase:
|
||||||
|
|
||||||
# Wishlist management methods
|
# 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:
|
source_type: str = "unknown", source_info: Dict[str, Any] = None) -> bool:
|
||||||
"""Add a failed track to the wishlist for retry"""
|
"""Add a failed track to the wishlist for retry"""
|
||||||
try:
|
try:
|
||||||
with self._get_connection() as conn:
|
with self._get_connection() as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
# Use Spotify track ID as unique identifier
|
# Use Spotify track ID as unique identifier
|
||||||
track_id = spotify_track_data.get('id')
|
track_id = spotify_track_data.get('id')
|
||||||
if not track_id:
|
if not track_id:
|
||||||
logger.error("Cannot add track to wishlist: missing Spotify track ID")
|
logger.error("Cannot add track to wishlist: missing Spotify track ID")
|
||||||
return False
|
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
|
# Convert data to JSON strings
|
||||||
spotify_json = json.dumps(spotify_track_data)
|
spotify_json = json.dumps(spotify_track_data)
|
||||||
source_json = json.dumps(source_info or {})
|
source_json = json.dumps(source_info or {})
|
||||||
|
|
||||||
|
# No duplicate found, insert the track
|
||||||
cursor.execute("""
|
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)
|
(spotify_track_id, spotify_data, failure_reason, source_type, source_info, date_added)
|
||||||
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||||
""", (track_id, spotify_json, failure_reason, source_type, source_json))
|
""", (track_id, spotify_json, failure_reason, source_type, source_json))
|
||||||
|
|
||||||
conn.commit()
|
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}")
|
logger.info(f"Added track to wishlist: '{track_name}' by {artist_name}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error adding track to wishlist: {e}")
|
logger.error(f"Error adding track to wishlist: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
@ -2393,6 +2421,61 @@ class MusicDatabase:
|
||||||
logger.error(f"Error clearing wishlist: {e}")
|
logger.error(f"Error clearing wishlist: {e}")
|
||||||
return False
|
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
|
# Watchlist operations
|
||||||
def add_artist_to_watchlist(self, spotify_artist_id: str, artist_name: str) -> bool:
|
def add_artist_to_watchlist(self, spotify_artist_id: str, artist_name: str) -> bool:
|
||||||
"""Add an artist to the watchlist for monitoring new releases"""
|
"""Add an artist to the watchlist for monitoring new releases"""
|
||||||
|
|
|
||||||
|
|
@ -7823,15 +7823,23 @@ def get_wishlist_tracks():
|
||||||
"""Endpoint to get wishlist tracks for display in modal."""
|
"""Endpoint to get wishlist tracks for display in modal."""
|
||||||
try:
|
try:
|
||||||
from core.wishlist_service import get_wishlist_service
|
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()
|
wishlist_service = get_wishlist_service()
|
||||||
raw_tracks = wishlist_service.get_wishlist_tracks_for_download()
|
raw_tracks = wishlist_service.get_wishlist_tracks_for_download()
|
||||||
|
|
||||||
# SANITIZE: Ensure consistent data format for frontend
|
# SANITIZE: Ensure consistent data format for frontend
|
||||||
sanitized_tracks = []
|
sanitized_tracks = []
|
||||||
for track in raw_tracks:
|
for track in raw_tracks:
|
||||||
sanitized_track = _sanitize_track_data_for_processing(track)
|
sanitized_track = _sanitize_track_data_for_processing(track)
|
||||||
sanitized_tracks.append(sanitized_track)
|
sanitized_tracks.append(sanitized_track)
|
||||||
|
|
||||||
return jsonify({"tracks": sanitized_tracks})
|
return jsonify({"tracks": sanitized_tracks})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error getting wishlist tracks: {e}")
|
print(f"Error getting wishlist tracks: {e}")
|
||||||
|
|
|
||||||
|
|
@ -5343,7 +5343,7 @@ function processModalStatusUpdate(playlistId, data) {
|
||||||
const missingCount = missingTracks.length;
|
const missingCount = missingTracks.length;
|
||||||
let completedCount = 0;
|
let completedCount = 0;
|
||||||
let failedOrCancelledCount = 0;
|
let failedOrCancelledCount = 0;
|
||||||
|
|
||||||
// Verify modal exists before processing tasks
|
// Verify modal exists before processing tasks
|
||||||
const modal = document.getElementById(`download-missing-modal-${playlistId}`);
|
const modal = document.getElementById(`download-missing-modal-${playlistId}`);
|
||||||
if (!modal) {
|
if (!modal) {
|
||||||
|
|
@ -5351,6 +5351,17 @@ function processModalStatusUpdate(playlistId, data) {
|
||||||
return;
|
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 => {
|
(data.tasks || []).forEach(task => {
|
||||||
const row = document.querySelector(`#download-missing-modal-${playlistId} tr[data-track-index="${task.track_index}"]`);
|
const row = document.querySelector(`#download-missing-modal-${playlistId} tr[data-track-index="${task.track_index}"]`);
|
||||||
if (!row) {
|
if (!row) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue