From e2dd387a8c02f1dea8c0fab051939cf2e67075dc Mon Sep 17 00:00:00 2001 From: NimBold Date: Thu, 18 Jun 2026 07:52:48 +0330 Subject: [PATCH] fix(integration): decode persisted settings and align ports --- src-tauri/src/extension_server.rs | 17 +-- src-tauri/src/lib.rs | 62 +++----- src-tauri/src/queue.rs | 31 ++-- src-tauri/src/scheduler.rs | 94 +++++------- src-tauri/src/settings.rs | 232 ++++++++++++++++++++++++++++++ 5 files changed, 306 insertions(+), 130 deletions(-) create mode 100644 src-tauri/src/settings.rs diff --git a/src-tauri/src/extension_server.rs b/src-tauri/src/extension_server.rs index fbf8b1b..edeb1d1 100644 --- a/src-tauri/src/extension_server.rs +++ b/src-tauri/src/extension_server.rs @@ -15,14 +15,12 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, RwLock}; use std::time::{SystemTime, UNIX_EPOCH}; use tauri::{AppHandle, Emitter, Manager}; -use tauri_plugin_store::StoreExt; use tokio::sync::watch; use tower_http::cors::{Any, CorsLayer}; use ts_rs::TS; -pub const EXTENSION_SERVER_PORT: u16 = 23522; -pub const EXTENSION_SERVER_PORT_RANGE: std::ops::RangeInclusive = - EXTENSION_SERVER_PORT..=23531; +pub const EXTENSION_SERVER_PORT: u16 = 6412; +pub const EXTENSION_SERVER_PORT_RANGE: std::ops::RangeInclusive = EXTENSION_SERVER_PORT..=6422; const MAX_URL_COUNT: usize = 200; const SIGNATURE_MAX_AGE_MS: u64 = 60_000; const MAIN_QUEUE_ID: &str = "00000000-0000-0000-0000-000000000001"; @@ -228,7 +226,7 @@ async fn enqueue_extension_download( app_handle: &AppHandle, download: &ExtensionDownload, ) -> Result<(), String> { - let Some(settings) = read_settings(app_handle) else { + let Ok(settings) = crate::settings::load_settings(app_handle) else { return Err("settings unavailable".to_string()); }; let state = app_handle.state::(); @@ -309,13 +307,6 @@ async fn enqueue_extension_download( Ok(()) } -fn read_settings(app_handle: &AppHandle) -> Option { - let store = app_handle.store("store.bin").ok()?; - let settings_value = store.get("settings")?; - let settings_text = settings_value.as_str()?; - serde_json::from_str(settings_text).ok() -} - fn merge_headers(referer: Option<&str>, headers: Option<&str>) -> Option { let mut values = Vec::new(); if let Some(referer) = referer { @@ -339,7 +330,7 @@ fn filename_from_url(raw_url: &str) -> String { .and_then(|url| { url.path_segments() .and_then(Iterator::last) - .and_then(|segment| sanitize_filename(segment)) + .and_then(sanitize_filename) }) .unwrap_or_else(|| "download".to_string()) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e984e33..67a90e0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -517,6 +517,7 @@ mod parity; pub mod error; pub mod commands; pub mod retry; +mod settings; pub use error::AppError; // Retained only for compatibility with the optional aria2 diagnostic monitor. @@ -2153,20 +2154,9 @@ pub fn run() { let max_concurrent = { - use tauri_plugin_store::StoreExt; - let mut capacity = crate::queue::DEFAULT_MAX_CONCURRENT; - if let Ok(store) = app.handle().store("store.bin") { - if let Some(settings_val) = store.get("settings") { - if let Some(settings_str) = settings_val.as_str() { - if let Ok(settings_json) = serde_json::from_str::(settings_str) { - if let Some(n) = settings_json.get("maxConcurrentDownloads").and_then(|v| v.as_u64()) { - capacity = n as usize; - } - } - } - } - } - capacity + crate::settings::load_settings(app.handle()) + .map(|settings| settings.max_concurrent_downloads) + .unwrap_or(crate::queue::DEFAULT_MAX_CONCURRENT) }; let queue_manager = Arc::new(queue::QueueManager::new(app.handle().clone(), max_concurrent)); @@ -2234,21 +2224,9 @@ pub fn run() { } crate::scheduler::spawn_scheduler(app.handle().clone()); - let mut global_speed_limit = String::new(); - { - use tauri_plugin_store::StoreExt; - if let Ok(store) = app.handle().store("store.bin") { - if let Some(settings_val) = store.get("settings") { - if let Some(settings_str) = settings_val.as_str() { - if let Ok(settings_json) = serde_json::from_str::(settings_str) { - if let Some(limit) = settings_json.get("globalSpeedLimit").and_then(|v| v.as_str()) { - global_speed_limit = limit.to_string(); - } - } - } - } - } - } + let global_speed_limit = crate::settings::load_settings(app.handle()) + .map(|settings| settings.global_speed_limit) + .unwrap_or_default(); match resolve_bundled_binary_path(app.handle(), "aria2c") { Ok(binary_path) => { @@ -2281,22 +2259,20 @@ pub fn run() { std::thread::spawn(move || { use std::io::BufRead; let reader = std::io::BufReader::new(stderr); - for line in reader.lines() { - if let Ok(trimmed) = line { - let trimmed = trimmed.trim().to_string(); - if let Ok(mut stderr_lock) = daemon_app.state::().last_stderr.lock() { - stderr_lock.push_str(&trimmed); - stderr_lock.push('\n'); - let excess = stderr_lock.len().saturating_sub(8192); - if excess > 0 { - let _ = stderr_lock.drain(..excess); - } - } - let lower = trimmed.to_lowercase(); - if lower.contains("error") || lower.contains("critical") { - log::error!("aria2c stderr: {}", trimmed); + for line in reader.lines().map_while(Result::ok) { + let trimmed = line.trim().to_string(); + if let Ok(mut stderr_lock) = daemon_app.state::().last_stderr.lock() { + stderr_lock.push_str(&trimmed); + stderr_lock.push('\n'); + let excess = stderr_lock.len().saturating_sub(8192); + if excess > 0 { + let _ = stderr_lock.drain(..excess); } } + let lower = trimmed.to_lowercase(); + if lower.contains("error") || lower.contains("critical") { + log::error!("aria2c stderr: {}", trimmed); + } } }); } diff --git a/src-tauri/src/queue.rs b/src-tauri/src/queue.rs index 7f0178a..cfa72db 100644 --- a/src-tauri/src/queue.rs +++ b/src-tauri/src/queue.rs @@ -257,17 +257,18 @@ impl QueueManager { loop { let debt = self.slots_to_retire.load(Ordering::Relaxed); let to_deduct = std::cmp::min(debt, delta); - match self.slots_to_retire.compare_exchange_weak( - debt, - debt - to_deduct, - Ordering::Relaxed, - Ordering::Relaxed, - ) { - Ok(_) => { - delta -= to_deduct; - break; - } - Err(_) => {} + if self + .slots_to_retire + .compare_exchange_weak( + debt, + debt - to_deduct, + Ordering::Relaxed, + Ordering::Relaxed, + ) + .is_ok() + { + delta -= to_deduct; + break; } } if delta > 0 { @@ -580,7 +581,7 @@ impl QueueManager { } let payload = payload.unwrap(); - let this = Arc::clone(&self); + let this = Arc::clone(self); let stale_gid = gid.to_string(); let id_for_task = id.clone(); let error_for_emit = error.clone(); @@ -890,8 +891,7 @@ impl SidecarSpawner for ProductionSpawner { let mut cancel_rx = state .download_coordinator .register_media(id.to_string()) - .await - .map_err(|e| e)?; + .await?; let outcome = crate::start_media_download_internal( self.app_handle.clone(), id, @@ -941,8 +941,7 @@ impl SidecarSpawner for ProductionSpawner { proxy: payload.proxy.clone(), }, ))) - .await - .map_err(|e| e)?; + .await?; Ok(()) } } diff --git a/src-tauri/src/scheduler.rs b/src-tauri/src/scheduler.rs index cc58407..6bf6b76 100644 --- a/src-tauri/src/scheduler.rs +++ b/src-tauri/src/scheduler.rs @@ -1,8 +1,6 @@ -use tauri::Emitter; -use chrono::{Local, Datelike}; +use chrono::{Datelike, Local}; use std::time::Duration; - - +use tauri::Emitter; pub fn spawn_scheduler(app_handle: tauri::AppHandle) { tauri::async_runtime::spawn(async move { @@ -10,68 +8,48 @@ pub fn spawn_scheduler(app_handle: tauri::AppHandle) { loop { interval.tick().await; - let settings_opt = { - use tauri_plugin_store::StoreExt; - let store = app_handle.store("store.bin"); - if let Ok(store) = store { - store.get("settings").and_then(|v| v.as_str().map(|s| s.to_string())) - } else { - None + if let Ok(settings) = crate::settings::load_settings(&app_handle) { + let scheduler = settings.scheduler; + if !scheduler.enabled { + continue; } - }; - if let Some(settings_str) = settings_opt { - if let Ok(mut settings) = serde_json::from_str::(&settings_str) { - if let Ok(scheduler) = serde_json::from_value::(settings.get("scheduler").unwrap_or(&serde_json::json!({})).clone()) { - if !scheduler.enabled { - continue; - } + let now = Local::now(); + let current_time = now.format("%H:%M").to_string(); + let current_day = now.weekday().num_days_from_sunday(); - let now = Local::now(); - let current_time = now.format("%H:%M").to_string(); - let current_day = now.weekday().num_days_from_sunday(); + let allowed_today = + scheduler.everyday || scheduler.selected_days.contains(¤t_day); + if !allowed_today { + continue; + } - let allowed_today = scheduler.everyday || scheduler.selected_days.contains(¤t_day); - if !allowed_today { - continue; - } + let date_key = now.format("%Y-%m-%d").to_string(); + let trigger_key = format!("{}-{}", date_key, current_time); - let date_key = now.format("%Y-%m-%d").to_string(); - let trigger_key = format!("{}-{}", date_key, current_time); + if scheduler.start_time == current_time + && settings.scheduler_last_start_key != trigger_key + { + let key = trigger_key.clone(); + let _ = crate::settings::update_settings_state(&app_handle, |state| { + state.insert("schedulerLastStartKey".to_string(), serde_json::json!(key)); + state.insert("schedulerRunning".to_string(), serde_json::json!(true)); + }); - let last_start_key = settings.get("schedulerLastStartKey").and_then(|v| v.as_str()).unwrap_or("").to_string(); - let last_stop_key = settings.get("schedulerLastStopKey").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let _ = app_handle.emit("schedule-trigger", "start"); + } - if scheduler.start_time == current_time && last_start_key != trigger_key { - settings["schedulerLastStartKey"] = serde_json::json!(trigger_key.clone()); - settings["schedulerRunning"] = serde_json::json!(true); - - let _ = app_handle.emit("schedule-trigger", "start"); - - if let Ok(updated) = serde_json::to_string(&settings) { - use tauri_plugin_store::StoreExt; - if let Ok(store) = app_handle.store("store.bin") { - store.set("settings", serde_json::json!(updated)); - let _ = store.save(); - } - } - } + if scheduler.stop_time_enabled + && scheduler.stop_time == current_time + && settings.scheduler_last_stop_key != trigger_key + { + let key = trigger_key.clone(); + let _ = crate::settings::update_settings_state(&app_handle, |state| { + state.insert("schedulerLastStopKey".to_string(), serde_json::json!(key)); + state.insert("schedulerRunning".to_string(), serde_json::json!(false)); + }); - if scheduler.stop_time_enabled && scheduler.stop_time == current_time && last_stop_key != trigger_key { - settings["schedulerLastStopKey"] = serde_json::json!(trigger_key.clone()); - settings["schedulerRunning"] = serde_json::json!(false); - - let _ = app_handle.emit("schedule-trigger", "stop"); - - if let Ok(updated) = serde_json::to_string(&settings) { - use tauri_plugin_store::StoreExt; - if let Ok(store) = app_handle.store("store.bin") { - store.set("settings", serde_json::json!(updated)); - let _ = store.save(); - } - } - } - } + let _ = app_handle.emit("schedule-trigger", "stop"); } } } diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs new file mode 100644 index 0000000..dcf6721 --- /dev/null +++ b/src-tauri/src/settings.rs @@ -0,0 +1,232 @@ +use crate::ipc::{ + AppFontSize, ListRowDensity, MediaCookieSource, PersistedSettings, PostQueueAction, ProxyMode, + SchedulerSettings, SettingsTab, Theme, +}; +use serde_json::{Map, Value}; +use std::collections::HashMap; +use tauri::AppHandle; +use tauri_plugin_store::StoreExt; + +const SETTINGS_STORE: &str = "store.bin"; +const SETTINGS_KEY: &str = "settings"; + +pub fn load_settings(app_handle: &AppHandle) -> Result { + let store = app_handle + .store(SETTINGS_STORE) + .map_err(|error| format!("failed to open settings store: {error}"))?; + let stored = store + .get(SETTINGS_KEY) + .ok_or_else(|| "settings are not persisted yet".to_string())?; + decode_stored_settings(&stored) +} + +pub fn decode_stored_settings(stored: &Value) -> Result { + let document = decode_document(stored)?; + let state = settings_state(&document)?; + let mut merged = serde_json::to_value(default_settings()) + .map_err(|error| format!("failed to serialize settings defaults: {error}"))?; + merge_json(&mut merged, state); + + let mut settings: PersistedSettings = serde_json::from_value(merged) + .map_err(|error| format!("invalid persisted settings: {error}"))?; + validate_settings(&mut settings); + Ok(settings) +} + +pub fn update_settings_state( + app_handle: &AppHandle, + update: impl FnOnce(&mut Map), +) -> Result<(), String> { + let store = app_handle + .store(SETTINGS_STORE) + .map_err(|error| format!("failed to open settings store: {error}"))?; + let stored = store + .get(SETTINGS_KEY) + .ok_or_else(|| "settings are not persisted yet".to_string())?; + let was_string = stored.is_string(); + let mut document = decode_document(&stored)?; + update(settings_state_mut(&mut document)?); + + let stored = if was_string { + Value::String( + serde_json::to_string(&document) + .map_err(|error| format!("failed to encode settings: {error}"))?, + ) + } else { + document + }; + store.set(SETTINGS_KEY, stored); + store + .save() + .map_err(|error| format!("failed to save settings: {error}")) +} + +fn decode_document(stored: &Value) -> Result { + match stored { + Value::String(text) => serde_json::from_str(text) + .map_err(|error| format!("failed to parse persisted settings: {error}")), + Value::Object(_) => Ok(stored.clone()), + _ => Err("persisted settings must be a JSON string or object".to_string()), + } +} + +fn settings_state(document: &Value) -> Result<&Value, String> { + if let Some(state) = document.get("state") { + if !state.is_object() { + return Err("persisted settings state must be an object".to_string()); + } + Ok(state) + } else if document.is_object() { + Ok(document) + } else { + Err("persisted settings must be an object".to_string()) + } +} + +fn settings_state_mut(document: &mut Value) -> Result<&mut Map, String> { + let has_envelope = document.get("state").is_some(); + let state = if has_envelope { + document + .get_mut("state") + .ok_or_else(|| "persisted settings state is missing".to_string())? + } else { + document + }; + state + .as_object_mut() + .ok_or_else(|| "persisted settings state must be an object".to_string()) +} + +fn merge_json(target: &mut Value, source: &Value) { + match (target, source) { + (Value::Object(target), Value::Object(source)) => { + for (key, value) in source { + if let Some(target_value) = target.get_mut(key) { + merge_json(target_value, value); + } else { + target.insert(key.clone(), value.clone()); + } + } + } + (target, source) => *target = source.clone(), + } +} + +fn validate_settings(settings: &mut PersistedSettings) { + if settings.max_concurrent_downloads == 0 { + settings.max_concurrent_downloads = default_settings().max_concurrent_downloads; + } +} + +fn default_settings() -> PersistedSettings { + let download_directories = [ + ("Musics", "~/Downloads/Musics"), + ("Movies", "~/Downloads/Movies"), + ("Compressed", "~/Downloads/Compressed"), + ("Documents", "~/Downloads/Documents"), + ("Pictures", "~/Downloads/Pictures"), + ("Applications", "~/Downloads/Applications"), + ("Other", "~/Downloads/Other"), + ] + .into_iter() + .map(|(category, path)| (category.to_string(), path.to_string())) + .collect::>(); + + PersistedSettings { + theme: Theme::System, + default_download_path: "~/Downloads".to_string(), + max_concurrent_downloads: 3, + global_speed_limit: String::new(), + is_sidebar_visible: true, + active_settings_tab: SettingsTab::Downloads, + scheduler: SchedulerSettings { + enabled: false, + start_time: "00:00".to_string(), + stop_time_enabled: false, + stop_time: "08:00".to_string(), + everyday: true, + selected_days: vec![0, 1, 2, 3, 4, 5, 6], + post_queue_action: PostQueueAction::None, + }, + scheduler_last_start_key: String::new(), + scheduler_last_stop_key: String::new(), + last_custom_speed_limit_ki_b: 1024, + per_server_connections: 16, + max_automatic_retries: 3, + show_notifications: true, + play_completion_sound: true, + app_font_size: AppFontSize::Standard, + list_row_density: ListRowDensity::Standard, + show_dock_badge: true, + show_menu_bar_icon: true, + proxy_mode: ProxyMode::None, + proxy_host: String::new(), + proxy_port: 8080, + custom_user_agent: String::new(), + ask_where_to_save_each_file: false, + prevents_sleep_while_downloading: true, + media_cookie_source: MediaCookieSource::None, + download_directories, + site_logins: Vec::new(), + extension_pairing_token: String::new(), + auto_check_updates: true, + } +} + +#[cfg(test)] +mod tests { + use super::decode_stored_settings; + use serde_json::{json, Value}; + + #[test] + fn decodes_zustand_envelope_and_preserves_non_default_startup_settings() { + let stored = json!({ + "state": { + "maxConcurrentDownloads": 7, + "globalSpeedLimit": "2M", + "scheduler": { + "enabled": true, + "startTime": "06:30", + "stopTimeEnabled": true, + "stopTime": "23:15", + "everyday": false, + "selectedDays": [1, 3, 5], + "postQueueAction": "sleep" + } + }, + "version": 0 + }); + + let settings = decode_stored_settings(&Value::String(stored.to_string())).unwrap(); + + assert_eq!(settings.max_concurrent_downloads, 7); + assert_eq!(settings.global_speed_limit, "2M"); + assert!(settings.scheduler.enabled); + assert_eq!(settings.scheduler.start_time, "06:30"); + assert_eq!(settings.scheduler.selected_days, vec![1, 3, 5]); + assert_eq!(settings.default_download_path, "~/Downloads"); + } + + #[test] + fn decodes_legacy_top_level_settings() { + let stored = json!({ + "maxConcurrentDownloads": 5, + "globalSpeedLimit": "512K" + }); + + let settings = decode_stored_settings(&Value::String(stored.to_string())).unwrap(); + + assert_eq!(settings.max_concurrent_downloads, 5); + assert_eq!(settings.global_speed_limit, "512K"); + assert!(!settings.scheduler.enabled); + } + + #[test] + fn replaces_zero_concurrency_with_the_safe_default() { + let stored = json!({"state": {"maxConcurrentDownloads": 0}, "version": 0}); + + let settings = decode_stored_settings(&Value::String(stored.to_string())).unwrap(); + + assert_eq!(settings.max_concurrent_downloads, 3); + } +}