use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; use tokio::sync::RwLock; use tauri::{AppHandle, Emitter}; use serde::{Serialize, Deserialize}; use crate::jellyfin::http_client::HttpClient; // Adaptive polling intervals (matches TypeScript) const AUTO_CHECK_INTERVAL_MS: u64 = 30000; // 30 seconds when online const RETRY_CHECK_INTERVAL_MS: u64 = 5000; // 5 seconds when offline /// Connectivity status #[derive(specta::Type, Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ConnectivityStatus { /// Whether the Jellyfin server is reachable pub is_server_reachable: bool, /// Last time we checked server reachability (ISO 8601 string) pub last_checked: Option, /// Error message from last connectivity check pub connection_error: Option, /// Whether we're currently checking connectivity pub is_checking: bool, } impl Default for ConnectivityStatus { fn default() -> Self { Self { // Start optimistic - assume online until proven otherwise // This prevents the app from appearing offline on startup is_server_reachable: true, last_checked: None, connection_error: None, is_checking: false, } } } /// Connectivity change event emitted to frontend #[derive(specta::Type, Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] struct ConnectivityChangeEvent { is_reachable: bool, } /// Connectivity monitor for tracking server reachability pub struct ConnectivityMonitor { server_url: Arc>>, http_client: Arc, status: Arc>, is_monitoring: Arc, app_handle: Option, } impl ConnectivityMonitor { /// Create a new connectivity monitor pub fn new(http_client: HttpClient) -> Self { Self { server_url: Arc::new(RwLock::new(None)), http_client: Arc::new(http_client), status: Arc::new(RwLock::new(ConnectivityStatus::default())), is_monitoring: Arc::new(AtomicBool::new(false)), app_handle: None, } } /// Set the Tauri app handle for event emission pub fn set_app_handle(&mut self, app_handle: AppHandle) { self.app_handle = Some(app_handle); } /// Update the server URL pub async fn set_server_url(&self, url: String) { log::info!("[ConnectivityMonitor] Setting server URL: {}", url); let mut server_url = self.server_url.write().await; *server_url = Some(url.clone()); drop(server_url); // Check new server immediately log::info!("[ConnectivityMonitor] Checking reachability of new server..."); let is_reachable = self.check_reachability().await; log::info!("[ConnectivityMonitor] New server is {}", if is_reachable { "REACHABLE" } else { "UNREACHABLE" }); } /// Get current connectivity status pub async fn get_status(&self) -> ConnectivityStatus { self.status.read().await.clone() } /// Check if the Jellyfin server is reachable pub async fn check_reachability(&self) -> bool { // Mark as checking { let mut status = self.status.write().await; status.is_checking = true; } let server_url = self.server_url.read().await.clone(); if server_url.is_none() { log::warn!("[ConnectivityMonitor] Cannot check reachability: No server URL configured"); let mut status = self.status.write().await; status.is_server_reachable = false; status.connection_error = Some("No server URL configured".to_string()); status.is_checking = false; return false; } let url = server_url.unwrap(); let ping_url = format!("{}/System/Info/Public", url); // Store previous reachability state let was_reachable = { let status = self.status.read().await; status.is_server_reachable }; log::debug!("[ConnectivityMonitor] Pinging server: {}", ping_url); // Attempt to ping the server let is_reachable = self.http_client.ping(&ping_url).await; log::debug!( "[ConnectivityMonitor] Ping result: {} (was: {})", if is_reachable { "SUCCESS" } else { "FAILED" }, if was_reachable { "reachable" } else { "unreachable" } ); // Update status { let mut status = self.status.write().await; status.is_server_reachable = is_reachable; status.last_checked = Some(chrono::Utc::now().to_rfc3339()); status.connection_error = if is_reachable { None } else { Some("Server unreachable".to_string()) }; status.is_checking = false; } // Emit events if reachability changed if is_reachable != was_reachable { self.emit_connectivity_change(is_reachable).await; } // Emit reconnection event if is_reachable && !was_reachable { self.emit_server_reconnected().await; } is_reachable } /// Mark server as reachable (called after successful API call) pub async fn mark_reachable(&self) { let mut status = self.status.write().await; let was_reachable = status.is_server_reachable; status.is_server_reachable = true; status.last_checked = Some(chrono::Utc::now().to_rfc3339()); status.connection_error = None; drop(status); if !was_reachable { log::info!("[ConnectivityMonitor] Server marked as reachable (was unreachable)"); self.emit_connectivity_change(true).await; self.emit_server_reconnected().await; } } /// Mark server as unreachable (called after failed API call) pub async fn mark_unreachable(&self, error: Option) { let mut status = self.status.write().await; let was_reachable = status.is_server_reachable; status.is_server_reachable = false; status.last_checked = Some(chrono::Utc::now().to_rfc3339()); status.connection_error = error.or_else(|| Some("Server unreachable".to_string())); let error_msg = status.connection_error.clone().unwrap_or_default(); drop(status); if was_reachable { log::warn!("[ConnectivityMonitor] Server marked as unreachable (was reachable): {}", error_msg); self.emit_connectivity_change(false).await; } } /// Start monitoring connectivity with adaptive polling pub async fn start_monitoring(&self) { if self.is_monitoring.swap(true, Ordering::SeqCst) { log::info!("[ConnectivityMonitor] Already monitoring"); return; } log::info!("[ConnectivityMonitor] Starting connectivity monitoring"); // Perform immediate check before starting background task // This ensures we get an accurate state right away instead of assuming offline let is_reachable = self.check_reachability().await; log::info!("[ConnectivityMonitor] Initial connectivity check: {}", if is_reachable { "ONLINE" } else { "OFFLINE" }); // Clone Arc references for the background task let status = Arc::clone(&self.status); let is_monitoring = Arc::clone(&self.is_monitoring); let server_url = Arc::clone(&self.server_url); let http_client = Arc::clone(&self.http_client); let self_clone = Arc::new(ConnectivityMonitorHandle { server_url, http_client, status, app_handle: self.app_handle.clone(), }); // Spawn background monitoring task tokio::spawn(async move { while is_monitoring.load(Ordering::SeqCst) { // Determine interval based on current reachability let interval_ms = { let status = self_clone.status.read().await; if status.is_server_reachable { AUTO_CHECK_INTERVAL_MS } else { RETRY_CHECK_INTERVAL_MS } }; // Wait for the interval tokio::time::sleep(Duration::from_millis(interval_ms)).await; // Check if still monitoring if !is_monitoring.load(Ordering::SeqCst) { break; } // Perform connectivity check let _ = self_clone.check_reachability().await; } log::info!("[ConnectivityMonitor] Stopped monitoring"); }); } /// Stop monitoring connectivity pub fn stop_monitoring(&self) { log::info!("[ConnectivityMonitor] Stopping connectivity monitoring"); self.is_monitoring.store(false, Ordering::SeqCst); } /// Emit connectivity change event to frontend async fn emit_connectivity_change(&self, is_reachable: bool) { if let Some(app_handle) = &self.app_handle { let event = ConnectivityChangeEvent { is_reachable }; if let Err(e) = app_handle.emit("connectivity:changed", event) { log::error!("[ConnectivityMonitor] Failed to emit connectivity change event: {}", e); } else { log::info!("[ConnectivityMonitor] Emitted connectivity change: {}", is_reachable); } } } /// Emit server reconnected event to frontend async fn emit_server_reconnected(&self) { if let Some(app_handle) = &self.app_handle { if let Err(e) = app_handle.emit("connectivity:reconnected", ()) { log::error!("[ConnectivityMonitor] Failed to emit reconnection event: {}", e); } else { log::info!("[ConnectivityMonitor] Emitted server reconnected event"); } } } } /// Handle for the background monitoring task struct ConnectivityMonitorHandle { server_url: Arc>>, http_client: Arc, status: Arc>, app_handle: Option, } impl ConnectivityMonitorHandle { async fn check_reachability(&self) -> bool { let server_url = self.server_url.read().await.clone(); if server_url.is_none() { return false; } let url = server_url.unwrap(); let ping_url = format!("{}/System/Info/Public", url); // Store previous reachability state let was_reachable = { let status = self.status.read().await; status.is_server_reachable }; // Attempt to ping the server let is_reachable = self.http_client.ping(&ping_url).await; // Update status { let mut status = self.status.write().await; status.is_server_reachable = is_reachable; status.last_checked = Some(chrono::Utc::now().to_rfc3339()); status.connection_error = if is_reachable { None } else { Some("Server unreachable".to_string()) }; } // Emit events if reachability changed if is_reachable != was_reachable { self.emit_connectivity_change(is_reachable).await; } // Emit reconnection event if is_reachable && !was_reachable { self.emit_server_reconnected().await; } is_reachable } async fn emit_connectivity_change(&self, is_reachable: bool) { if let Some(app_handle) = &self.app_handle { let event = ConnectivityChangeEvent { is_reachable }; if let Err(e) = app_handle.emit("connectivity:changed", event) { log::error!("[ConnectivityMonitor] Failed to emit connectivity change event: {}", e); } } } async fn emit_server_reconnected(&self) { if let Some(app_handle) = &self.app_handle { if let Err(e) = app_handle.emit("connectivity:reconnected", ()) { log::error!("[ConnectivityMonitor] Failed to emit reconnection event: {}", e); } } } } #[cfg(test)] mod tests { use super::*; use crate::jellyfin::http_client::HttpConfig; #[test] fn test_intervals() { // Verify intervals match TypeScript assert_eq!(AUTO_CHECK_INTERVAL_MS, 30000); assert_eq!(RETRY_CHECK_INTERVAL_MS, 5000); } #[tokio::test] async fn test_default_status() { let status = ConnectivityStatus::default(); // Default is now optimistic (assume online until proven otherwise) assert!(status.is_server_reachable); assert!(status.last_checked.is_none()); assert!(status.connection_error.is_none()); assert!(!status.is_checking); } }