Isolate service client initialization so one failure doesn't break the entire app
This commit is contained in:
parent
bc22bdca07
commit
e42f373f80
1 changed files with 84 additions and 33 deletions
117
web_server.py
117
web_server.py
|
|
@ -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']:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue