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:
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue