Fix iTunes wishlist and remove Single suffix
iTunes tracks now include album field for proper wishlist support. Strip " - Single" and " - EP" suffixes from iTunes album names.
This commit is contained in:
parent
375dcb8a19
commit
8248fab16e
3 changed files with 92 additions and 14 deletions
|
|
@ -40,6 +40,23 @@ def rate_limited(func):
|
||||||
raise e
|
raise e
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
def _clean_itunes_album_name(album_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Remove iTunes-specific suffixes like " - Single", " - EP" from album names.
|
||||||
|
iTunes API adds these suffixes but users don't want them displayed.
|
||||||
|
"""
|
||||||
|
if not album_name:
|
||||||
|
return album_name
|
||||||
|
|
||||||
|
# List of suffixes to remove
|
||||||
|
suffixes_to_remove = [' - Single', ' - EP']
|
||||||
|
|
||||||
|
for suffix in suffixes_to_remove:
|
||||||
|
if album_name.endswith(suffix):
|
||||||
|
return album_name[:-len(suffix)]
|
||||||
|
|
||||||
|
return album_name
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Track:
|
class Track:
|
||||||
id: str
|
id: str
|
||||||
|
|
@ -75,7 +92,7 @@ class Track:
|
||||||
id=str(track_data.get('trackId', '')),
|
id=str(track_data.get('trackId', '')),
|
||||||
name=track_data.get('trackName', ''),
|
name=track_data.get('trackName', ''),
|
||||||
artists=artists,
|
artists=artists,
|
||||||
album=track_data.get('collectionName', ''),
|
album=_clean_itunes_album_name(track_data.get('collectionName', '')),
|
||||||
duration_ms=track_data.get('trackTimeMillis', 0),
|
duration_ms=track_data.get('trackTimeMillis', 0),
|
||||||
popularity=0, # iTunes doesn't provide popularity
|
popularity=0, # iTunes doesn't provide popularity
|
||||||
preview_url=track_data.get('previewUrl'),
|
preview_url=track_data.get('previewUrl'),
|
||||||
|
|
@ -161,7 +178,7 @@ class Album:
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
id=str(album_data.get('collectionId', '')),
|
id=str(album_data.get('collectionId', '')),
|
||||||
name=album_data.get('collectionName', ''),
|
name=_clean_itunes_album_name(album_data.get('collectionName', '')),
|
||||||
artists=[album_data.get('artistName', 'Unknown Artist')],
|
artists=[album_data.get('artistName', 'Unknown Artist')],
|
||||||
release_date=album_data.get('releaseDate', ''),
|
release_date=album_data.get('releaseDate', ''),
|
||||||
total_tracks=track_count,
|
total_tracks=track_count,
|
||||||
|
|
@ -370,7 +387,7 @@ class iTunesClient:
|
||||||
'primary_artist': clean_artist_name,
|
'primary_artist': clean_artist_name,
|
||||||
'album': {
|
'album': {
|
||||||
'id': str(track_data.get('collectionId', '')),
|
'id': str(track_data.get('collectionId', '')),
|
||||||
'name': track_data.get('collectionName', ''),
|
'name': _clean_itunes_album_name(track_data.get('collectionName', '')),
|
||||||
'total_tracks': track_data.get('trackCount', 0),
|
'total_tracks': track_data.get('trackCount', 0),
|
||||||
'release_date': track_data.get('releaseDate', ''),
|
'release_date': track_data.get('releaseDate', ''),
|
||||||
'album_type': 'album', # iTunes doesn't distinguish clearly
|
'album_type': 'album', # iTunes doesn't distinguish clearly
|
||||||
|
|
@ -408,7 +425,8 @@ class iTunesClient:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Get album name and explicitness
|
# Get album name and explicitness
|
||||||
album_name = album_data.get('collectionName', '').lower().strip()
|
# Clean album name before comparison for better deduplication
|
||||||
|
album_name = _clean_itunes_album_name(album_data.get('collectionName', '')).lower().strip()
|
||||||
artist_name = album_data.get('artistName', '').lower().strip()
|
artist_name = album_data.get('artistName', '').lower().strip()
|
||||||
is_explicit = album_data.get('collectionExplicitness') == 'explicit'
|
is_explicit = album_data.get('collectionExplicitness') == 'explicit'
|
||||||
|
|
||||||
|
|
@ -466,7 +484,7 @@ class iTunesClient:
|
||||||
|
|
||||||
album_result = {
|
album_result = {
|
||||||
'id': str(album_data.get('collectionId', '')),
|
'id': str(album_data.get('collectionId', '')),
|
||||||
'name': album_data.get('collectionName', ''),
|
'name': _clean_itunes_album_name(album_data.get('collectionName', '')),
|
||||||
'images': images,
|
'images': images,
|
||||||
'artists': [{'name': album_data.get('artistName', 'Unknown Artist'), 'id': str(album_data.get('artistId', ''))}],
|
'artists': [{'name': album_data.get('artistName', 'Unknown Artist'), 'id': str(album_data.get('artistId', ''))}],
|
||||||
'release_date': album_data.get('releaseDate', '')[:10] if album_data.get('releaseDate') else '', # YYYY-MM-DD format
|
'release_date': album_data.get('releaseDate', '')[:10] if album_data.get('releaseDate') else '', # YYYY-MM-DD format
|
||||||
|
|
@ -498,7 +516,21 @@ class iTunesClient:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# First result is usually the album/collection info
|
# First result is usually the album/collection info
|
||||||
# Remaining results are tracks
|
# Extract album information to include in each track (like Spotify does)
|
||||||
|
album_info = None
|
||||||
|
album_images = []
|
||||||
|
for item in results:
|
||||||
|
if item.get('wrapperType') == 'collection':
|
||||||
|
album_info = item
|
||||||
|
# Build album images array
|
||||||
|
if item.get('artworkUrl100'):
|
||||||
|
base_url = item['artworkUrl100'].replace('100x100bb', '{size}x{size}bb')
|
||||||
|
album_images = [
|
||||||
|
{'url': base_url.replace('{size}x{size}bb', '600x600bb'), 'height': 600, 'width': 600},
|
||||||
|
{'url': base_url.replace('{size}x{size}bb', '300x300bb'), 'height': 300, 'width': 300},
|
||||||
|
{'url': item['artworkUrl100'], 'height': 100, 'width': 100}
|
||||||
|
]
|
||||||
|
break
|
||||||
|
|
||||||
# Collect artist IDs for batch lookup
|
# Collect artist IDs for batch lookup
|
||||||
artist_ids = set()
|
artist_ids = set()
|
||||||
|
|
@ -519,11 +551,20 @@ class iTunesClient:
|
||||||
artist_id = str(item.get('artistId', ''))
|
artist_id = str(item.get('artistId', ''))
|
||||||
clean_artist = clean_artist_map.get(artist_id, item.get('artistName', 'Unknown Artist'))
|
clean_artist = clean_artist_map.get(artist_id, item.get('artistName', 'Unknown Artist'))
|
||||||
|
|
||||||
|
# Build album object for this track (like Spotify format)
|
||||||
|
track_album = {
|
||||||
|
'id': str(item.get('collectionId', album_id)),
|
||||||
|
'name': _clean_itunes_album_name(item.get('collectionName', 'Unknown Album')),
|
||||||
|
'images': album_images,
|
||||||
|
'release_date': item.get('releaseDate', '')[:10] if item.get('releaseDate') else ''
|
||||||
|
}
|
||||||
|
|
||||||
# Normalize each track to Spotify-compatible format
|
# Normalize each track to Spotify-compatible format
|
||||||
normalized_track = {
|
normalized_track = {
|
||||||
'id': str(item.get('trackId', '')),
|
'id': str(item.get('trackId', '')),
|
||||||
'name': item.get('trackName', ''),
|
'name': item.get('trackName', ''),
|
||||||
'artists': [{'name': clean_artist}], # List of dicts like Spotify
|
'artists': [{'name': clean_artist}], # List of dicts like Spotify
|
||||||
|
'album': track_album, # CRITICAL: Include album info like Spotify does
|
||||||
'duration_ms': item.get('trackTimeMillis', 0),
|
'duration_ms': item.get('trackTimeMillis', 0),
|
||||||
'track_number': item.get('trackNumber', 0),
|
'track_number': item.get('trackNumber', 0),
|
||||||
'disc_number': item.get('discNumber', 1),
|
'disc_number': item.get('discNumber', 1),
|
||||||
|
|
|
||||||
|
|
@ -11447,6 +11447,7 @@ def _process_failed_tracks_to_wishlist_exact(batch_id):
|
||||||
with tasks_lock:
|
with tasks_lock:
|
||||||
if batch_id in download_batches:
|
if batch_id in download_batches:
|
||||||
download_batches[batch_id]['wishlist_summary'] = completion_summary
|
download_batches[batch_id]['wishlist_summary'] = completion_summary
|
||||||
|
download_batches[batch_id]['wishlist_processing_complete'] = True
|
||||||
# Phase already set to 'complete' in _on_download_completed
|
# Phase already set to 'complete' in _on_download_completed
|
||||||
|
|
||||||
print(f"✅ [Wishlist Processing] Completed wishlist processing for batch {batch_id}")
|
print(f"✅ [Wishlist Processing] Completed wishlist processing for batch {batch_id}")
|
||||||
|
|
@ -11469,6 +11470,7 @@ def _process_failed_tracks_to_wishlist_exact(batch_id):
|
||||||
'total_failed': 0,
|
'total_failed': 0,
|
||||||
'error_message': str(e)
|
'error_message': str(e)
|
||||||
}
|
}
|
||||||
|
download_batches[batch_id]['wishlist_processing_complete'] = True
|
||||||
except Exception as lock_error:
|
except Exception as lock_error:
|
||||||
print(f"❌ [Wishlist Processing] Failed to update batch after error: {lock_error}")
|
print(f"❌ [Wishlist Processing] Failed to update batch after error: {lock_error}")
|
||||||
|
|
||||||
|
|
@ -11718,6 +11720,9 @@ def _on_download_completed(batch_id, task_id, success=True):
|
||||||
print(f"🎉 [Batch Manager] Batch {batch_id} complete - stopping monitor")
|
print(f"🎉 [Batch Manager] Batch {batch_id} complete - stopping monitor")
|
||||||
download_monitor.stop_monitoring(batch_id)
|
download_monitor.stop_monitoring(batch_id)
|
||||||
|
|
||||||
|
# Mark that wishlist processing is starting (prevents premature cleanup)
|
||||||
|
batch['wishlist_processing_started'] = True
|
||||||
|
|
||||||
# Process wishlist outside of the lock to prevent threading issues
|
# Process wishlist outside of the lock to prevent threading issues
|
||||||
if is_auto_batch:
|
if is_auto_batch:
|
||||||
# For auto-initiated batches, handle completion and schedule next cycle
|
# For auto-initiated batches, handle completion and schedule next cycle
|
||||||
|
|
@ -13824,8 +13829,21 @@ def cleanup_batch():
|
||||||
with tasks_lock:
|
with tasks_lock:
|
||||||
# Check if the batch exists before trying to delete
|
# Check if the batch exists before trying to delete
|
||||||
if batch_id in download_batches:
|
if batch_id in download_batches:
|
||||||
|
batch = download_batches[batch_id]
|
||||||
|
|
||||||
|
# CRITICAL: Don't allow cleanup if wishlist processing is in progress
|
||||||
|
# This prevents a race condition where cleanup deletes the batch before
|
||||||
|
# the wishlist processing thread can access it
|
||||||
|
if batch.get('wishlist_processing_started') and not batch.get('wishlist_processing_complete'):
|
||||||
|
print(f"⏳ [Cleanup] Batch {batch_id} cleanup deferred - wishlist processing in progress")
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": "Batch cleanup deferred - wishlist processing in progress",
|
||||||
|
"deferred": True
|
||||||
|
}), 202 # 202 = Accepted but not yet processed
|
||||||
|
|
||||||
# Get the list of task IDs before deleting the batch
|
# Get the list of task IDs before deleting the batch
|
||||||
task_ids_to_remove = download_batches[batch_id].get('queue', [])
|
task_ids_to_remove = batch.get('queue', [])
|
||||||
|
|
||||||
# Delete the batch record
|
# Delete the batch record
|
||||||
del download_batches[batch_id]
|
del download_batches[batch_id]
|
||||||
|
|
|
||||||
|
|
@ -5068,12 +5068,31 @@ async function cleanupDownloadProcess(playlistId) {
|
||||||
if (process.batchId) {
|
if (process.batchId) {
|
||||||
try {
|
try {
|
||||||
console.log(`🚀 Sending cleanup request to server for batch: ${process.batchId}`);
|
console.log(`🚀 Sending cleanup request to server for batch: ${process.batchId}`);
|
||||||
await fetch('/api/playlists/cleanup_batch', {
|
const response = await fetch('/api/playlists/cleanup_batch', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ batch_id: process.batchId })
|
body: JSON.stringify({ batch_id: process.batchId })
|
||||||
});
|
});
|
||||||
console.log(`✅ Server cleanup completed for batch: ${process.batchId}`);
|
|
||||||
|
// Handle deferred cleanup (202 = wishlist processing in progress)
|
||||||
|
if (response.status === 202) {
|
||||||
|
console.log(`⏳ Wishlist processing in progress for batch ${process.batchId}, will retry cleanup in 2s...`);
|
||||||
|
// Retry cleanup after delay to allow wishlist processing to complete
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
await fetch('/api/playlists/cleanup_batch', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ batch_id: process.batchId })
|
||||||
|
});
|
||||||
|
console.log(`✅ Delayed cleanup completed for batch: ${process.batchId}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`⚠️ Delayed cleanup failed:`, error);
|
||||||
|
}
|
||||||
|
}, 2000); // 2 second delay
|
||||||
|
} else {
|
||||||
|
console.log(`✅ Server cleanup completed for batch: ${process.batchId}`);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`⚠️ Failed to send cleanup request to server:`, error);
|
console.warn(`⚠️ Failed to send cleanup request to server:`, error);
|
||||||
// Don't show toast for cleanup failures - they're not user-facing
|
// Don't show toast for cleanup failures - they're not user-facing
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue