From 52082c1e1d27fac23608bf8feb38c2a9ce34e2c5 Mon Sep 17 00:00:00 2001 From: NimBold Date: Sat, 1 Aug 2026 21:58:09 +0330 Subject: [PATCH] feat(torrents): add peer discovery controls --- src-tauri/src/ipc.rs | 24 +++++++++++++ src-tauri/src/lib.rs | 56 ++++++++++++++++++++++++++++++ src-tauri/src/settings.rs | 26 ++++++++++++++ src/bindings/PersistedSettings.ts | 2 +- src/components/SettingsView.tsx | 55 +++++++++++++++++++++++++++++ src/i18n/catalogs/en.ts | 10 ++++++ src/i18n/catalogs/fa.ts | 10 ++++++ src/i18n/catalogs/he.ts | 10 ++++++ src/i18n/catalogs/ru.ts | 10 ++++++ src/i18n/catalogs/uk.ts | 10 ++++++ src/i18n/catalogs/zh-CN.ts | 10 ++++++ src/store/useSettingsStore.test.ts | 36 +++++++++++++++++++ src/store/useSettingsStore.ts | 24 +++++++++++++ 13 files changed, 282 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/ipc.rs b/src-tauri/src/ipc.rs index 6a0f7cf..a700537 100644 --- a/src-tauri/src/ipc.rs +++ b/src-tauri/src/ipc.rs @@ -14,6 +14,22 @@ fn default_sidebar_position() -> String { "auto".to_string() } +fn default_torrent_enable_dht() -> bool { + true +} + +fn default_torrent_enable_dht6() -> bool { + false +} + +fn default_torrent_enable_pex() -> bool { + true +} + +fn default_torrent_enable_lpd() -> bool { + false +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize, TS)] #[serde(rename_all = "lowercase")] #[ts(export, export_to = "../../src/bindings/")] @@ -408,6 +424,14 @@ pub struct PersistedSettings { pub proxy_mode: ProxyMode, pub proxy_host: String, pub proxy_port: u16, + #[serde(default = "default_torrent_enable_dht")] + pub torrent_enable_dht: bool, + #[serde(default = "default_torrent_enable_dht6")] + pub torrent_enable_dht6: bool, + #[serde(default = "default_torrent_enable_pex")] + pub torrent_enable_pex: bool, + #[serde(default = "default_torrent_enable_lpd")] + pub torrent_enable_lpd: bool, pub custom_user_agent: String, pub ask_where_to_save_each_file: bool, pub remember_last_used_download_directory: bool, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f581b51..4d5255f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -6324,6 +6324,23 @@ pub(crate) fn normalize_speed_limit_for_aria2(limit: &str) -> Option { }) } +fn apply_aria2_torrent_peer_discovery_options( + command: &mut std::process::Command, + enable_dht: bool, + enable_dht6: bool, + enable_pex: bool, + enable_lpd: bool, +) { + // These are daemon-global BitTorrent options. Keep them explicit at + // launch so Firelink does not silently inherit a different aria2 + // configuration from the host or a future bundled-engine default. + command + .arg(format!("--enable-dht={enable_dht}")) + .arg(format!("--enable-dht6={enable_dht6}")) + .arg(format!("--enable-peer-exchange={enable_pex}")) + .arg(format!("--bt-enable-lpd={enable_lpd}")); +} + #[tauri::command] async fn set_global_speed_limit( state: tauri::State<'_, AppState>, @@ -7400,6 +7417,7 @@ mod tests { cookie_scope_for_url, metadata_authentication_error, metadata_cookie_header_present, metadata_headers, metadata_response_error, normalize_speed_limit_for_aria2, + apply_aria2_torrent_peer_discovery_options, parse_firelink_deep_link, parse_ffmpeg_version, parse_media_progress_line, redact_log_line, redact_log_line_for_output, sanitize_ytdlp_config_value, has_resumable_download_assets, is_media_artifact_name, @@ -7429,6 +7447,25 @@ mod tests { assert!(validate_keychain_grant_request_id(&"x".repeat(129)).is_err()); } + #[test] + fn aria2_torrent_peer_discovery_options_are_explicit_and_launch_scoped() { + let mut command = std::process::Command::new("aria2c"); + apply_aria2_torrent_peer_discovery_options(&mut command, false, true, false, true); + let args = command + .get_args() + .map(|arg| arg.to_string_lossy().into_owned()) + .collect::>(); + assert_eq!( + args, + vec![ + "--enable-dht=false", + "--enable-dht6=true", + "--enable-peer-exchange=false", + "--bt-enable-lpd=true", + ] + ); + } + #[test] fn aria2_active_connection_count_uses_only_nonnegative_daemon_values() { assert_eq!( @@ -10033,6 +10070,17 @@ pub fn run() { .as_ref() .map(|settings| settings.global_speed_limit.clone()) .unwrap_or_default(); + let torrent_peer_discovery = persisted_settings + .as_ref() + .map(|settings| { + ( + settings.torrent_enable_dht, + settings.torrent_enable_dht6, + settings.torrent_enable_pex, + settings.torrent_enable_lpd, + ) + }) + .unwrap_or((true, false, true, false)); let aria2_secret_clone = aria2_secret.clone(); let app_handle_bg = app.handle().clone(); @@ -10067,6 +10115,14 @@ pub fn run() { .arg("--check-certificate=true") .arg(format!("--stop-with-process={}", std::process::id())); + apply_aria2_torrent_peer_discovery_options( + &mut cmd, + torrent_peer_discovery.0, + torrent_peer_discovery.1, + torrent_peer_discovery.2, + torrent_peer_discovery.3, + ); + if let Some(limit) = normalize_speed_limit_for_aria2(&global_speed_limit) { cmd.arg(format!("--max-overall-download-limit={}", limit)); } diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index ec6a52c..2c24f54 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -188,6 +188,14 @@ fn sanitize_persisted_setting_values(state: &mut Value) { sanitize_integer_setting(state, "maxConcurrentDownloads", |value| value.as_u64().is_some()); sanitize_integer_setting(state, "perServerConnections", |value| value.as_i64().is_some()); sanitize_integer_setting(state, "maxAutomaticRetries", |value| value.as_i64().is_some()); + for key in [ + "torrentEnableDht", + "torrentEnableDht6", + "torrentEnablePex", + "torrentEnableLpd", + ] { + sanitize_boolean_setting(state, key); + } sanitize_allowed_string( state, "theme", @@ -273,6 +281,12 @@ fn sanitize_integer_setting( } } +fn sanitize_boolean_setting(state: &mut serde_json::Map, key: &str) { + if state.get(key).is_some_and(|value| !value.is_boolean()) { + state.remove(key); + } +} + fn sanitize_allowed_string( state: &mut serde_json::Map, key: &str, @@ -482,6 +496,10 @@ fn default_settings() -> PersistedSettings { proxy_mode: ProxyMode::None, proxy_host: String::new(), proxy_port: 8080, + torrent_enable_dht: true, + torrent_enable_dht6: false, + torrent_enable_pex: true, + torrent_enable_lpd: false, custom_user_agent: String::new(), ask_where_to_save_each_file: false, remember_last_used_download_directory: false, @@ -770,6 +788,10 @@ mod tests { "maxConcurrentDownloads": "not-a-number", "perServerConnections": 5, "showNotifications": "yes", + "torrentEnableDht": "yes", + "torrentEnableDht6": 1, + "torrentEnablePex": null, + "torrentEnableLpd": [], "theme": "not-a-theme", "calendarPreference": "lunar", "siteLogins": [{"id": "valid", "urlPattern": "example.com", "username": "user"}, {"id": 3}] @@ -783,6 +805,10 @@ mod tests { assert_eq!(settings.max_concurrent_downloads, 3); assert_eq!(settings.per_server_connections, 5); assert!(settings.show_notifications); + assert!(settings.torrent_enable_dht); + assert!(!settings.torrent_enable_dht6); + assert!(settings.torrent_enable_pex); + assert!(!settings.torrent_enable_lpd); assert!(matches!(settings.theme, crate::ipc::Theme::System)); assert!(matches!( settings.calendar_preference, diff --git a/src/bindings/PersistedSettings.ts b/src/bindings/PersistedSettings.ts index 7f0ecd3..28accb3 100644 --- a/src/bindings/PersistedSettings.ts +++ b/src/bindings/PersistedSettings.ts @@ -11,4 +11,4 @@ import type { SiteLogin } from "./SiteLogin"; import type { Theme } from "./Theme"; import type { WindowControlStyle } from "./WindowControlStyle"; -export type PersistedSettings = { theme: Theme, fontFamily: FontFamily, windowControlStyle: WindowControlStyle, calendarPreference: CalendarPreference, language: string, baseDownloadFolder: string, categorySubfoldersEnabled: boolean, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array, maxConcurrentDownloads: number, globalSpeedLimit: string, speedLimitPresetValues: Array, logsEnabled: boolean, isSidebarVisible: boolean, sidebarPosition: string, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, lastCustomSpeedLimitUnit: string, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, autoAddClipboardLinks: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, customUserAgent: string, askWhereToSaveEachFile: boolean, rememberLastUsedDownloadDirectory: boolean, preventsSleepWhileDownloading: boolean, preventsDisplaySleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array, autoCheckUpdates: boolean, keychainAccessGranted: boolean, }; +export type PersistedSettings = { theme: Theme, fontFamily: FontFamily, windowControlStyle: WindowControlStyle, calendarPreference: CalendarPreference, language: string, baseDownloadFolder: string, categorySubfoldersEnabled: boolean, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array, maxConcurrentDownloads: number, globalSpeedLimit: string, speedLimitPresetValues: Array, logsEnabled: boolean, isSidebarVisible: boolean, sidebarPosition: string, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, lastCustomSpeedLimitUnit: string, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, autoAddClipboardLinks: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, torrentEnableDht: boolean, torrentEnableDht6: boolean, torrentEnablePex: boolean, torrentEnableLpd: boolean, customUserAgent: string, askWhereToSaveEachFile: boolean, rememberLastUsedDownloadDirectory: boolean, preventsSleepWhileDownloading: boolean, preventsDisplaySleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array, autoCheckUpdates: boolean, keychainAccessGranted: boolean, }; diff --git a/src/components/SettingsView.tsx b/src/components/SettingsView.tsx index 7187b0b..855804b 100644 --- a/src/components/SettingsView.tsx +++ b/src/components/SettingsView.tsx @@ -1157,6 +1157,61 @@ runEngineChecks(false); )} +

{t($ => $.settings.network.torrentPeerDiscovery)}

+
+ + + + +

+ {t($ => $.settings.network.torrentPeerDiscoveryRestartNote)} +

+
+

{t($ => $.settings.network.identity)}

diff --git a/src/i18n/catalogs/en.ts b/src/i18n/catalogs/en.ts index 9c1addb..4d99fd3 100644 --- a/src/i18n/catalogs/en.ts +++ b/src/i18n/catalogs/en.ts @@ -700,6 +700,16 @@ const common = { detectedSystemProxy: 'A system proxy was detected. Normal file downloads require an HTTP or HTTPS endpoint; media downloads can use SOCKS.', noSystemProxy: 'No usable system proxy was detected. Downloads will use no proxy.', systemProxyReadFailed: 'System proxy configuration could not be read. Choose No Proxy or try again when it is available.', + torrentPeerDiscovery: 'BitTorrent peer discovery', + torrentDht: 'IPv4 DHT and UDP trackers', + torrentDhtDescription: 'Find peers without relying only on trackers. Disabling this also disables UDP tracker support.', + torrentDht6: 'IPv6 DHT', + torrentDht6Description: 'Use IPv6 for distributed peer discovery when the network provides a usable IPv6 path.', + torrentPex: 'Peer Exchange (PEX)', + torrentPexDescription: 'Allow connected peers to share additional peer addresses.', + torrentLpd: 'Local Peer Discovery (LPD)', + torrentLpdDescription: 'Discover compatible peers on the local network. This increases local network visibility.', + torrentPeerDiscoveryRestartNote: 'These options are global to Aria2 and take effect after Firelink restarts. Aria2 still disables peer discovery for private torrents.', identity: 'Identity', customUserAgent: 'Custom User-Agent', userAgentDescription: 'Applied to metadata fetches and download engines.', diff --git a/src/i18n/catalogs/fa.ts b/src/i18n/catalogs/fa.ts index 818750d..657c767 100644 --- a/src/i18n/catalogs/fa.ts +++ b/src/i18n/catalogs/fa.ts @@ -700,6 +700,16 @@ const fa = { detectedSystemProxy: 'یک پروکسی سیستم شناسایی شد. دانلودهای فایل عادی به یک نقطه پایانی HTTP یا HTTPS نیاز دارند؛ دانلودهای رسانه می‌توانند از SOCKS استفاده کنند.', noSystemProxy: 'هیچ پروکسی سیستم قابل استفاده‌ای شناسایی نشد. دانلودها از هیچ پروکسی‌ای استفاده نخواهند کرد.', systemProxyReadFailed: 'پیکربندی پروکسی سیستم قابل خواندن نیست. بدون پروکسی را انتخاب کنید یا هنگامی که در دسترس است دوباره امتحان کنید.', + torrentPeerDiscovery: 'کشف همتا در بیت‌تورنت', + torrentDht: 'DHT نسخه IPv4 و ترکرهای UDP', + torrentDhtDescription: 'همتاها را بدون تکیه صرف بر ترکرها پیدا می‌کند. خاموش کردن آن پشتیبانی از ترکرهای UDP را هم خاموش می‌کند.', + torrentDht6: 'DHT نسخه IPv6', + torrentDht6Description: 'وقتی مسیر IPv6 قابل استفاده باشد، از آن برای کشف توزیع‌شده همتاها استفاده می‌کند.', + torrentPex: 'تبادل همتا (PEX)', + torrentPexDescription: 'به همتاهای متصل اجازه می‌دهد آدرس همتاهای بیشتری را به اشتراک بگذارند.', + torrentLpd: 'کشف همتای محلی (LPD)', + torrentLpdDescription: 'همتاهای سازگار در شبکه محلی را پیدا می‌کند و دیده‌شدن ترافیک در شبکه محلی را افزایش می‌دهد.', + torrentPeerDiscoveryRestartNote: 'این گزینه‌ها سراسری و مربوط به Aria2 هستند و پس از راه‌اندازی مجدد Firelink اعمال می‌شوند. Aria2 همچنان کشف همتا را برای تورنت‌های خصوصی خاموش می‌کند.', identity: 'هویت', customUserAgent: 'User-Agent سفارشی', userAgentDescription: 'در دریافت‌های متادیتا و موتورهای دانلود اعمال می‌شود.', diff --git a/src/i18n/catalogs/he.ts b/src/i18n/catalogs/he.ts index 32f8edf..a79e5d6 100644 --- a/src/i18n/catalogs/he.ts +++ b/src/i18n/catalogs/he.ts @@ -700,6 +700,16 @@ const he = { detectedSystemProxy: 'זוהה פרוקסי מערכת. הורדות קבצים רגילות דורשות נקודת קצה של HTTP או HTTPS; הורדות מדיה יכולות להשתמש ב-SOCKS.', noSystemProxy: 'לא זוהה פרוקסי מערכת שימושי. ההורדות לא ישתמשו בפרוקסי.', systemProxyReadFailed: 'לא ניתן לקרוא את תצורת פרוקסי המערכת. בחר "ללא פרוקסי" או נסה שוב כשהיא תהיה זמינה.', + torrentPeerDiscovery: 'גילוי עמיתים ב-BitTorrent', + torrentDht: 'DHT של IPv4 ומעקבי UDP', + torrentDhtDescription: 'מאתר עמיתים בלי להסתמך רק על מעקבים. השבתה מכבה גם תמיכה במעקבי UDP.', + torrentDht6: 'DHT של IPv6', + torrentDht6Description: 'משתמש ב-IPv6 לגילוי מבוזר של עמיתים כשיש נתיב IPv6 זמין.', + torrentPex: 'החלפת עמיתים (PEX)', + torrentPexDescription: 'מאפשר לעמיתים מחוברים לשתף כתובות של עמיתים נוספים.', + torrentLpd: 'גילוי עמיתים מקומיים (LPD)', + torrentLpdDescription: 'מאתר עמיתים תואמים ברשת המקומית ומגדיל את החשיפה המקומית של התעבורה.', + torrentPeerDiscoveryRestartNote: 'האפשרויות האלה הן כלליות ל-Aria2 ונכנסות לתוקף לאחר הפעלה מחדש של Firelink. Aria2 עדיין משבית גילוי עמיתים בטורנטים פרטיים.', identity: 'זהות', customUserAgent: 'User-Agent מותאם אישית', userAgentDescription: 'מוחל על משיכות מטא נתונים ומנועי הורדה.', diff --git a/src/i18n/catalogs/ru.ts b/src/i18n/catalogs/ru.ts index 211857b..ebc7c4c 100644 --- a/src/i18n/catalogs/ru.ts +++ b/src/i18n/catalogs/ru.ts @@ -700,6 +700,16 @@ const ru = { detectedSystemProxy: 'Обнаружен системный прокси. Для обычных загрузок файлов требуется HTTP- или HTTPS-прокси; для загрузки медиа можно использовать SOCKS.', noSystemProxy: 'Подходящий системный прокси не обнаружен. Загрузки будут выполняться без прокси.', systemProxyReadFailed: 'Не удалось прочитать конфигурацию системного прокси. Выберите «Без прокси» или повторите попытку, когда она станет доступна.', + torrentPeerDiscovery: 'Обнаружение пиров BitTorrent', + torrentDht: 'IPv4 DHT и UDP-трекеры', + torrentDhtDescription: 'Ищет пиры не только через трекеры. Отключение также отключает поддержку UDP-трекеров.', + torrentDht6: 'DHT по IPv6', + torrentDht6Description: 'Использует IPv6 для распределённого поиска пиров, если доступен рабочий IPv6-маршрут.', + torrentPex: 'Обмен пирами (PEX)', + torrentPexDescription: 'Позволяет подключённым пирам передавать адреса дополнительных пиров.', + torrentLpd: 'Локальное обнаружение пиров (LPD)', + torrentLpdDescription: 'Ищет подходящие пиры в локальной сети и увеличивает видимость трафика в ней.', + torrentPeerDiscoveryRestartNote: 'Эти параметры являются глобальными для Aria2 и применяются после перезапуска Firelink. Aria2 по-прежнему отключает обнаружение пиров для приватных торрентов.', identity: 'Идентификация', customUserAgent: 'Собственный User-Agent', userAgentDescription: 'Применяется при получении метаданных и работе движков загрузки.', diff --git a/src/i18n/catalogs/uk.ts b/src/i18n/catalogs/uk.ts index e93f466..58c3a95 100644 --- a/src/i18n/catalogs/uk.ts +++ b/src/i18n/catalogs/uk.ts @@ -700,6 +700,16 @@ const uk = { detectedSystemProxy: 'Виявлено системний проксі. Звичайні завантаження файлів вимагають HTTP або HTTPS проксі; медіазавантаження можуть використовувати SOCKS.', noSystemProxy: 'Не виявлено придатного системного проксі. Завантаження використовуватимуть пряме підключення (без проксі).', systemProxyReadFailed: 'Не вдалося прочитати конфігурацію системного проксі. Виберіть "Без проксі" або спробуйте ще раз, коли він стане доступним.', + torrentPeerDiscovery: 'Пошук пірів BitTorrent', + torrentDht: 'IPv4 DHT і UDP-трекери', + torrentDhtDescription: 'Шукає пірів не лише через трекери. Вимкнення також вимикає підтримку UDP-трекерів.', + torrentDht6: 'DHT через IPv6', + torrentDht6Description: 'Використовує IPv6 для розподіленого пошуку пірів, якщо доступний робочий маршрут IPv6.', + torrentPex: 'Обмін пірами (PEX)', + torrentPexDescription: 'Дозволяє підключеним пірам передавати адреси додаткових пірів.', + torrentLpd: 'Локальний пошук пірів (LPD)', + torrentLpdDescription: 'Шукає сумісних пірів у локальній мережі та збільшує видимість трафіку в ній.', + torrentPeerDiscoveryRestartNote: 'Ці параметри є глобальними для Aria2 і застосовуються після перезапуску Firelink. Aria2 і надалі вимикає пошук пірів для приватних торрентів.', identity: 'Ідентифікація', customUserAgent: 'Власний User-Agent', userAgentDescription: 'Застосовується до запитів метаданих та рушіїв завантаження.', diff --git a/src/i18n/catalogs/zh-CN.ts b/src/i18n/catalogs/zh-CN.ts index a855336..35c9076 100644 --- a/src/i18n/catalogs/zh-CN.ts +++ b/src/i18n/catalogs/zh-CN.ts @@ -700,6 +700,16 @@ const zhCN = { detectedSystemProxy: '检测到系统代理。普通文件下载需要 HTTP 或 HTTPS 端点;媒体下载可以使用 SOCKS。', noSystemProxy: '未检测到可用的系统代理。下载将不使用代理。', systemProxyReadFailed: '无法读取系统代理配置。请选择“无代理”,或者在其可用时重试。', + torrentPeerDiscovery: 'BitTorrent 对等节点发现', + torrentDht: 'IPv4 DHT 与 UDP Tracker', + torrentDhtDescription: '不只依赖 Tracker 查找节点。关闭后也会禁用 UDP Tracker 支持。', + torrentDht6: 'IPv6 分布式哈希表', + torrentDht6Description: '当网络提供可用的 IPv6 路径时,使用 IPv6 进行分布式节点发现。', + torrentPex: '节点交换(PEX)', + torrentPexDescription: '允许已连接的节点共享其他节点的地址。', + torrentLpd: '本地节点发现(LPD)', + torrentLpdDescription: '在本地网络中发现兼容节点,这会增加本地网络中的流量可见性。', + torrentPeerDiscoveryRestartNote: '这些选项是 Aria2 的全局设置,需要重启 Firelink 后生效。Aria2 仍会对私有 Torrent 禁用节点发现。', identity: '身份', customUserAgent: '自定义 User-Agent', userAgentDescription: '应用于元数据获取和下载引擎。', diff --git a/src/store/useSettingsStore.test.ts b/src/store/useSettingsStore.test.ts index 0007581..ee78dcb 100644 --- a/src/store/useSettingsStore.test.ts +++ b/src/store/useSettingsStore.test.ts @@ -21,6 +21,42 @@ describe('last used download directory preference', () => { }); }); +describe('Torrent peer discovery preferences', () => { + beforeEach(() => { + vi.clearAllMocks(); + useSettingsStore.setState({ + torrentEnableDht: true, + torrentEnableDht6: false, + torrentEnablePex: true, + torrentEnableLpd: false + }); + }); + + it('matches Aria2 defaults and persists explicit changes', async () => { + expect(useSettingsStore.getState().torrentEnableDht).toBe(true); + expect(useSettingsStore.getState().torrentEnableDht6).toBe(false); + expect(useSettingsStore.getState().torrentEnablePex).toBe(true); + expect(useSettingsStore.getState().torrentEnableLpd).toBe(false); + + useSettingsStore.getState().setTorrentEnableDht(false); + useSettingsStore.getState().setTorrentEnableDht6(true); + useSettingsStore.getState().setTorrentEnablePex(false); + useSettingsStore.getState().setTorrentEnableLpd(true); + await vi.waitFor(() => { + const save = vi.mocked(ipc.invokeCommand).mock.calls + .filter(([command]) => command === 'db_save_settings') + .slice(-1)[0]; + expect(save).toBeDefined(); + expect(JSON.parse((save?.[1] as { data: string }).data).state).toMatchObject({ + torrentEnableDht: false, + torrentEnableDht6: true, + torrentEnablePex: false, + torrentEnableLpd: true + }); + }); + }); +}); + describe('calendar preference', () => { it('keeps Gregorian as the default and persists explicit calendar choices', async () => { vi.clearAllMocks(); diff --git a/src/store/useSettingsStore.ts b/src/store/useSettingsStore.ts index 66da803..3774b5b 100644 --- a/src/store/useSettingsStore.ts +++ b/src/store/useSettingsStore.ts @@ -234,6 +234,10 @@ export interface SettingsState { proxyMode: ProxyMode; proxyHost: string; proxyPort: number; + torrentEnableDht: boolean; + torrentEnableDht6: boolean; + torrentEnablePex: boolean; + torrentEnableLpd: boolean; customUserAgent: string; askWhereToSaveEachFile: boolean; preventsSleepWhileDownloading: boolean; @@ -284,6 +288,10 @@ export interface SettingsState { setProxyMode: (mode: ProxyMode) => void; setProxyHost: (host: string) => void; setProxyPort: (port: number) => void; + setTorrentEnableDht: (enabled: boolean) => void; + setTorrentEnableDht6: (enabled: boolean) => void; + setTorrentEnablePex: (enabled: boolean) => void; + setTorrentEnableLpd: (enabled: boolean) => void; setCustomUserAgent: (userAgent: string) => void; setAskWhereToSaveEachFile: (ask: boolean) => void; setPreventsSleepWhileDownloading: (prevent: boolean) => void; @@ -360,6 +368,10 @@ export const useSettingsStore = create()( proxyMode: 'none', proxyHost: '', proxyPort: 8080, + torrentEnableDht: true, + torrentEnableDht6: false, + torrentEnablePex: true, + torrentEnableLpd: false, customUserAgent: '', askWhereToSaveEachFile: false, preventsSleepWhileDownloading: true, @@ -454,6 +466,10 @@ export const useSettingsStore = create()( ? Math.min(65535, Math.max(1, Math.trunc(proxyPort))) : 8080 }), + setTorrentEnableDht: (torrentEnableDht) => set({ torrentEnableDht }), + setTorrentEnableDht6: (torrentEnableDht6) => set({ torrentEnableDht6 }), + setTorrentEnablePex: (torrentEnablePex) => set({ torrentEnablePex }), + setTorrentEnableLpd: (torrentEnableLpd) => set({ torrentEnableLpd }), setCustomUserAgent: (customUserAgent) => set({ customUserAgent }), setAskWhereToSaveEachFile: (askWhereToSaveEachFile) => set({ askWhereToSaveEachFile }), setPreventsSleepWhileDownloading: (preventsSleepWhileDownloading) => { @@ -635,6 +651,10 @@ export const useSettingsStore = create()( proxyMode: state.proxyMode, proxyHost: state.proxyHost, proxyPort: state.proxyPort, + torrentEnableDht: state.torrentEnableDht, + torrentEnableDht6: state.torrentEnableDht6, + torrentEnablePex: state.torrentEnablePex, + torrentEnableLpd: state.torrentEnableLpd, customUserAgent: state.customUserAgent, askWhereToSaveEachFile: state.askWhereToSaveEachFile, preventsSleepWhileDownloading: state.preventsSleepWhileDownloading, @@ -680,6 +700,10 @@ export const useSettingsStore = create()( listRowDensity: isAllowedSetting(LIST_ROW_DENSITY_VALUES, persisted.listRowDensity) ? persisted.listRowDensity : currentState.listRowDensity, + torrentEnableDht: persistedBoolean(persisted.torrentEnableDht, currentState.torrentEnableDht), + torrentEnableDht6: persistedBoolean(persisted.torrentEnableDht6, currentState.torrentEnableDht6), + torrentEnablePex: persistedBoolean(persisted.torrentEnablePex, currentState.torrentEnablePex), + torrentEnableLpd: persistedBoolean(persisted.torrentEnableLpd, currentState.torrentEnableLpd), sidebarPosition: isAllowedSetting(SIDEBAR_POSITION_VALUES, persisted.sidebarPosition) ? persisted.sidebarPosition : currentState.sidebarPosition,