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);
|
||||
|
||||
match self.http_client.get_json_with_retry::<PublicSystemInfo>(&endpoint).await {
|
||||
match self.http_client.get_json_fast::<PublicSystemInfo>(&endpoint).await {
|
||||
Ok(info) => {
|
||||
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)
|
||||
};
|
||||
|
||||
// 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.
|
||||
let repo_for_resolve = Arc::clone(&repo);
|
||||
let outcome = resolve_pending_download_urls(
|
||||
|
||||
@ -217,6 +217,45 @@ impl HttpClient {
|
||||
.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)
|
||||
pub async fn ping(&self, url: &str) -> bool {
|
||||
let request = self.client.get(url)
|
||||
|
||||
@ -934,9 +934,11 @@ pub fn run() {
|
||||
playback_mode_arc.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_wrapper = SessionPollerWrapper(session_poller_arc);
|
||||
let session_poller_wrapper = SessionPollerWrapper(session_poller_arc.clone());
|
||||
app.manage(session_poller_wrapper);
|
||||
|
||||
// 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);
|
||||
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
|
||||
let connectivity_arc = Arc::new(tokio::sync::Mutex::new(connectivity_monitor));
|
||||
let connectivity_wrapper = ConnectivityMonitorWrapper(connectivity_arc.clone());
|
||||
|
||||
@ -32,6 +32,13 @@ pub struct SessionPollerManager {
|
||||
jellyfin_client: Arc<Mutex<Option<JellyfinClient>>>,
|
||||
playback_mode_manager: Arc<PlaybackModeManager>,
|
||||
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
|
||||
is_running: Arc<AtomicBool>,
|
||||
@ -52,6 +59,7 @@ impl SessionPollerManager {
|
||||
jellyfin_client,
|
||||
playback_mode_manager,
|
||||
event_emitter: Arc::new(Mutex::new(None)),
|
||||
connectivity_reporter: Arc::new(Mutex::new(None)),
|
||||
is_running: Arc::new(AtomicBool::new(false)),
|
||||
current_hint: Arc::new(RwLock::new(PollingHint::Normal)),
|
||||
current_interval_ms: Arc::new(AtomicU64::new(10000)), // Default 10s
|
||||
@ -64,6 +72,13 @@ impl SessionPollerManager {
|
||||
*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
|
||||
pub fn start(&self) {
|
||||
if self.is_running.swap(true, Ordering::Relaxed) {
|
||||
@ -77,6 +92,7 @@ impl SessionPollerManager {
|
||||
let client = self.jellyfin_client.clone();
|
||||
let mode_manager = self.playback_mode_manager.clone();
|
||||
let emitter = self.event_emitter.clone();
|
||||
let connectivity_reporter = self.connectivity_reporter.clone();
|
||||
let is_running = self.is_running.clone();
|
||||
let hint = self.current_hint.clone();
|
||||
let interval_ms = self.current_interval_ms.clone();
|
||||
@ -96,18 +112,35 @@ impl SessionPollerManager {
|
||||
|
||||
debug!("[SessionPoller] Polling with interval: {}ms", new_interval);
|
||||
|
||||
// Fetch sessions
|
||||
let sessions_result = rt.block_on(async {
|
||||
// Fetch sessions. `had_client` distinguishes "server didn't
|
||||
// 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();
|
||||
match client_opt {
|
||||
Some(c) => c.get_sessions().await,
|
||||
Some(c) => (c.get_sessions().await, true),
|
||||
None => {
|
||||
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
|
||||
match sessions_result {
|
||||
Ok(sessions) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user