Improve stuck flag recovery and album classification
Adds critical fixes to reschedule timers after stuck flag recovery for wishlist and watchlist processes, preventing deadlocks and ensuring continuity. Refines album classification by prioritizing Spotify's album_type over track count heuristics, and ensures album_type is included in API responses. Updates frontend logic to pass and use fresh album/artist context for discover_album modals, improving metadata accuracy and display.
This commit is contained in:
parent
74e5c3d4d7
commit
c251edc709
2 changed files with 94 additions and 23 deletions
|
|
@ -8778,6 +8778,13 @@ def check_and_recover_stuck_flags():
|
||||||
with wishlist_timer_lock:
|
with wishlist_timer_lock:
|
||||||
wishlist_auto_processing = False
|
wishlist_auto_processing = False
|
||||||
wishlist_auto_processing_timestamp = 0
|
wishlist_auto_processing_timestamp = 0
|
||||||
|
|
||||||
|
# CRITICAL FIX: Reschedule timer after recovery to maintain continuity
|
||||||
|
print("🔄 [Stuck Recovery] Rescheduling wishlist processing in 30 minutes")
|
||||||
|
try:
|
||||||
|
schedule_next_wishlist_processing()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ [Stuck Recovery] Failed to reschedule wishlist processing: {e}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Check watchlist flag
|
# Check watchlist flag
|
||||||
|
|
@ -8788,6 +8795,13 @@ def check_and_recover_stuck_flags():
|
||||||
with watchlist_timer_lock:
|
with watchlist_timer_lock:
|
||||||
watchlist_auto_scanning = False
|
watchlist_auto_scanning = False
|
||||||
watchlist_auto_scanning_timestamp = 0
|
watchlist_auto_scanning_timestamp = 0
|
||||||
|
|
||||||
|
# CRITICAL FIX: Reschedule timer after recovery to maintain continuity
|
||||||
|
print("🔄 [Stuck Recovery] Rescheduling watchlist scan in 24 hours")
|
||||||
|
try:
|
||||||
|
schedule_next_watchlist_scan()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ [Stuck Recovery] Failed to reschedule watchlist scan: {e}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
@ -8886,13 +8900,21 @@ def _process_wishlist_automatically():
|
||||||
print("🤖 [Auto-Wishlist] Timer triggered - starting automatic wishlist processing...")
|
print("🤖 [Auto-Wishlist] Timer triggered - starting automatic wishlist processing...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# CRITICAL FIX: Use smart stuck detection BEFORE acquiring lock
|
||||||
|
# This prevents deadlock and handles stuck flags (2-hour timeout)
|
||||||
|
if is_wishlist_actually_processing():
|
||||||
|
print("⚠️ [Auto-Wishlist] Already processing (verified with stuck detection), skipping.")
|
||||||
|
schedule_next_wishlist_processing()
|
||||||
|
return
|
||||||
|
|
||||||
# Check conditions and set flag
|
# Check conditions and set flag
|
||||||
should_skip_already_running = False
|
should_skip_already_running = False
|
||||||
should_skip_watchlist_conflict = False
|
should_skip_watchlist_conflict = False
|
||||||
|
|
||||||
with wishlist_timer_lock:
|
with wishlist_timer_lock:
|
||||||
|
# Re-check inside lock to handle race conditions
|
||||||
if wishlist_auto_processing:
|
if wishlist_auto_processing:
|
||||||
print("⚠️ Wishlist auto-processing already running, skipping.")
|
print("⚠️ [Auto-Wishlist] Already processing (race condition check), skipping.")
|
||||||
should_skip_already_running = True
|
should_skip_already_running = True
|
||||||
|
|
||||||
# Check if watchlist scan is currently running (using smart detection)
|
# Check if watchlist scan is currently running (using smart detection)
|
||||||
|
|
@ -9053,19 +9075,25 @@ def _process_wishlist_automatically():
|
||||||
total_tracks = album_data.get('total_tracks')
|
total_tracks = album_data.get('total_tracks')
|
||||||
album_type = album_data.get('album_type', 'album').lower()
|
album_type = album_data.get('album_type', 'album').lower()
|
||||||
|
|
||||||
# Categorize by track count if available, otherwise use album_type
|
# CRITICAL FIX: Prioritize Spotify's album_type (most accurate source)
|
||||||
# Single: 1 track, EP: 2-5 tracks, Album: 6+ tracks
|
# Only fall back to track count heuristic if album_type is missing
|
||||||
is_single_or_ep = False
|
is_single_or_ep = False
|
||||||
is_album = False
|
is_album = False
|
||||||
|
|
||||||
if total_tracks is not None and total_tracks > 0:
|
# Use Spotify's album_type classification first
|
||||||
# Use track count (most accurate)
|
if album_type and album_type in ['single', 'ep', 'album', 'compilation']:
|
||||||
|
# Spotify's classification is authoritative
|
||||||
|
is_single_or_ep = album_type in ['single', 'ep']
|
||||||
|
is_album = album_type in ['album', 'compilation']
|
||||||
|
elif total_tracks is not None and total_tracks > 0:
|
||||||
|
# Fallback: Use track count heuristic only if album_type unavailable
|
||||||
|
# Single: 1 track, EP: 2-5 tracks, Album: 6+ tracks
|
||||||
is_single_or_ep = total_tracks < 6
|
is_single_or_ep = total_tracks < 6
|
||||||
is_album = total_tracks >= 6
|
is_album = total_tracks >= 6
|
||||||
else:
|
else:
|
||||||
# Fall back to Spotify's album_type
|
# No classification data available - default to album
|
||||||
is_single_or_ep = album_type in ['single', 'ep']
|
is_album = True
|
||||||
is_album = album_type == 'album'
|
is_single_or_ep = False
|
||||||
|
|
||||||
if current_cycle == 'singles' and is_single_or_ep:
|
if current_cycle == 'singles' and is_single_or_ep:
|
||||||
filtered_tracks.append(track)
|
filtered_tracks.append(track)
|
||||||
|
|
@ -13883,6 +13911,7 @@ def get_album_tracks(album_id):
|
||||||
'artists': album_data.get('artists', []),
|
'artists': album_data.get('artists', []),
|
||||||
'release_date': album_data.get('release_date', ''),
|
'release_date': album_data.get('release_date', ''),
|
||||||
'total_tracks': album_data.get('total_tracks', 0),
|
'total_tracks': album_data.get('total_tracks', 0),
|
||||||
|
'album_type': album_data.get('album_type', 'album'), # CRITICAL FIX: Include album_type for correct classification
|
||||||
'images': album_data.get('images', []),
|
'images': album_data.get('images', []),
|
||||||
'tracks': tracks
|
'tracks': tracks
|
||||||
}
|
}
|
||||||
|
|
@ -17470,9 +17499,17 @@ def _process_watchlist_scan_automatically():
|
||||||
print("🤖 [Auto-Watchlist] Timer triggered - starting automatic watchlist scan...")
|
print("🤖 [Auto-Watchlist] Timer triggered - starting automatic watchlist scan...")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# CRITICAL FIX: Use smart stuck detection BEFORE acquiring lock
|
||||||
|
# This prevents deadlock and handles stuck flags (2-hour timeout)
|
||||||
|
if is_watchlist_actually_scanning():
|
||||||
|
print("⚠️ [Auto-Watchlist] Already scanning (verified with stuck detection), skipping.")
|
||||||
|
schedule_next_watchlist_scan()
|
||||||
|
return
|
||||||
|
|
||||||
with watchlist_timer_lock:
|
with watchlist_timer_lock:
|
||||||
|
# Re-check inside lock to handle race conditions
|
||||||
if watchlist_auto_scanning:
|
if watchlist_auto_scanning:
|
||||||
print("⚠️ Watchlist auto-scanning already running, skipping.")
|
print("⚠️ [Auto-Watchlist] Already scanning (race condition check), skipping.")
|
||||||
schedule_next_watchlist_scan()
|
schedule_next_watchlist_scan()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5667,7 +5667,7 @@ function exportPlaylistAsM3U(playlistId) {
|
||||||
console.log(`✅ Exported M3U - Total: ${process.tracks.length}, Available: ${availableCount}, Missing: ${missingCount}`);
|
console.log(`✅ Exported M3U - Total: ${process.tracks.length}, Available: ${availableCount}, Missing: ${missingCount}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistName, spotifyTracks) {
|
async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistName, spotifyTracks, artist = null, album = null) {
|
||||||
showLoadingOverlay('Loading YouTube playlist...');
|
showLoadingOverlay('Loading YouTube playlist...');
|
||||||
// Check if a process is already active for this virtual playlist
|
// Check if a process is already active for this virtual playlist
|
||||||
if (activeDownloadProcesses[virtualPlaylistId]) {
|
if (activeDownloadProcesses[virtualPlaylistId]) {
|
||||||
|
|
@ -5710,7 +5710,9 @@ async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistNam
|
||||||
poller: null,
|
poller: null,
|
||||||
batchId: null,
|
batchId: null,
|
||||||
playlist: virtualPlaylist,
|
playlist: virtualPlaylist,
|
||||||
tracks: spotifyTracks
|
tracks: spotifyTracks,
|
||||||
|
artist: artist, // ✅ Store artist context
|
||||||
|
album: album // ✅ Store album context
|
||||||
};
|
};
|
||||||
|
|
||||||
// Generate hero section with dynamic source detection
|
// Generate hero section with dynamic source detection
|
||||||
|
|
@ -5726,9 +5728,11 @@ async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistNam
|
||||||
|
|
||||||
// Store metadata for discover download sidebar (will be added when Begin Analysis is clicked)
|
// Store metadata for discover download sidebar (will be added when Begin Analysis is clicked)
|
||||||
if (source === 'SoulSync' || virtualPlaylistId.startsWith('discover_lb_') || virtualPlaylistId.startsWith('listenbrainz_')) {
|
if (source === 'SoulSync' || virtualPlaylistId.startsWith('discover_lb_') || virtualPlaylistId.startsWith('listenbrainz_')) {
|
||||||
// Extract image URL from first track's album cover
|
// Extract image URL from album context or first track's album cover
|
||||||
let imageUrl = null;
|
let imageUrl = null;
|
||||||
if (spotifyTracks && spotifyTracks.length > 0) {
|
if (album && album.images && album.images.length > 0) {
|
||||||
|
imageUrl = album.images[0].url;
|
||||||
|
} else if (spotifyTracks && spotifyTracks.length > 0) {
|
||||||
const firstTrack = spotifyTracks[0];
|
const firstTrack = spotifyTracks[0];
|
||||||
if (firstTrack.album && firstTrack.album.images && firstTrack.album.images.length > 0) {
|
if (firstTrack.album && firstTrack.album.images && firstTrack.album.images.length > 0) {
|
||||||
imageUrl = firstTrack.album.images[0].url;
|
imageUrl = firstTrack.album.images[0].url;
|
||||||
|
|
@ -5737,11 +5741,22 @@ async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistNam
|
||||||
// Store in process for later use when Begin Analysis is clicked
|
// Store in process for later use when Begin Analysis is clicked
|
||||||
activeDownloadProcesses[virtualPlaylistId].discoverMetadata = {
|
activeDownloadProcesses[virtualPlaylistId].discoverMetadata = {
|
||||||
imageUrl: imageUrl,
|
imageUrl: imageUrl,
|
||||||
type: 'album'
|
type: album ? 'album' : 'playlist' // ✅ Use 'album' if album context provided
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const heroContext = {
|
// CRITICAL FIX: Use album context for discover_album playlists
|
||||||
|
const isDiscoverAlbum = virtualPlaylistId.startsWith('discover_album_');
|
||||||
|
const heroContext = isDiscoverAlbum && album && artist ? {
|
||||||
|
type: 'album', // ✅ Show as album, not playlist
|
||||||
|
album: {
|
||||||
|
name: album.name,
|
||||||
|
artist: artist.name, // ✅ Use actual artist name, not 'SoulSync'
|
||||||
|
images: album.images || []
|
||||||
|
},
|
||||||
|
trackCount: spotifyTracks.length,
|
||||||
|
playlistId: virtualPlaylistId
|
||||||
|
} : {
|
||||||
type: 'playlist',
|
type: 'playlist',
|
||||||
playlist: { name: playlistName, owner: source },
|
playlist: { name: playlistName, owner: source },
|
||||||
trackCount: spotifyTracks.length,
|
trackCount: spotifyTracks.length,
|
||||||
|
|
@ -32839,10 +32854,10 @@ async function openDownloadModalForRecentAlbum(albumIndex) {
|
||||||
throw new Error('No tracks found in album');
|
throw new Error('No tracks found in album');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to expected format
|
// Convert to expected format - CRITICAL FIX: Use fresh albumData from Spotify, not cached album
|
||||||
const spotifyTracks = albumData.tracks.map(track => {
|
const spotifyTracks = albumData.tracks.map(track => {
|
||||||
// Normalize artists to array of strings
|
// Normalize artists to array of strings
|
||||||
let artists = track.artists || [{ name: album.artist_name }];
|
let artists = track.artists || albumData.artists || [{ name: album.artist_name }];
|
||||||
if (Array.isArray(artists)) {
|
if (Array.isArray(artists)) {
|
||||||
artists = artists.map(a => a.name || a);
|
artists = artists.map(a => a.name || a);
|
||||||
}
|
}
|
||||||
|
|
@ -32852,19 +32867,38 @@ async function openDownloadModalForRecentAlbum(albumIndex) {
|
||||||
name: track.name,
|
name: track.name,
|
||||||
artists: artists,
|
artists: artists,
|
||||||
album: {
|
album: {
|
||||||
name: album.album_name,
|
id: albumData.id, // ✅ Album ID for proper tracking
|
||||||
images: album.album_cover_url ? [{ url: album.album_cover_url }] : []
|
name: albumData.name, // ✅ Use fresh data, not cached
|
||||||
|
album_type: albumData.album_type || 'album', // ✅ Critical: Album type for classification
|
||||||
|
total_tracks: albumData.total_tracks || 0, // ✅ Total tracks for context
|
||||||
|
release_date: albumData.release_date || '', // ✅ Release date
|
||||||
|
images: albumData.images || [] // ✅ Use Spotify images
|
||||||
},
|
},
|
||||||
duration_ms: track.duration_ms || 0
|
duration_ms: track.duration_ms || 0,
|
||||||
|
track_number: track.track_number || 0
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create virtual playlist ID
|
// Create virtual playlist ID
|
||||||
const virtualPlaylistId = `discover_album_${album.album_spotify_id}`;
|
const virtualPlaylistId = `discover_album_${album.album_spotify_id}`;
|
||||||
const playlistName = `${album.album_name} - ${album.artist_name}`;
|
|
||||||
|
|
||||||
// Open download modal
|
// CRITICAL FIX: Pass proper artist/album context for modal display
|
||||||
await openDownloadMissingModalForYouTube(virtualPlaylistId, playlistName, spotifyTracks);
|
const artistContext = {
|
||||||
|
id: album.artist_spotify_id,
|
||||||
|
name: album.artist_name
|
||||||
|
};
|
||||||
|
|
||||||
|
const albumContext = {
|
||||||
|
id: albumData.id,
|
||||||
|
name: albumData.name,
|
||||||
|
album_type: albumData.album_type || 'album',
|
||||||
|
total_tracks: albumData.total_tracks || 0,
|
||||||
|
release_date: albumData.release_date || '',
|
||||||
|
images: albumData.images || []
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open download modal with artist/album context
|
||||||
|
await openDownloadMissingModalForYouTube(virtualPlaylistId, albumData.name, spotifyTracks, artistContext, albumContext);
|
||||||
|
|
||||||
hideLoadingOverlay();
|
hideLoadingOverlay();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue