4444# Swaparr processing thread
4545swaparr_thread = None
4646
47+ # Background refresher for Prowlarr statistics
48+ prowlarr_stats_thread = None
49+
4750# Define which apps have background processing cycles
4851CYCLICAL_APP_TYPES = ["sonarr" , "radarr" , "lidarr" , "readarr" , "whisparr" , "eros" ]
4952
@@ -693,6 +696,16 @@ def shutdown_threads():
693696 else :
694697 logger .info ("Hourly API cap scheduler stopped" )
695698
699+ # Stop the Prowlarr stats refresher
700+ global prowlarr_stats_thread
701+ if prowlarr_stats_thread and prowlarr_stats_thread .is_alive ():
702+ logger .info ("Waiting for Prowlarr stats refresher to stop..." )
703+ prowlarr_stats_thread .join (timeout = 5.0 )
704+ if prowlarr_stats_thread .is_alive ():
705+ logger .warning ("Prowlarr stats refresher did not stop gracefully" )
706+ else :
707+ logger .info ("Prowlarr stats refresher stopped" )
708+
696709 # Stop the Swaparr processing thread
697710 global swaparr_thread
698711 if swaparr_thread and swaparr_thread .is_alive ():
@@ -776,6 +789,52 @@ def hourly_cap_scheduler_loop():
776789
777790 logger .info ("Hourly API cap scheduler stopped" )
778791
792+ def prowlarr_stats_loop ():
793+ """Background loop to refresh Prowlarr statistics cache every 5 minutes.
794+ Runs independently of the frontend and does nothing if Prowlarr is not configured or disabled.
795+ """
796+ refresher_logger = get_logger ("prowlarr" )
797+ refresher_logger .info ("Prowlarr stats refresher thread started" )
798+ try :
799+ from src .primary .settings_manager import load_settings
800+ # Import inside loop target to avoid circular issues at module import time
801+ from src .primary .apps import prowlarr_routes as prow
802+
803+ refresh_interval_seconds = 300 # 5 minutes
804+
805+ # Do an immediate pass on start
806+ while not stop_event .is_set ():
807+ try :
808+ settings = load_settings ("prowlarr" )
809+ api_url = (settings .get ("api_url" , "" ) or "" ).strip ()
810+ api_key = (settings .get ("api_key" , "" ) or "" ).strip ()
811+ enabled = settings .get ("enabled" , True )
812+
813+ if not api_url or not api_key or not enabled :
814+ # Not configured or disabled; sleep a bit and check again
815+ if stop_event .wait (60 ):
816+ break
817+ continue
818+
819+ # Trigger cache update (safe even if cache is warm)
820+ try :
821+ prow ._update_stats_cache ()
822+ except Exception as e :
823+ refresher_logger .error (f"Prowlarr stats refresh error: { e } " , exc_info = True )
824+
825+ # Sleep until next refresh or until stop requested
826+ if stop_event .wait (refresh_interval_seconds ):
827+ break
828+
829+ except Exception as loop_error :
830+ refresher_logger .error (f"Unexpected error in Prowlarr stats refresher: { loop_error } " , exc_info = True )
831+ # Back off briefly to avoid tight error loops
832+ if stop_event .wait (60 ):
833+ break
834+ finally :
835+ refresher_logger .info ("Prowlarr stats refresher thread stopped" )
836+
837+
779838def swaparr_app_loop ():
780839 """Dedicated Swaparr processing loop that follows same patterns as other apps"""
781840 swaparr_logger = get_logger ("swaparr" )
@@ -864,7 +923,7 @@ def swaparr_app_loop():
864923 # Log progress every 30 seconds (like other apps)
865924 if elapsed > 0 and elapsed % 30 == 0 :
866925 swaparr_logger .debug (f"Still sleeping, { sleep_duration - elapsed } seconds remaining before next cycle..." )
867-
926+
868927 except Exception as e :
869928 swaparr_logger .error (f"Unexpected error in Swaparr loop: { e } " , exc_info = True )
870929 # Sleep briefly to avoid spinning in case of repeated errors
@@ -875,8 +934,6 @@ def swaparr_app_loop():
875934
876935 swaparr_logger .info ("Swaparr thread stopped" )
877936
878-
879-
880937def start_hourly_cap_scheduler ():
881938 """Start the hourly API cap scheduler thread"""
882939 global hourly_cap_scheduler_thread
@@ -895,6 +952,20 @@ def start_hourly_cap_scheduler():
895952
896953 logger .info (f"Hourly API cap scheduler started. Thread is alive: { hourly_cap_scheduler_thread .is_alive ()} " )
897954
955+ def start_prowlarr_stats_thread ():
956+ """Start the Prowlarr statistics refresher thread (5-minute cadence)."""
957+ global prowlarr_stats_thread
958+ if prowlarr_stats_thread and prowlarr_stats_thread .is_alive ():
959+ logger .info ("Prowlarr stats refresher already running" )
960+ return
961+ prowlarr_stats_thread = threading .Thread (
962+ target = prowlarr_stats_loop ,
963+ name = "ProwlarrStatsRefresher" ,
964+ daemon = True ,
965+ )
966+ prowlarr_stats_thread .start ()
967+ logger .info (f"Prowlarr stats refresher started. Thread is alive: { prowlarr_stats_thread .is_alive ()} " )
968+
898969def start_swaparr_thread ():
899970 """Start the dedicated Swaparr processing thread"""
900971 global swaparr_thread
@@ -933,7 +1004,14 @@ def start_huntarr():
9331004 logger .info ("Swaparr thread started successfully" )
9341005 except Exception as e :
9351006 logger .error (f"Failed to start Swaparr thread: { e } " )
936-
1007+
1008+ # Start the Prowlarr stats refresher
1009+ try :
1010+ start_prowlarr_stats_thread ()
1011+ logger .info ("Prowlarr stats refresher started successfully" )
1012+ except Exception as e :
1013+ logger .error (f"Failed to start Prowlarr stats refresher: { e } " )
1014+
9371015 # Start the scheduler engine
9381016 try :
9391017 start_scheduler ()
0 commit comments