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:
Broque Thomas 2026-01-06 16:04:05 -08:00
parent 74e5c3d4d7
commit c251edc709
2 changed files with 94 additions and 23 deletions

View file

@ -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

View file

@ -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();