Isolate service client initialization so one failure doesn't break the entire app

This commit is contained in:
Broque Thomas 2026-03-11 11:25:15 -07:00
parent bc22bdca07
commit e42f373f80

View file

@ -259,38 +259,81 @@ def _make_context_key(username, filename):
return f"{username}::{normalized}" return f"{username}::{normalized}"
# --- Initialize Core Application Components --- # --- Initialize Core Application Components ---
# Each client is initialized independently so one failure doesn't take down everything.
# Previously, a single exception set ALL clients to None, breaking the entire app.
print("🚀 Initializing SoulSync services for Web UI...") print("🚀 Initializing SoulSync services for Web UI...")
spotify_client = plex_client = jellyfin_client = navidrome_client = soulseek_client = tidal_client = matching_engine = sync_service = web_scan_manager = None
try: try:
spotify_client = SpotifyClient() spotify_client = SpotifyClient()
print(" ✅ Spotify client initialized")
except Exception as e:
print(f" ⚠️ Spotify client failed to initialize: {e}")
try:
plex_client = PlexClient() plex_client = PlexClient()
print(" ✅ Plex client initialized")
except Exception as e:
print(f" ⚠️ Plex client failed to initialize: {e}")
try:
jellyfin_client = JellyfinClient() jellyfin_client = JellyfinClient()
print(" ✅ Jellyfin client initialized")
except Exception as e:
print(f" ⚠️ Jellyfin client failed to initialize: {e}")
try:
navidrome_client = NavidromeClient() navidrome_client = NavidromeClient()
# Use DownloadOrchestrator instead of SoulseekClient directly (routes between Soulseek/YouTube) print(" ✅ Navidrome client initialized")
except Exception as e:
print(f" ⚠️ Navidrome client failed to initialize: {e}")
try:
soulseek_client = DownloadOrchestrator() soulseek_client = DownloadOrchestrator()
print(" ✅ Download orchestrator initialized")
except Exception as e:
print(f" ⚠️ Download orchestrator failed to initialize: {e}")
try:
tidal_client = TidalClient() tidal_client = TidalClient()
print(" ✅ Tidal client initialized")
except Exception as e:
print(f" ⚠️ Tidal client failed to initialize: {e}")
try:
matching_engine = MusicMatchingEngine() matching_engine = MusicMatchingEngine()
print(" ✅ Matching engine initialized")
except Exception as e:
print(f" ⚠️ Matching engine failed to initialize: {e}")
try:
sync_service = PlaylistSyncService(spotify_client, plex_client, soulseek_client, jellyfin_client, navidrome_client) sync_service = PlaylistSyncService(spotify_client, plex_client, soulseek_client, jellyfin_client, navidrome_client)
print(" ✅ Playlist sync service initialized")
# Inject shutdown check callback into YouTube and Tidal clients (avoids circular imports) except Exception as e:
# The callback uses the global IS_SHUTTING_DOWN flag from this module print(f" ⚠️ Playlist sync service failed to initialize: {e}")
# Inject shutdown check callback into YouTube and Tidal clients (avoids circular imports)
if soulseek_client:
if hasattr(soulseek_client, 'youtube'): if hasattr(soulseek_client, 'youtube'):
soulseek_client.youtube.set_shutdown_check(lambda: IS_SHUTTING_DOWN) soulseek_client.youtube.set_shutdown_check(lambda: IS_SHUTTING_DOWN)
print("✅ Configured YouTube client shutdown callback") print(" ✅ Configured YouTube client shutdown callback")
if hasattr(soulseek_client, 'tidal'): if hasattr(soulseek_client, 'tidal'):
soulseek_client.tidal.set_shutdown_check(lambda: IS_SHUTTING_DOWN) soulseek_client.tidal.set_shutdown_check(lambda: IS_SHUTTING_DOWN)
print("✅ Configured Tidal download client shutdown callback") print(" ✅ Configured Tidal download client shutdown callback")
# Initialize web scan manager for automatic post-download scanning # Initialize web scan manager for automatic post-download scanning
try:
media_clients = { media_clients = {
'plex_client': plex_client, 'plex_client': plex_client,
'jellyfin_client': jellyfin_client, 'jellyfin_client': jellyfin_client,
'navidrome_client': navidrome_client 'navidrome_client': navidrome_client
} }
web_scan_manager = WebScanManager(media_clients, delay_seconds=60) web_scan_manager = WebScanManager(media_clients, delay_seconds=60)
print("✅ Core service clients and scan manager initialized.") print(" ✅ Web scan manager initialized")
except Exception as e: except Exception as e:
print(f"🔴 FATAL: Error initializing service clients: {e}") print(f" ⚠️ Web scan manager failed to initialize: {e}")
spotify_client = plex_client = jellyfin_client = navidrome_client = soulseek_client = tidal_client = matching_engine = sync_service = web_scan_manager = None
print("✅ Core service initialization complete.")
# --- Automation Engine --- # --- Automation Engine ---
try: try:
@ -3519,8 +3562,8 @@ STATUS_CACHE_TTL = 120 # Cache for 2 minutes (reduces API calls while staying f
@app.route('/status') @app.route('/status')
def get_status(): def get_status():
if not all([spotify_client, plex_client, jellyfin_client, soulseek_client, config_manager]): if not config_manager:
return jsonify({"error": "Core services not initialized."}), 500 return jsonify({"error": "Server configuration manager is not initialized."}), 500
try: try:
import time import time
@ -3529,11 +3572,10 @@ def get_status():
# Test Spotify - with caching to avoid excessive API calls # Test Spotify - with caching to avoid excessive API calls
if current_time - _status_cache_timestamps['spotify'] > STATUS_CACHE_TTL: if current_time - _status_cache_timestamps['spotify'] > STATUS_CACHE_TTL:
# Check is_rate_limited() first — this calls _is_globally_rate_limited() which # Guard against spotify_client being None (partial init)
# records ban-end timestamp if the ban just expired, enabling cooldown detection. is_rate_limited = spotify_client.is_rate_limited() if spotify_client else False
is_rate_limited = spotify_client.is_rate_limited() rate_limit_info = spotify_client.get_rate_limit_info() if (spotify_client and is_rate_limited) else None
rate_limit_info = spotify_client.get_rate_limit_info() if is_rate_limited else None cooldown_remaining = spotify_client.get_post_ban_cooldown_remaining() if spotify_client else 0
cooldown_remaining = spotify_client.get_post_ban_cooldown_remaining()
if is_rate_limited or cooldown_remaining > 0: if is_rate_limited or cooldown_remaining > 0:
# During rate limit or post-ban cooldown, skip the auth probe entirely. # During rate limit or post-ban cooldown, skip the auth probe entirely.
@ -3542,7 +3584,7 @@ def get_status():
spotify_response_time = 0 spotify_response_time = 0
else: else:
spotify_start = time.time() spotify_start = time.time()
spotify_connected = spotify_client.is_spotify_authenticated() spotify_connected = spotify_client.is_spotify_authenticated() if spotify_client else False
spotify_response_time = (time.time() - spotify_start) * 1000 spotify_response_time = (time.time() - spotify_start) * 1000
music_source = 'spotify' if spotify_connected else 'itunes' music_source = 'spotify' if spotify_connected else 'itunes'
@ -3561,13 +3603,13 @@ def get_status():
if current_time - _status_cache_timestamps['media_server'] > STATUS_CACHE_TTL: if current_time - _status_cache_timestamps['media_server'] > STATUS_CACHE_TTL:
media_server_start = time.time() media_server_start = time.time()
media_server_status = False media_server_status = False
if active_server == "plex": if active_server == "plex" and plex_client:
# Use existing instance - has 30s internal connection cache # Use existing instance - has 30s internal connection cache
media_server_status = plex_client.is_connected() media_server_status = plex_client.is_connected()
elif active_server == "jellyfin": elif active_server == "jellyfin" and jellyfin_client:
# Use existing instance - has internal connection caching # Use existing instance - has internal connection caching
media_server_status = jellyfin_client.is_connected() media_server_status = jellyfin_client.is_connected()
elif active_server == "navidrome": elif active_server == "navidrome" and navidrome_client:
# Use existing instance # Use existing instance
media_server_status = navidrome_client.is_connected() media_server_status = navidrome_client.is_connected()
media_server_response_time = (time.time() - media_server_start) * 1000 media_server_response_time = (time.time() - media_server_start) * 1000
@ -3583,7 +3625,7 @@ def get_status():
if current_time - _status_cache_timestamps['soulseek'] > STATUS_CACHE_TTL: if current_time - _status_cache_timestamps['soulseek'] > STATUS_CACHE_TTL:
soulseek_start = time.time() soulseek_start = time.time()
try: try:
soulseek_status = run_async(soulseek_client.check_connection()) soulseek_status = run_async(soulseek_client.check_connection()) if soulseek_client else False
except Exception: except Exception:
soulseek_status = False soulseek_status = False
soulseek_response_time = (time.time() - soulseek_start) * 1000 soulseek_response_time = (time.time() - soulseek_start) * 1000
@ -3966,17 +4008,26 @@ def handle_settings():
add_activity_item("⚙️", "Settings Updated", f"{services_text} configuration saved", "Now") add_activity_item("⚙️", "Settings Updated", f"{services_text} configuration saved", "Now")
spotify_client.reload_config() # Reload service clients with new settings (guard against None from partial init)
plex_client.server = None if spotify_client:
jellyfin_client.reload_config() spotify_client.reload_config()
navidrome_client.reload_config() if plex_client:
plex_client.server = None
if jellyfin_client:
jellyfin_client.reload_config()
if navidrome_client:
navidrome_client.reload_config()
# Reload orchestrator settings (download source mode, hybrid_primary, etc.) # Reload orchestrator settings (download source mode, hybrid_primary, etc.)
soulseek_client.reload_settings() if soulseek_client:
# Reload YouTube client settings (rate limiting, cookies) soulseek_client.reload_settings()
if hasattr(soulseek_client, 'youtube'): # Reload YouTube client settings (rate limiting, cookies)
soulseek_client.youtube.reload_settings() if hasattr(soulseek_client, 'youtube'):
soulseek_client.youtube.reload_settings()
# FIX: Re-instantiate the global tidal_client to pick up new settings # FIX: Re-instantiate the global tidal_client to pick up new settings
tidal_client = TidalClient() try:
tidal_client = TidalClient()
except Exception:
pass # Keep existing tidal_client if re-init fails
# Reload enrichment worker clients for key-based services # Reload enrichment worker clients for key-based services
if lastfm_worker: if lastfm_worker:
lastfm_worker._init_client() lastfm_worker._init_client()
@ -4789,7 +4840,7 @@ def test_connection_endpoint():
current_time = time.time() current_time = time.time()
if service == 'spotify': if service == 'spotify':
_status_cache['spotify']['connected'] = True _status_cache['spotify']['connected'] = True
_status_cache['spotify']['source'] = 'spotify' if spotify_client.is_spotify_authenticated() else 'itunes' _status_cache['spotify']['source'] = 'spotify' if (spotify_client and spotify_client.is_spotify_authenticated()) else 'itunes'
_status_cache_timestamps['spotify'] = current_time _status_cache_timestamps['spotify'] = current_time
print("✅ Updated Spotify status cache after successful test") print("✅ Updated Spotify status cache after successful test")
elif service in ['plex', 'jellyfin', 'navidrome']: elif service in ['plex', 'jellyfin', 'navidrome']:
@ -4839,7 +4890,7 @@ def test_dashboard_connection_endpoint():
current_time = time.time() current_time = time.time()
if service == 'spotify': if service == 'spotify':
_status_cache['spotify']['connected'] = True _status_cache['spotify']['connected'] = True
_status_cache['spotify']['source'] = 'spotify' if spotify_client.is_spotify_authenticated() else 'itunes' _status_cache['spotify']['source'] = 'spotify' if (spotify_client and spotify_client.is_spotify_authenticated()) else 'itunes'
_status_cache_timestamps['spotify'] = current_time _status_cache_timestamps['spotify'] = current_time
print("✅ Updated Spotify status cache after successful dashboard test") print("✅ Updated Spotify status cache after successful dashboard test")
elif service in ['plex', 'jellyfin', 'navidrome']: elif service in ['plex', 'jellyfin', 'navidrome']: