diff --git a/src/i18n/catalogs/en.ts b/src/i18n/catalogs/en.ts
index 336ba21..04347f4 100644
--- a/src/i18n/catalogs/en.ts
+++ b/src/i18n/catalogs/en.ts
@@ -242,6 +242,12 @@ const common = {
torrentExcludeTrackers: 'Excluded Torrent trackers',
torrentExcludeTrackersHint: 'One HTTP, HTTPS, or UDP tracker per line, or * to exclude all announce URLs. Credentials are not allowed; DHT and PEX settings are unchanged.',
torrentExcludeTrackersInvalid: 'Excluded Torrent tracker list is invalid. Use HTTP, HTTPS, or UDP tracker URLs without credentials, or *.',
+ torrentTrackerConnectTimeout: 'Tracker connect timeout',
+ torrentTrackerTimeout: 'Tracker request timeout',
+ torrentTrackerInterval: 'Tracker interval',
+ torrentTrackerTimingHint: 'Connect timeout covers establishing a tracker connection; request timeout covers the response afterward. Blank values keep Aria2’s 60-second defaults, and interval 0 follows tracker response and download progress.',
+ torrentTrackerTimeoutInvalid: 'Tracker timeout must be a whole number from 1 to 604800 seconds',
+ torrentTrackerIntervalInvalid: 'Tracker interval must be a whole number from 0 to 604800 seconds',
torrentVerifyIntegrity: 'Verify Torrent integrity',
torrentVerifyIntegrityHint: 'Applied when this Torrent starts or retries. It may recheck pieces and download damaged data; active transfers cannot change it.',
torrentMaxPeers: 'Maximum Torrent peers',
@@ -527,6 +533,12 @@ const common = {
torrentExcludeTrackers: 'Excluded Torrent trackers',
torrentExcludeTrackersHint: 'Saved with this Torrent and applied on its next start or retry. * excludes all announce URLs; DHT and PEX settings are unchanged.',
torrentExcludeTrackersInvalid: 'Excluded Torrent tracker list is invalid. Use HTTP, HTTPS, or UDP tracker URLs without credentials, or *.',
+ torrentTrackerConnectTimeout: 'Tracker connect timeout',
+ torrentTrackerTimeout: 'Tracker request timeout',
+ torrentTrackerInterval: 'Tracker interval',
+ torrentTrackerTimingHint: 'Saved with this Torrent and applied on its next start or retry. Connect timeout covers establishing the connection; request timeout covers the response afterward. Blank values keep Aria2’s 60-second defaults, and interval 0 follows tracker response and download progress.',
+ torrentTrackerTimeoutInvalid: 'Tracker timeout must be a whole number from 1 to 604800 seconds',
+ torrentTrackerIntervalInvalid: 'Tracker interval must be a whole number from 0 to 604800 seconds',
torrentVerifyIntegrity: 'Verify Torrent integrity',
torrentVerifyIntegrityHint: 'Recheck piece hashes when starting or retrying; damaged pieces may be downloaded again.',
torrentMaxPeers: 'Maximum Torrent peers',
diff --git a/src/i18n/catalogs/fa.ts b/src/i18n/catalogs/fa.ts
index 63f7246..5b8edea 100644
--- a/src/i18n/catalogs/fa.ts
+++ b/src/i18n/catalogs/fa.ts
@@ -242,6 +242,12 @@ const fa = {
torrentExcludeTrackers: 'Trackerهای مستثناشده تورنت',
torrentExcludeTrackersHint: 'در هر خط یک Tracker از نوع HTTP، HTTPS یا UDP، یا * برای حذف همه آدرسهای announce وارد کنید. اطلاعات ورود مجاز نیست؛ تنظیمات DHT و PEX تغییر نمیکند.',
torrentExcludeTrackersInvalid: 'فهرست Trackerهای مستثناشده نامعتبر است. از آدرس HTTP، HTTPS یا UDP بدون اطلاعات ورود، یا * استفاده کنید.',
+ torrentTrackerConnectTimeout: 'مهلت اتصال به Tracker',
+ torrentTrackerTimeout: 'مهلت درخواست Tracker',
+ torrentTrackerInterval: 'فاصله درخواست Tracker',
+ torrentTrackerTimingHint: 'مهلت اتصال برای برقراری اتصال به Tracker و مهلت درخواست برای پاسخ پس از آن است. مقدار خالی پیشفرض ۶۰ ثانیهای آریا۲ را نگه میدارد و فاصله ۰ از پاسخ Tracker و پیشرفت دانلود پیروی میکند.',
+ torrentTrackerTimeoutInvalid: 'مهلت Tracker باید عددی صحیح بین ۱ تا ۶۰۴۸۰۰ ثانیه باشد',
+ torrentTrackerIntervalInvalid: 'فاصله Tracker باید عددی صحیح بین ۰ تا ۶۰۴۸۰۰ ثانیه باشد',
torrentVerifyIntegrity: 'بررسی صحت تورنت',
torrentVerifyIntegrityHint: 'هنگام شروع یا تلاش مجدد این تورنت اعمال میشود. ممکن است قطعهها دوباره بررسی و دادههای خراب دوباره دانلود شوند؛ در انتقال فعال قابل تغییر نیست.',
torrentMaxPeers: 'حداکثر همتاهای تورنت',
@@ -527,6 +533,12 @@ const fa = {
torrentExcludeTrackers: 'Trackerهای مستثناشده تورنت',
torrentExcludeTrackersHint: 'با این تورنت ذخیره و در شروع یا تلاش مجدد بعدی اعمال میشود. * همه آدرسهای announce را حذف میکند؛ تنظیمات DHT و PEX تغییر نمیکند.',
torrentExcludeTrackersInvalid: 'فهرست Trackerهای مستثناشده نامعتبر است. از آدرس HTTP، HTTPS یا UDP بدون اطلاعات ورود، یا * استفاده کنید.',
+ torrentTrackerConnectTimeout: 'مهلت اتصال به Tracker',
+ torrentTrackerTimeout: 'مهلت درخواست Tracker',
+ torrentTrackerInterval: 'فاصله درخواست Tracker',
+ torrentTrackerTimingHint: 'با این تورنت ذخیره و در شروع یا تلاش مجدد بعدی اعمال میشود. مهلت اتصال برای برقراری اتصال و مهلت درخواست برای پاسخ پس از آن است؛ مقدار خالی پیشفرض ۶۰ ثانیهای آریا۲ را نگه میدارد و فاصله ۰ از پاسخ Tracker و پیشرفت دانلود پیروی میکند.',
+ torrentTrackerTimeoutInvalid: 'مهلت Tracker باید عددی صحیح بین ۱ تا ۶۰۴۸۰۰ ثانیه باشد',
+ torrentTrackerIntervalInvalid: 'فاصله Tracker باید عددی صحیح بین ۰ تا ۶۰۴۸۰۰ ثانیه باشد',
torrentVerifyIntegrity: 'بررسی صحت تورنت',
torrentVerifyIntegrityHint: 'هنگام شروع یا تلاش مجدد، هش قطعهها را بررسی میکند؛ قطعههای خراب ممکن است دوباره دانلود شوند.',
torrentMaxPeers: 'حداکثر همتاهای تورنت',
diff --git a/src/i18n/catalogs/he.ts b/src/i18n/catalogs/he.ts
index ac2dc80..8b43531 100644
--- a/src/i18n/catalogs/he.ts
+++ b/src/i18n/catalogs/he.ts
@@ -242,6 +242,12 @@ const he = {
torrentExcludeTrackers: 'עוקבי טורנט להחרגה',
torrentExcludeTrackersHint: 'עוקב HTTP, HTTPS או UDP אחד בכל שורה, או * כדי להחריג את כל כתובות ההכרזה. פרטי התחברות אינם מותרים; הגדרות DHT ו-PEX לא ישתנו.',
torrentExcludeTrackersInvalid: 'רשימת עוקבי הטורנט להחרגה אינה תקינה. השתמש בכתובות HTTP, HTTPS או UDP ללא פרטי התחברות, או ב-*.',
+ torrentTrackerConnectTimeout: 'זמן קצוב לחיבור ל-Tracker',
+ torrentTrackerTimeout: 'זמן קצוב לבקשת Tracker',
+ torrentTrackerInterval: 'מרווח בין בקשות Tracker',
+ torrentTrackerTimingHint: 'זמן קצוב לחיבור חל על יצירת החיבור ל-Tracker, וזמן קצוב לבקשה חל על התגובה שלאחר מכן. ערכים ריקים שומרים על ברירת המחדל של Aria2, 60 שניות; מרווח 0 עוקב אחר תגובת ה-Tracker והתקדמות ההורדה.',
+ torrentTrackerTimeoutInvalid: 'זמן הקצוב ל-Tracker חייב להיות מספר שלם בין 1 ל-604800 שניות',
+ torrentTrackerIntervalInvalid: 'מרווח ה-Tracker חייב להיות מספר שלם בין 0 ל-604800 שניות',
torrentVerifyIntegrity: 'אימות תקינות הטורנט',
torrentVerifyIntegrityHint: 'מוחל כשהטורנט מתחיל או מנסה שוב. ייתכן שהחלקים ייבדקו מחדש ונתונים פגומים יורדו שוב; אי אפשר לשנות זאת בהעברה פעילה.',
torrentMaxPeers: 'מספר העמיתים המרבי בטורנט',
@@ -527,6 +533,12 @@ const he = {
torrentExcludeTrackers: 'עוקבי טורנט להחרגה',
torrentExcludeTrackersHint: 'נשמרים עם הטורנט ומוחלים בהפעלה או בניסיון החוזר הבא. * מחריג את כל כתובות ההכרזה; הגדרות DHT ו-PEX לא ישתנו.',
torrentExcludeTrackersInvalid: 'רשימת עוקבי הטורנט להחרגה אינה תקינה. השתמש בכתובות HTTP, HTTPS או UDP ללא פרטי התחברות, או ב-*.',
+ torrentTrackerConnectTimeout: 'זמן קצוב לחיבור ל-Tracker',
+ torrentTrackerTimeout: 'זמן קצוב לבקשת Tracker',
+ torrentTrackerInterval: 'מרווח בין בקשות Tracker',
+ torrentTrackerTimingHint: 'נשמר עם ה-Torrent ומוחל בהפעלה או בניסיון חוזר. זמן קצוב לחיבור חל על יצירת החיבור, וזמן קצוב לבקשה חל על התגובה שלאחר מכן; ערכים ריקים שומרים על ברירת המחדל של Aria2, 60 שניות, ומרווח 0 עוקב אחר תגובת ה-Tracker והתקדמות ההורדה.',
+ torrentTrackerTimeoutInvalid: 'זמן הקצוב ל-Tracker חייב להיות מספר שלם בין 1 ל-604800 שניות',
+ torrentTrackerIntervalInvalid: 'מרווח ה-Tracker חייב להיות מספר שלם בין 0 ל-604800 שניות',
torrentVerifyIntegrity: 'אימות תקינות הטורנט',
torrentVerifyIntegrityHint: 'בדיקת גיבובי החלקים בעת התחלה או ניסיון חוזר; חלקים פגומים עשויים להיות מורדים מחדש.',
torrentMaxPeers: 'מספר העמיתים המרבי בטורנט',
diff --git a/src/i18n/catalogs/ru.ts b/src/i18n/catalogs/ru.ts
index 35aeee3..9df0dc5 100644
--- a/src/i18n/catalogs/ru.ts
+++ b/src/i18n/catalogs/ru.ts
@@ -242,6 +242,12 @@ const ru = {
torrentExcludeTrackers: 'Исключаемые трекеры торрента',
torrentExcludeTrackersHint: 'По одному HTTP-, HTTPS- или UDP-трекеру в строке или * для исключения всех announce-адресов. Данные для входа не допускаются; настройки DHT и PEX не изменяются.',
torrentExcludeTrackersInvalid: 'Список исключаемых трекеров недействителен. Используйте URL HTTP, HTTPS или UDP без данных для входа либо *.',
+ torrentTrackerConnectTimeout: 'Тайм-аут подключения к трекеру',
+ torrentTrackerTimeout: 'Тайм-аут запроса к трекеру',
+ torrentTrackerInterval: 'Интервал запросов к трекеру',
+ torrentTrackerTimingHint: 'Тайм-аут подключения действует при установлении соединения с трекером, а тайм-аут запроса — после этого для ответа. Пустые значения сохраняют стандартные 60 секунд Aria2; интервал 0 следует ответу трекера и прогрессу загрузки.',
+ torrentTrackerTimeoutInvalid: 'Тайм-аут трекера должен быть целым числом от 1 до 604800 секунд',
+ torrentTrackerIntervalInvalid: 'Интервал трекера должен быть целым числом от 0 до 604800 секунд',
torrentVerifyIntegrity: 'Проверять целостность торрента',
torrentVerifyIntegrityHint: 'Применяется при запуске или повторной попытке. Может повторно проверить части и скачать повреждённые данные; во время активной передачи изменить нельзя.',
torrentMaxPeers: 'Максимум пиров торрента',
@@ -527,6 +533,12 @@ const ru = {
torrentExcludeTrackers: 'Исключаемые трекеры торрента',
torrentExcludeTrackersHint: 'Сохраняется вместе с торрентом и применяется при следующем запуске или повторной попытке. * исключает все announce-адреса; настройки DHT и PEX не изменяются.',
torrentExcludeTrackersInvalid: 'Список исключаемых трекеров недействителен. Используйте URL HTTP, HTTPS или UDP без данных для входа либо *.',
+ torrentTrackerConnectTimeout: 'Тайм-аут подключения к трекеру',
+ torrentTrackerTimeout: 'Тайм-аут запроса к трекеру',
+ torrentTrackerInterval: 'Интервал запросов к трекеру',
+ torrentTrackerTimingHint: 'Сохраняется вместе с Torrent и применяется при следующем запуске или повторной попытке. Тайм-аут подключения действует при установлении соединения, а тайм-аут запроса — для ответа после этого; пустые значения сохраняют стандартные 60 секунд Aria2, а интервал 0 следует ответу трекера и прогрессу загрузки.',
+ torrentTrackerTimeoutInvalid: 'Тайм-аут трекера должен быть целым числом от 1 до 604800 секунд',
+ torrentTrackerIntervalInvalid: 'Интервал трекера должен быть целым числом от 0 до 604800 секунд',
torrentVerifyIntegrity: 'Проверять целостность торрента',
torrentVerifyIntegrityHint: 'Проверка хешей частей при запуске или повторной попытке; повреждённые части могут быть загружены заново.',
torrentMaxPeers: 'Максимум пиров торрента',
diff --git a/src/i18n/catalogs/uk.ts b/src/i18n/catalogs/uk.ts
index ed6a37c..fce4942 100644
--- a/src/i18n/catalogs/uk.ts
+++ b/src/i18n/catalogs/uk.ts
@@ -242,6 +242,12 @@ const uk = {
torrentExcludeTrackers: 'Виключені трекери торрента',
torrentExcludeTrackersHint: 'Один HTTP-, HTTPS- або UDP-трекер у рядку або * для виключення всіх announce-адрес. Дані для входу не дозволені; налаштування DHT і PEX не змінюються.',
torrentExcludeTrackersInvalid: 'Список виключених трекерів недійсний. Використовуйте URL HTTP, HTTPS або UDP без даних для входу або *.',
+ torrentTrackerConnectTimeout: 'Час очікування підключення до трекера',
+ torrentTrackerTimeout: 'Час очікування запиту до трекера',
+ torrentTrackerInterval: 'Інтервал запитів до трекера',
+ torrentTrackerTimingHint: 'Час очікування підключення діє під час встановлення з’єднання з трекером, а час очікування запиту — для відповіді після цього. Порожні значення зберігають стандартні 60 секунд Aria2; інтервал 0 використовує відповідь трекера та прогрес завантаження.',
+ torrentTrackerTimeoutInvalid: 'Час очікування трекера має бути цілим числом від 1 до 604800 секунд',
+ torrentTrackerIntervalInvalid: 'Інтервал трекера має бути цілим числом від 0 до 604800 секунд',
torrentVerifyIntegrity: 'Перевіряти цілісність торрента',
torrentVerifyIntegrityHint: 'Застосовується під час запуску або повторної спроби. Частини можуть перевірятися повторно, а пошкоджені дані — завантажуватися знову; під час активної передачі змінити не можна.',
torrentMaxPeers: 'Максимум пірів торрента',
@@ -527,6 +533,12 @@ const uk = {
torrentExcludeTrackers: 'Виключені трекери торрента',
torrentExcludeTrackersHint: 'Зберігається разом із торрентом і застосовується під час наступного запуску або повторної спроби. * виключає всі announce-адреси; налаштування DHT і PEX не змінюються.',
torrentExcludeTrackersInvalid: 'Список виключених трекерів недійсний. Використовуйте URL HTTP, HTTPS або UDP без даних для входу або *.',
+ torrentTrackerConnectTimeout: 'Час очікування підключення до трекера',
+ torrentTrackerTimeout: 'Час очікування запиту до трекера',
+ torrentTrackerInterval: 'Інтервал запитів до трекера',
+ torrentTrackerTimingHint: 'Зберігається разом із Torrent і застосовується під час наступного запуску або повторної спроби. Час очікування підключення діє під час встановлення з’єднання, а час очікування запиту — для відповіді після цього; порожні значення зберігають стандартні 60 секунд Aria2, а інтервал 0 використовує відповідь трекера та прогрес завантаження.',
+ torrentTrackerTimeoutInvalid: 'Час очікування трекера має бути цілим числом від 1 до 604800 секунд',
+ torrentTrackerIntervalInvalid: 'Інтервал трекера має бути цілим числом від 0 до 604800 секунд',
torrentVerifyIntegrity: 'Перевіряти цілісність торрента',
torrentVerifyIntegrityHint: 'Перевіряє хеші частин під час запуску або повторної спроби; пошкоджені частини можуть завантажуватися знову.',
torrentMaxPeers: 'Максимум пірів торрента',
diff --git a/src/i18n/catalogs/zh-CN.ts b/src/i18n/catalogs/zh-CN.ts
index e091908..b5e503e 100644
--- a/src/i18n/catalogs/zh-CN.ts
+++ b/src/i18n/catalogs/zh-CN.ts
@@ -242,6 +242,12 @@ const zhCN = {
torrentExcludeTrackers: '排除的 Torrent Tracker',
torrentExcludeTrackersHint: '每行一个 HTTP、HTTPS 或 UDP Tracker,或使用 * 排除所有 announce 地址。不允许填写凭据;不会更改 DHT 和 PEX 设置。',
torrentExcludeTrackersInvalid: '排除的 Torrent Tracker 列表无效。请使用不含凭据的 HTTP、HTTPS 或 UDP 地址,或使用 *。',
+ torrentTrackerConnectTimeout: 'Tracker 连接超时',
+ torrentTrackerTimeout: 'Tracker 请求超时',
+ torrentTrackerInterval: 'Tracker 请求间隔',
+ torrentTrackerTimingHint: '连接超时用于建立 Tracker 连接,请求超时用于连接后的响应。留空会保留 Aria2 的 60 秒默认值;间隔 0 会遵循 Tracker 响应和下载进度。',
+ torrentTrackerTimeoutInvalid: 'Tracker 超时必须是 1 到 604800 秒之间的整数',
+ torrentTrackerIntervalInvalid: 'Tracker 间隔必须是 0 到 604800 秒之间的整数',
torrentVerifyIntegrity: '验证 Torrent 完整性',
torrentVerifyIntegrityHint: '在 Torrent 启动或重试时应用。可能会重新检查分片并重新下载损坏的数据;活动传输期间无法更改。',
torrentMaxPeers: 'Torrent 最大对等节点数',
@@ -527,6 +533,12 @@ const zhCN = {
torrentExcludeTrackers: '排除的 Torrent Tracker',
torrentExcludeTrackersHint: '随该 Torrent 保存,并在下次启动或重试时应用。* 会排除所有 announce 地址;不会更改 DHT 和 PEX 设置。',
torrentExcludeTrackersInvalid: '排除的 Torrent Tracker 列表无效。请使用不含凭据的 HTTP、HTTPS 或 UDP 地址,或使用 *。',
+ torrentTrackerConnectTimeout: 'Tracker 连接超时',
+ torrentTrackerTimeout: 'Tracker 请求超时',
+ torrentTrackerInterval: 'Tracker 请求间隔',
+ torrentTrackerTimingHint: '随 Torrent 保存,并在下次启动或重试时应用。连接超时用于建立连接,请求超时用于之后的响应;留空会保留 Aria2 的 60 秒默认值,间隔 0 会遵循 Tracker 响应和下载进度。',
+ torrentTrackerTimeoutInvalid: 'Tracker 超时必须是 1 到 604800 秒之间的整数',
+ torrentTrackerIntervalInvalid: 'Tracker 间隔必须是 0 到 604800 秒之间的整数',
torrentVerifyIntegrity: '验证 Torrent 完整性',
torrentVerifyIntegrityHint: '启动或重试时重新检查分片哈希;损坏的分片可能会再次下载。',
torrentMaxPeers: 'Torrent 最大对等节点数',
diff --git a/src/store/useDownloadStore.test.ts b/src/store/useDownloadStore.test.ts
index 357eea5..666c288 100644
--- a/src/store/useDownloadStore.test.ts
+++ b/src/store/useDownloadStore.test.ts
@@ -856,6 +856,9 @@ describe('useDownloadStore', () => {
torrentCheckIntegrity: 'yes' as unknown as boolean,
torrentTrackers: 123 as unknown as string,
torrentExcludeTrackers: 123 as unknown as string,
+ torrentTrackerConnectTimeout: 0,
+ torrentTrackerTimeout: 604801,
+ torrentTrackerInterval: -1,
torrentStopTimeout: 604801,
torrentPrioritizePiece: 'head=1G',
torrentRemoveUnselectedFile: 'yes' as unknown as boolean,
@@ -867,6 +870,9 @@ describe('useDownloadStore', () => {
expect(normalized.torrentCheckIntegrity).toBeUndefined();
expect(normalized.torrentTrackers).toBeUndefined();
expect(normalized.torrentExcludeTrackers).toBeUndefined();
+ expect(normalized.torrentTrackerConnectTimeout).toBeUndefined();
+ expect(normalized.torrentTrackerTimeout).toBeUndefined();
+ expect(normalized.torrentTrackerInterval).toBeUndefined();
expect(normalized.torrentStopTimeout).toBeUndefined();
expect(normalized.torrentPrioritizePiece).toBeUndefined();
expect(normalized.torrentRemoveUnselectedFile).toBeUndefined();
@@ -1524,6 +1530,9 @@ describe('useDownloadStore', () => {
torrentCheckIntegrity: true,
torrentTrackers: 'https://tracker.example/announce',
torrentExcludeTrackers: '*',
+ torrentTrackerConnectTimeout: 11,
+ torrentTrackerTimeout: 22,
+ torrentTrackerInterval: 33,
torrentStopTimeout: 300,
torrentPrioritizePiece: 'head=1M,tail=1M',
torrentEncryptionPolicy: 'force-encryption',
@@ -1542,6 +1551,9 @@ describe('useDownloadStore', () => {
torrent_check_integrity: true,
torrent_trackers: 'https://tracker.example/announce',
torrent_exclude_trackers: '*',
+ torrent_tracker_connect_timeout: 11,
+ torrent_tracker_timeout: 22,
+ torrent_tracker_interval: 33,
torrent_stop_timeout: 300,
torrent_prioritize_piece: 'head=1M,tail=1M',
torrent_encryption_policy: 'force-encryption',
diff --git a/src/store/useDownloadStore.ts b/src/store/useDownloadStore.ts
index 5f5f578..cdbdea8 100644
--- a/src/store/useDownloadStore.ts
+++ b/src/store/useDownloadStore.ts
@@ -9,7 +9,7 @@ import type { ExtensionCookieScope } from '../bindings/ExtensionCookieScope';
import type { Queue } from '../bindings/Queue';
import { useSettingsStore } from './useSettingsStore';
import { useDownloadProgressStore } from './downloadProgressStore';
-import { canonicalizeDownloadFileName, categoryForFileName, isActiveDownloadStatus, isTransferActiveStatus, isValidTorrentExcludeTrackerList, isValidTorrentTrackerList, MAX_TORRENT_STOP_TIMEOUT, normalizeSpeedLimitForBackend, normalizeTorrentEncryptionPolicy, normalizeTorrentPrioritizePiece, redactDownloadForPersistence, resolveDownloadConnections } from '../utils/downloads';
+import { canonicalizeDownloadFileName, categoryForFileName, isActiveDownloadStatus, isTransferActiveStatus, isValidTorrentExcludeTrackerList, isValidTorrentTrackerList, MAX_TORRENT_STOP_TIMEOUT, normalizeSpeedLimitForBackend, normalizeTorrentEncryptionPolicy, normalizeTorrentPrioritizePiece, normalizeTorrentTrackerInterval, normalizeTorrentTrackerTimeout, redactDownloadForPersistence, resolveDownloadConnections } from '../utils/downloads';
import {
resolveCategoryDestination
} from '../utils/downloadLocations';
@@ -353,6 +353,9 @@ async function dispatchItemInternal(id: string, proxyOverride?: string | null):
torrent_check_integrity: item.torrentCheckIntegrity,
torrent_trackers: item.torrentTrackers || undefined,
torrent_exclude_trackers: item.torrentExcludeTrackers || undefined,
+ torrent_tracker_connect_timeout: item.torrentTrackerConnectTimeout,
+ torrent_tracker_timeout: item.torrentTrackerTimeout,
+ torrent_tracker_interval: item.torrentTrackerInterval,
torrent_stop_timeout: item.torrentStopTimeout,
torrent_prioritize_piece: item.torrentPrioritizePiece || undefined,
torrent_remove_unselected_file: item.torrentRemoveUnselectedFile,
@@ -645,6 +648,12 @@ export const normalizePersistedDownloadProgress = (download: DownloadItem): Down
const normalizedExcludeTrackers = typeof rawExcludeTrackers === 'string' && rawExcludeTrackers.trim() && isValidTorrentExcludeTrackerList(rawExcludeTrackers)
? rawExcludeTrackers.trim()
: undefined;
+ const rawTrackerConnectTimeout = download.torrentTrackerConnectTimeout as unknown;
+ const normalizedTrackerConnectTimeout = normalizeTorrentTrackerTimeout(rawTrackerConnectTimeout);
+ const rawTrackerTimeout = download.torrentTrackerTimeout as unknown;
+ const normalizedTrackerTimeout = normalizeTorrentTrackerTimeout(rawTrackerTimeout);
+ const rawTrackerInterval = download.torrentTrackerInterval as unknown;
+ const normalizedTrackerInterval = normalizeTorrentTrackerInterval(rawTrackerInterval);
const rawStopTimeout = download.torrentStopTimeout as unknown;
const normalizedStopTimeout = typeof rawStopTimeout === 'number' &&
Number.isInteger(rawStopTimeout) &&
@@ -667,6 +676,9 @@ export const normalizePersistedDownloadProgress = (download: DownloadItem): Down
rawCheckIntegrity !== normalizedCheckIntegrity ||
rawTrackers !== normalizedTrackers ||
rawExcludeTrackers !== normalizedExcludeTrackers ||
+ rawTrackerConnectTimeout !== normalizedTrackerConnectTimeout ||
+ rawTrackerTimeout !== normalizedTrackerTimeout ||
+ rawTrackerInterval !== normalizedTrackerInterval ||
rawStopTimeout !== normalizedStopTimeout ||
rawPrioritizePiece !== normalizedPrioritizePiece ||
rawRemoveUnselectedFile !== normalizedRemoveUnselectedFile ||
@@ -678,6 +690,9 @@ export const normalizePersistedDownloadProgress = (download: DownloadItem): Down
torrentCheckIntegrity: normalizedCheckIntegrity,
torrentTrackers: normalizedTrackers,
torrentExcludeTrackers: normalizedExcludeTrackers,
+ torrentTrackerConnectTimeout: normalizedTrackerConnectTimeout,
+ torrentTrackerTimeout: normalizedTrackerTimeout,
+ torrentTrackerInterval: normalizedTrackerInterval,
torrentStopTimeout: normalizedStopTimeout,
torrentPrioritizePiece: normalizedPrioritizePiece,
torrentRemoveUnselectedFile: normalizedRemoveUnselectedFile,
@@ -2215,6 +2230,9 @@ export const useDownloadStore = create((set, get) => {
torrent_check_integrity: item.torrentCheckIntegrity,
torrent_trackers: item.torrentTrackers || undefined,
torrent_exclude_trackers: item.torrentExcludeTrackers || undefined,
+ torrent_tracker_connect_timeout: item.torrentTrackerConnectTimeout,
+ torrent_tracker_timeout: item.torrentTrackerTimeout,
+ torrent_tracker_interval: item.torrentTrackerInterval,
torrent_stop_timeout: item.torrentStopTimeout,
torrent_prioritize_piece: item.torrentPrioritizePiece || undefined,
torrent_remove_unselected_file: item.torrentRemoveUnselectedFile,
diff --git a/src/utils/downloads.test.ts b/src/utils/downloads.test.ts
index b5abd2d..b012276 100644
--- a/src/utils/downloads.test.ts
+++ b/src/utils/downloads.test.ts
@@ -10,6 +10,8 @@ import {
isValidTorrentTrackerList,
normalizeTorrentEncryptionPolicy,
normalizeTorrentPrioritizePiece,
+ normalizeTorrentTrackerInterval,
+ normalizeTorrentTrackerTimeout,
redactDownloadForPersistence,
resolveDownloadConnections
} from './downloads';
@@ -107,6 +109,23 @@ describe('Torrent encryption policy validation', () => {
});
});
+describe('Torrent tracker timing validation', () => {
+ it('accepts bounded timeout values and an automatic interval', () => {
+ expect(normalizeTorrentTrackerTimeout('1')).toBe(1);
+ expect(normalizeTorrentTrackerTimeout(604800)).toBe(604800);
+ expect(normalizeTorrentTrackerInterval('0')).toBe(0);
+ expect(normalizeTorrentTrackerInterval(604800)).toBe(604800);
+ });
+
+ it('rejects zero timeouts and out-of-range timing values', () => {
+ expect(normalizeTorrentTrackerTimeout('0')).toBeUndefined();
+ expect(normalizeTorrentTrackerTimeout(604801)).toBeUndefined();
+ expect(normalizeTorrentTrackerInterval(-1)).toBeUndefined();
+ expect(normalizeTorrentTrackerInterval(604801)).toBeUndefined();
+ expect(normalizeTorrentTrackerTimeout('1.5')).toBeUndefined();
+ });
+});
+
describe('download connection resolution', () => {
it('uses a clamped fallback for legacy rows without a saved value', () => {
expect(resolveDownloadConnections(undefined, 8)).toBe(8);
diff --git a/src/utils/downloads.ts b/src/utils/downloads.ts
index 304cb46..365cdb2 100644
--- a/src/utils/downloads.ts
+++ b/src/utils/downloads.ts
@@ -65,6 +65,34 @@ export const normalizeTorrentEncryptionPolicy = (
return undefined;
};
+export const MAX_TORRENT_TRACKER_TIMEOUT = 604800;
+export const MAX_TORRENT_TRACKER_INTERVAL = 604800;
+
+const parseIntegerOption = (value: unknown): number | undefined => {
+ if (typeof value === 'number') {
+ return Number.isInteger(value) ? value : undefined;
+ }
+ if (typeof value === 'string' && value.trim()) {
+ const parsed = Number(value);
+ return Number.isInteger(parsed) ? parsed : undefined;
+ }
+ return undefined;
+};
+
+export const normalizeTorrentTrackerTimeout = (value: unknown): number | undefined => {
+ const parsed = parseIntegerOption(value);
+ return parsed !== undefined && parsed >= 1 && parsed <= MAX_TORRENT_TRACKER_TIMEOUT
+ ? parsed
+ : undefined;
+};
+
+export const normalizeTorrentTrackerInterval = (value: unknown): number | undefined => {
+ const parsed = parseIntegerOption(value);
+ return parsed !== undefined && parsed >= 0 && parsed <= MAX_TORRENT_TRACKER_INTERVAL
+ ? parsed
+ : undefined;
+};
+
// Keep every filename component within the common cross-platform filesystem
// limit. Count UTF-8 bytes because POSIX filesystems enforce bytes, while this
// bound is also conservative for Windows filename components.