diff --git a/web_server.py b/web_server.py index e2ded352..2e8f7a73 100644 --- a/web_server.py +++ b/web_server.py @@ -251,8 +251,7 @@ wishlist_timer_lock = threading.Lock() # Thread safety for timer operations # --- Automatic Watchlist Scanning Infrastructure --- # Server-side timer system for automatic watchlist scanning (mirrors wishlist pattern for consistency) -watchlist_scheduler_thread = None # background thread -watchlist_stop_event = threading.Event() +watchlist_auto_timer = None # threading.Timer for scheduling next auto-scanning watchlist_auto_scanning = False # Flag to prevent concurrent auto-scanning watchlist_auto_scanning_timestamp = 0 # Timestamp when scanning started (for stuck detection) watchlist_next_run_time = 0 # Timestamp when next auto-scanning is scheduled (for countdown display) @@ -17459,99 +17458,47 @@ watchlist_scan_state = { 'error': None } -def _watchlist_heartbeat_loop(): - """ - Background heartbeat loop that triggers watchlist scanning every 24 hours. - Allows for robust retry logic when conflicts occur with the wishlist processor. - """ - print("💓 [Auto-Watchlist] Heartbeat scheduler started") - - # Initial delay of 5 minutes - if watchlist_stop_event.wait(300): - return - - while not watchlist_stop_event.is_set(): - try: - # Run the processing logic - # Returns: 'completed', 'skipped_conflict', 'skipped_idle', 'skipped_active' - result = _process_watchlist_scan_automatically() - - if result == 'skipped_conflict': - # If conflict with wishlist, wait 10 minutes and retry - print("âŗ [Auto-Watchlist] Pausing 10 minutes before retry due to wishlist conflict") - - # Update UI timer - with watchlist_timer_lock: - watchlist_next_run_time = time.time() + 600.0 - - if watchlist_stop_event.wait(600): - break - else: - # Standard 24 hour cycle for success or other skips - with watchlist_timer_lock: - watchlist_next_run_time = time.time() + 86400.0 - - if watchlist_stop_event.wait(86400): - break - - except Exception as e: - print(f"❌ [Auto-Watchlist] Error in heartbeat loop: {e}") - # Wait 10 minutes on error before retrying to avoid spamming - with watchlist_timer_lock: - watchlist_next_run_time = time.time() + 600.0 - - if watchlist_stop_event.wait(600): - break - def start_watchlist_auto_scanning(): - """Start automatic watchlist scanning with robust heartbeat scheduler.""" - global watchlist_scheduler_thread, watchlist_next_run_time - - # Reset stop event - watchlist_stop_event.clear() + """Start automatic watchlist scanning with 5-minute initial delay (Timer-based like wishlist)""" + global watchlist_auto_timer, watchlist_next_run_time print("🚀 [Auto-Watchlist] Initializing automatic watchlist scanning...") with watchlist_timer_lock: - # Stop any existing thread - if watchlist_scheduler_thread is not None and watchlist_scheduler_thread.is_alive(): - print("âš ī¸ Watchlist scheduler already running") - return + # Stop any existing timer to prevent duplicates + if watchlist_auto_timer is not None: + watchlist_auto_timer.cancel() - print("🔄 Starting automatic watchlist heartbeat system (5 minute initial delay)") + print("🔄 Starting automatic watchlist scanning system (5 minute initial delay)") watchlist_next_run_time = time.time() + 300.0 # Set timestamp for countdown display - - watchlist_scheduler_thread = threading.Thread(target=_watchlist_heartbeat_loop, daemon=True, name="WatchlistHeartbeat") - watchlist_scheduler_thread.start() - print(f"✅ [Auto-Watchlist] Heartbeat thread started successfully") + watchlist_auto_timer = threading.Timer(300.0, _process_watchlist_scan_automatically) # 5 minutes + watchlist_auto_timer.daemon = True + watchlist_auto_timer.start() + print(f"✅ [Auto-Watchlist] Timer started successfully - will trigger in 5 minutes") def stop_watchlist_auto_scanning(): - """Stop automatic watchlist scanning and cleanup thread.""" - global watchlist_scheduler_thread, watchlist_auto_scanning, watchlist_auto_scanning_timestamp, watchlist_next_run_time + """Stop automatic watchlist scanning and cleanup timer.""" + global watchlist_auto_timer, watchlist_auto_scanning with watchlist_timer_lock: - # Signal thread to stop - watchlist_stop_event.set() - - if watchlist_scheduler_thread is not None: - watchlist_scheduler_thread = None + if watchlist_auto_timer is not None: + watchlist_auto_timer.cancel() + watchlist_auto_timer = None print("âšī¸ Stopped automatic watchlist scanning") watchlist_auto_scanning = False watchlist_auto_scanning_timestamp = 0 - watchlist_next_run_time = 0 def schedule_next_watchlist_scan(): - """ - Update the UI timestamp for the next run. - NOTE: The actual scheduling is now handled by _watchlist_heartbeat_loop. - This function exists to update the UI countdown. - """ - global watchlist_next_run_time + """Schedule next automatic watchlist scan in 24 hours.""" + global watchlist_auto_timer, watchlist_next_run_time with watchlist_timer_lock: - print("⏰ [UI Update] Updating next watchlist scan timestamp (handled by heartbeat)") + print("⏰ Scheduling next automatic watchlist scan in 24 hours") watchlist_next_run_time = time.time() + 86400.0 # Set timestamp for countdown display + watchlist_auto_timer = threading.Timer(86400.0, _process_watchlist_scan_automatically) # 24 hours + watchlist_auto_timer.daemon = True + watchlist_auto_timer.start() def _process_watchlist_scan_automatically(): """Main automatic scanning logic that runs in background thread.""" @@ -17563,19 +17510,19 @@ def _process_watchlist_scan_automatically(): with watchlist_timer_lock: if watchlist_auto_scanning: print("âš ī¸ Watchlist auto-scanning already running, skipping.") - # We return 'skipped_active' so heartbeat knows we're busy but alive - # Heartbeat will wait 24h as per normal cycle since it's already running - schedule_next_watchlist_scan() - return 'skipped_active' + schedule_next_watchlist_scan() + return # Check if wishlist processing is currently running (using smart detection) if is_wishlist_actually_processing(): print("đŸŽĩ Wishlist processing in progress, rescheduling watchlist scan for 10 minutes from now") - # Just update UI timer. Heartbeat loop will handle the 10m wait and retry. - global watchlist_next_run_time - with watchlist_timer_lock: - watchlist_next_run_time = time.time() + 600.0 - return 'skipped_conflict' + # Smart retry: don't wait 24 hours, just wait 10 minutes and try again + global watchlist_auto_timer, watchlist_next_run_time + watchlist_next_run_time = time.time() + 600.0 # Set timestamp for countdown display + watchlist_auto_timer = threading.Timer(600.0, _process_watchlist_scan_automatically) # 10 minutes + watchlist_auto_timer.daemon = True + watchlist_auto_timer.start() + return # Set flag and timestamp import time @@ -17599,7 +17546,7 @@ def _process_watchlist_scan_automatically(): watchlist_auto_scanning = False watchlist_auto_scanning_timestamp = 0 schedule_next_watchlist_scan() - return 'skipped_idle' + return if not spotify_client or not spotify_client.is_authenticated(): print("â„šī¸ [Auto-Watchlist] Spotify client not available or not authenticated.") @@ -17857,8 +17804,6 @@ def _process_watchlist_scan_automatically(): if total_added_to_wishlist > 0: add_activity_item("đŸ‘ī¸", "Watchlist Scan Complete", f"{total_added_to_wishlist} new tracks added to wishlist", "Now") - return 'completed' - except Exception as e: print(f"❌ Error in automatic watchlist scan: {e}") import traceback @@ -17868,10 +17813,11 @@ def _process_watchlist_scan_automatically(): watchlist_scan_state['error'] = str(e) finally: - # Always reset flag + # Always reset flag and schedule next scan with watchlist_timer_lock: watchlist_auto_scanning = False watchlist_auto_scanning_timestamp = 0 + schedule_next_watchlist_scan() print("✅ Automatic watchlist scanning initialized")