offline mode fixes
This commit is contained in:
parent
acb7e5f221
commit
36be192d44
@ -133,7 +133,7 @@ impl AuthManager {
|
|||||||
|
|
||||||
log::info!("[AuthManager] Connecting to server: {}", normalized_url);
|
log::info!("[AuthManager] Connecting to server: {}", normalized_url);
|
||||||
|
|
||||||
match self.http_client.get_json_with_retry::<PublicSystemInfo>(&endpoint).await {
|
match self.http_client.get_json_fast::<PublicSystemInfo>(&endpoint).await {
|
||||||
Ok(info) => {
|
Ok(info) => {
|
||||||
log::info!("[AuthManager] Connected to server: {} ({})", info.server_name, info.version);
|
log::info!("[AuthManager] Connected to server: {} ({})", info.server_name, info.version);
|
||||||
|
|
||||||
|
|||||||
@ -271,6 +271,21 @@ pub async fn resume_queued_downloads(
|
|||||||
(Arc::new(database.service()), target_dir)
|
(Arc::new(database.service()), target_dir)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Recover stale downloads: rows left in 'downloading' when the app was killed
|
||||||
|
// mid-transfer are orphaned — nothing ever restarts them, so they show as
|
||||||
|
// permanently "downloading". Reset them to 'pending' and clear the stale
|
||||||
|
// stream_url so they get re-resolved and restarted from scratch below.
|
||||||
|
let recover_query = Query::new(
|
||||||
|
"UPDATE downloads SET status = 'pending', stream_url = NULL, progress = 0, \
|
||||||
|
bytes_downloaded = 0, started_at = NULL \
|
||||||
|
WHERE status = 'downloading'",
|
||||||
|
);
|
||||||
|
match db_service.execute(recover_query).await {
|
||||||
|
Ok(n) if n > 0 => info!("[Catalog] Reset {} stale 'downloading' rows to pending", n),
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => warn!("[Catalog] Failed to reset stale downloads: {}", e),
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve each row's URL against the (now reachable) repository.
|
// Resolve each row's URL against the (now reachable) repository.
|
||||||
let repo_for_resolve = Arc::clone(&repo);
|
let repo_for_resolve = Arc::clone(&repo);
|
||||||
let outcome = resolve_pending_download_urls(
|
let outcome = resolve_pending_download_urls(
|
||||||
|
|||||||
@ -217,6 +217,45 @@ impl HttpClient {
|
|||||||
.map_err(|e| format!("Failed to parse JSON: {}", e))
|
.map_err(|e| format!("Failed to parse JSON: {}", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Make a GET request and deserialize JSON with a short timeout and no retries.
|
||||||
|
///
|
||||||
|
/// Intended for the initial "connect to server" probe on the login screen:
|
||||||
|
/// a wrong/unreachable URL must fail fast instead of burning through the
|
||||||
|
/// default 30s-per-attempt timeout and exponential backoff retries.
|
||||||
|
pub async fn get_json_fast<T: DeserializeOwned>(&self, url: &str) -> Result<T, String> {
|
||||||
|
// Short timeout so an unreachable host fails quickly.
|
||||||
|
const FAST_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
|
let request = self
|
||||||
|
.client
|
||||||
|
.get(url)
|
||||||
|
.timeout(FAST_TIMEOUT)
|
||||||
|
.build()
|
||||||
|
.map_err(|e| format!("Failed to build request: {}", e))?;
|
||||||
|
|
||||||
|
// No retry: connection failures on a wrong URL won't succeed on retry,
|
||||||
|
// they'd only multiply the wait the user sees before an error.
|
||||||
|
let response = self
|
||||||
|
.client
|
||||||
|
.execute(request)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Request failed: {}", e))?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let status = response.status();
|
||||||
|
let error_text = response
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|_| "Unknown error".to_string());
|
||||||
|
return Err(format!("HTTP {}: {}", status, error_text));
|
||||||
|
}
|
||||||
|
|
||||||
|
response
|
||||||
|
.json::<T>()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to parse JSON: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
/// Quick ping to check if a server is reachable (no retry)
|
/// Quick ping to check if a server is reachable (no retry)
|
||||||
pub async fn ping(&self, url: &str) -> bool {
|
pub async fn ping(&self, url: &str) -> bool {
|
||||||
let request = self.client.get(url)
|
let request = self.client.get(url)
|
||||||
|
|||||||
@ -934,9 +934,11 @@ pub fn run() {
|
|||||||
playback_mode_arc.clone(),
|
playback_mode_arc.clone(),
|
||||||
);
|
);
|
||||||
session_poller.set_event_emitter(event_emitter.clone());
|
session_poller.set_event_emitter(event_emitter.clone());
|
||||||
session_poller.start();
|
// Note: start() is deferred until after the connectivity monitor is
|
||||||
|
// created below, so the poller can report reachability from its first
|
||||||
|
// poll (it drives offline detection + recovery while the user is idle).
|
||||||
let session_poller_arc = Arc::new(session_poller);
|
let session_poller_arc = Arc::new(session_poller);
|
||||||
let session_poller_wrapper = SessionPollerWrapper(session_poller_arc);
|
let session_poller_wrapper = SessionPollerWrapper(session_poller_arc.clone());
|
||||||
app.manage(session_poller_wrapper);
|
app.manage(session_poller_wrapper);
|
||||||
|
|
||||||
// On Android, set up the MediaSession (lockscreen) handler and the
|
// On Android, set up the MediaSession (lockscreen) handler and the
|
||||||
@ -998,6 +1000,12 @@ pub fn run() {
|
|||||||
let mut connectivity_monitor = ConnectivityMonitor::new(http_client);
|
let mut connectivity_monitor = ConnectivityMonitor::new(http_client);
|
||||||
connectivity_monitor.set_app_handle(app.handle().clone());
|
connectivity_monitor.set_app_handle(app.handle().clone());
|
||||||
|
|
||||||
|
// Wire the connectivity reporter into the session poller so its
|
||||||
|
// continuous background polls drive reachability (offline detection
|
||||||
|
// + recovery) even when the user isn't browsing, then start it.
|
||||||
|
session_poller_arc.set_connectivity_reporter(connectivity_monitor.reporter());
|
||||||
|
session_poller_arc.start();
|
||||||
|
|
||||||
// Wrap in Arc for sharing with AuthManager
|
// Wrap in Arc for sharing with AuthManager
|
||||||
let connectivity_arc = Arc::new(tokio::sync::Mutex::new(connectivity_monitor));
|
let connectivity_arc = Arc::new(tokio::sync::Mutex::new(connectivity_monitor));
|
||||||
let connectivity_wrapper = ConnectivityMonitorWrapper(connectivity_arc.clone());
|
let connectivity_wrapper = ConnectivityMonitorWrapper(connectivity_arc.clone());
|
||||||
|
|||||||
@ -32,6 +32,13 @@ pub struct SessionPollerManager {
|
|||||||
jellyfin_client: Arc<Mutex<Option<JellyfinClient>>>,
|
jellyfin_client: Arc<Mutex<Option<JellyfinClient>>>,
|
||||||
playback_mode_manager: Arc<PlaybackModeManager>,
|
playback_mode_manager: Arc<PlaybackModeManager>,
|
||||||
event_emitter: Arc<Mutex<Option<Arc<dyn PlayerEventEmitter>>>>,
|
event_emitter: Arc<Mutex<Option<Arc<dyn PlayerEventEmitter>>>>,
|
||||||
|
/// Optional connectivity reporter. The session poller is the one piece of
|
||||||
|
/// server traffic that runs continuously even when the user is idle (not
|
||||||
|
/// browsing the library), so feeding its poll outcomes into the reporter is
|
||||||
|
/// what lets the app detect going offline — and, crucially, recover when the
|
||||||
|
/// server returns — without any user interaction. Repository traffic alone
|
||||||
|
/// can't do this because it only happens while browsing.
|
||||||
|
connectivity_reporter: Arc<Mutex<Option<crate::connectivity::ConnectivityReporter>>>,
|
||||||
|
|
||||||
// Polling state
|
// Polling state
|
||||||
is_running: Arc<AtomicBool>,
|
is_running: Arc<AtomicBool>,
|
||||||
@ -52,6 +59,7 @@ impl SessionPollerManager {
|
|||||||
jellyfin_client,
|
jellyfin_client,
|
||||||
playback_mode_manager,
|
playback_mode_manager,
|
||||||
event_emitter: Arc::new(Mutex::new(None)),
|
event_emitter: Arc::new(Mutex::new(None)),
|
||||||
|
connectivity_reporter: Arc::new(Mutex::new(None)),
|
||||||
is_running: Arc::new(AtomicBool::new(false)),
|
is_running: Arc::new(AtomicBool::new(false)),
|
||||||
current_hint: Arc::new(RwLock::new(PollingHint::Normal)),
|
current_hint: Arc::new(RwLock::new(PollingHint::Normal)),
|
||||||
current_interval_ms: Arc::new(AtomicU64::new(10000)), // Default 10s
|
current_interval_ms: Arc::new(AtomicU64::new(10000)), // Default 10s
|
||||||
@ -64,6 +72,13 @@ impl SessionPollerManager {
|
|||||||
*self.event_emitter.lock_safe() = Some(emitter);
|
*self.event_emitter.lock_safe() = Some(emitter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wire the connectivity reporter so each poll outcome updates reachability.
|
||||||
|
/// A successful poll recovers the app to online instantly; sustained poll
|
||||||
|
/// failures flip it offline (subject to the reporter's debounce window).
|
||||||
|
pub fn set_connectivity_reporter(&self, reporter: crate::connectivity::ConnectivityReporter) {
|
||||||
|
*self.connectivity_reporter.lock_safe() = Some(reporter);
|
||||||
|
}
|
||||||
|
|
||||||
/// Start the background polling thread
|
/// Start the background polling thread
|
||||||
pub fn start(&self) {
|
pub fn start(&self) {
|
||||||
if self.is_running.swap(true, Ordering::Relaxed) {
|
if self.is_running.swap(true, Ordering::Relaxed) {
|
||||||
@ -77,6 +92,7 @@ impl SessionPollerManager {
|
|||||||
let client = self.jellyfin_client.clone();
|
let client = self.jellyfin_client.clone();
|
||||||
let mode_manager = self.playback_mode_manager.clone();
|
let mode_manager = self.playback_mode_manager.clone();
|
||||||
let emitter = self.event_emitter.clone();
|
let emitter = self.event_emitter.clone();
|
||||||
|
let connectivity_reporter = self.connectivity_reporter.clone();
|
||||||
let is_running = self.is_running.clone();
|
let is_running = self.is_running.clone();
|
||||||
let hint = self.current_hint.clone();
|
let hint = self.current_hint.clone();
|
||||||
let interval_ms = self.current_interval_ms.clone();
|
let interval_ms = self.current_interval_ms.clone();
|
||||||
@ -96,18 +112,35 @@ impl SessionPollerManager {
|
|||||||
|
|
||||||
debug!("[SessionPoller] Polling with interval: {}ms", new_interval);
|
debug!("[SessionPoller] Polling with interval: {}ms", new_interval);
|
||||||
|
|
||||||
// Fetch sessions
|
// Fetch sessions. `had_client` distinguishes "server didn't
|
||||||
let sessions_result = rt.block_on(async {
|
// answer" from "no client configured" so we only feed real
|
||||||
|
// request outcomes into the connectivity reporter.
|
||||||
|
let (sessions_result, had_client) = rt.block_on(async {
|
||||||
let client_opt = client.lock_safe().clone();
|
let client_opt = client.lock_safe().clone();
|
||||||
match client_opt {
|
match client_opt {
|
||||||
Some(c) => c.get_sessions().await,
|
Some(c) => (c.get_sessions().await, true),
|
||||||
None => {
|
None => {
|
||||||
debug!("[SessionPoller] Jellyfin client not configured, skipping poll");
|
debug!("[SessionPoller] Jellyfin client not configured, skipping poll");
|
||||||
Ok(Vec::new())
|
(Ok(Vec::new()), false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Drive the connectivity reporter from this poll's outcome. This
|
||||||
|
// is what recovers the app to online when the server returns
|
||||||
|
// while the user is idle, and detects going offline when no
|
||||||
|
// library browsing is happening. See connectivity/mod.rs.
|
||||||
|
if had_client {
|
||||||
|
if let Some(reporter) = connectivity_reporter.lock_safe().clone() {
|
||||||
|
rt.block_on(async {
|
||||||
|
match &sessions_result {
|
||||||
|
Ok(_) => reporter.report_success().await,
|
||||||
|
Err(e) => reporter.report_network_failure(Some(e.clone())).await,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Emit event if successful
|
// Emit event if successful
|
||||||
match sessions_result {
|
match sessions_result {
|
||||||
Ok(sessions) => {
|
Ok(sessions) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user