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:
wishlist_auto_processing = False
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
# Check watchlist flag
@ -8788,6 +8795,13 @@ def check_and_recover_stuck_flags():
with watchlist_timer_lock:
watchlist_auto_scanning = False
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 False
@ -8886,13 +8900,21 @@ def _process_wishlist_automatically():
print("🤖 [Auto-Wishlist] Timer triggered - starting automatic wishlist processing...")
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
should_skip_already_running = False
should_skip_watchlist_conflict = False
with wishlist_timer_lock:
# Re-check inside lock to handle race conditions
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
# 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')
album_type = album_data.get('album_type', 'album').lower()
# Categorize by track count if available, otherwise use album_type
# Single: 1 track, EP: 2-5 tracks, Album: 6+ tracks
# CRITICAL FIX: Prioritize Spotify's album_type (most accurate source)
# Only fall back to track count heuristic if album_type is missing
is_single_or_ep = False
is_album = False
if total_tracks is not None and total_tracks > 0:
# Use track count (most accurate)
# Use Spotify's album_type classification first
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_album = total_tracks >= 6
else:
# Fall back to Spotify's album_type
is_single_or_ep = album_type in ['single', 'ep']
is_album = album_type == 'album'
# No classification data available - default to album
is_album = True
is_single_or_ep = False
if current_cycle == 'singles' and is_single_or_ep:
filtered_tracks.append(track)
@ -13883,6 +13911,7 @@ def get_album_tracks(album_id):
'artists': album_data.get('artists', []),
'release_date': album_data.get('release_date', ''),
'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', []),
'tracks': tracks
}
@ -17470,9 +17499,17 @@ def _process_watchlist_scan_automatically():
print("🤖 [Auto-Watchlist] Timer triggered - starting automatic watchlist scan...")
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:
# Re-check inside lock to handle race conditions
if watchlist_auto_scanning:
print("⚠️ Watchlist auto-scanning already running, skipping.")
print("⚠️ [Auto-Watchlist] Already scanning (race condition check), skipping.")
schedule_next_watchlist_scan()
return

View file

@ -5667,7 +5667,7 @@ function exportPlaylistAsM3U(playlistId) {
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...');
// Check if a process is already active for this virtual playlist
if (activeDownloadProcesses[virtualPlaylistId]) {
@ -5710,7 +5710,9 @@ async function openDownloadMissingModalForYouTube(virtualPlaylistId, playlistNam
poller: null,
batchId: null,
playlist: virtualPlaylist,
tracks: spotifyTracks
tracks: spotifyTracks,
artist: artist, // ✅ Store artist context
album: album // ✅ Store album context
};
// 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)
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;
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];
if (firstTrack.album && firstTrack.album.images && firstTrack.album.images.length > 0) {
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
activeDownloadProcesses[virtualPlaylistId].discoverMetadata = {
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',
playlist: { name: playlistName, owner: source },
trackCount: spotifyTracks.length,
@ -32839,10 +32854,10 @@ async function openDownloadModalForRecentAlbum(albumIndex) {
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 => {
// 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)) {
artists = artists.map(a => a.name || a);
}
@ -32852,19 +32867,38 @@ async function openDownloadModalForRecentAlbum(albumIndex) {
name: track.name,
artists: artists,
album: {
name: album.album_name,
images: album.album_cover_url ? [{ url: album.album_cover_url }] : []
id: albumData.id, // ✅ Album ID for proper tracking
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
const virtualPlaylistId = `discover_album_${album.album_spotify_id}`;
const playlistName = `${album.album_name} - ${album.artist_name}`;
// Open download modal
await openDownloadMissingModalForYouTube(virtualPlaylistId, playlistName, spotifyTracks);
// CRITICAL FIX: Pass proper artist/album context for modal display
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();