diff --git a/src/i18n/catalogs/en.ts b/src/i18n/catalogs/en.ts
index ee4ebc0..052f5b4 100644
--- a/src/i18n/catalogs/en.ts
+++ b/src/i18n/catalogs/en.ts
@@ -245,6 +245,10 @@ const common = {
torrentPeerSpeedLimit: 'Peer speed threshold',
torrentMaxPeersInvalid: 'Torrent maximum peers must be an integer from 0 to 1000',
torrentPeerSpeedLimitInvalid: 'Torrent peer speed threshold must be greater than zero',
+ seconds: 'seconds',
+ torrentStopTimeout: 'Stop stalled Torrent after',
+ torrentStopTimeoutHint: 'Aria2 stops this Torrent after this many consecutive seconds at 0 B/s. 0 disables the policy; changes apply when the Torrent starts or retries.',
+ torrentStopTimeoutInvalid: 'Torrent stall timeout must be a whole number from 0 to 604800 seconds',
liveTorrentPeerOptionsFailed: 'Could not update live Torrent peer controls: {{detail}}',
category: 'Category',
lastTry: 'Last try',
@@ -480,6 +484,7 @@ const common = {
seedAfterDownload: 'Seed after download completes',
seedTime: 'Seed time',
minutes: 'minutes',
+ seconds: 'seconds',
seedRatio: 'Seed ratio',
seedRatioHint: '0 means time-only seeding; otherwise seeding stops at the first limit reached.',
limitTorrentUpload: 'Limit torrent upload',
@@ -497,6 +502,9 @@ const common = {
torrentPeerOptionsHint: 'Leave blank for Aria2 defaults (55 peers and 50K). 0 peers means unlimited.',
torrentMaxPeersInvalid: 'Torrent maximum peers must be an integer from 0 to 1000',
torrentPeerSpeedLimitInvalid: 'Torrent peer speed threshold must be greater than zero',
+ torrentStopTimeout: 'Stop stalled Torrent after',
+ torrentStopTimeoutHint: 'Aria2 stops this Torrent after this many consecutive seconds at 0 B/s. 0 disables the policy.',
+ torrentStopTimeoutInvalid: 'Torrent stall timeout must be a whole number from 0 to 604800 seconds',
required: 'Required',
free: 'Free',
preview: 'Preview',
diff --git a/src/i18n/catalogs/fa.ts b/src/i18n/catalogs/fa.ts
index 267862b..ac860fe 100644
--- a/src/i18n/catalogs/fa.ts
+++ b/src/i18n/catalogs/fa.ts
@@ -245,6 +245,10 @@ const fa = {
torrentPeerSpeedLimit: 'آستانه سرعت همتا',
torrentMaxPeersInvalid: 'حداکثر همتاهای تورنت باید عددی صحیح بین ۰ و ۱۰۰۰ باشد',
torrentPeerSpeedLimitInvalid: 'آستانه سرعت همتای تورنت باید بیشتر از صفر باشد',
+ seconds: 'ثانیه',
+ torrentStopTimeout: 'توقف تورنتِ بدون سرعت پس از',
+ torrentStopTimeoutHint: 'آریا۲ پس از این تعداد ثانیه پیاپی با سرعت صفر، تورنت را متوقف میکند. ۰ این سیاست را غیرفعال میکند؛ تغییرات هنگام شروع یا تلاش مجدد اعمال میشوند.',
+ torrentStopTimeoutInvalid: 'مهلت توقف تورنت باید عددی صحیح بین ۰ و ۶۰۴۸۰۰ ثانیه باشد',
liveTorrentPeerOptionsFailed: 'کنترل زنده همتاهای تورنت بهروزرسانی نشد: {{detail}}',
category: 'دسته',
lastTry: 'آخرین تلاش',
@@ -480,6 +484,7 @@ const fa = {
seedAfterDownload: 'پس از پایان دانلود سید شود',
seedTime: 'مدت سید',
minutes: 'دقیقه',
+ seconds: 'ثانیه',
seedRatio: 'نسبت سید',
seedRatioHint: '۰ یعنی فقط مدت زمان تعیینشده ملاک است؛ در غیر این صورت با رسیدن به اولین حد متوقف میشود.',
limitTorrentUpload: 'محدود کردن آپلود تورنت',
@@ -497,6 +502,9 @@ const fa = {
torrentPeerOptionsHint: 'برای استفاده از پیشفرضهای آریا۲ خالی بگذارید (۵۵ همتا و 50K). صفر یعنی نامحدود.',
torrentMaxPeersInvalid: 'حداکثر همتاهای تورنت باید عددی صحیح بین ۰ و ۱۰۰۰ باشد',
torrentPeerSpeedLimitInvalid: 'آستانه سرعت همتای تورنت باید بیشتر از صفر باشد',
+ torrentStopTimeout: 'توقف تورنتِ بدون سرعت پس از',
+ torrentStopTimeoutHint: 'آریا۲ پس از این تعداد ثانیه پیاپی با سرعت صفر، تورنت را متوقف میکند. ۰ این سیاست را غیرفعال میکند.',
+ torrentStopTimeoutInvalid: 'مهلت توقف تورنت باید عددی صحیح بین ۰ و ۶۰۴۸۰۰ ثانیه باشد',
required: 'الزامی',
free: 'فضای آزاد',
preview: 'پیشنمایش',
diff --git a/src/i18n/catalogs/he.ts b/src/i18n/catalogs/he.ts
index 90c9921..faadb26 100644
--- a/src/i18n/catalogs/he.ts
+++ b/src/i18n/catalogs/he.ts
@@ -245,6 +245,10 @@ const he = {
torrentPeerSpeedLimit: 'סף מהירות עמיתים',
torrentMaxPeersInvalid: 'מספר העמיתים המרבי חייב להיות מספר שלם בין 0 ל-1000',
torrentPeerSpeedLimitInvalid: 'סף מהירות העמיתים חייב להיות גדול מאפס',
+ seconds: 'שניות',
+ torrentStopTimeout: 'עצירת טורנט תקוע לאחר',
+ torrentStopTimeoutHint: 'Aria2 יעצור את הטורנט לאחר מספר זה של שניות רצופות במהירות 0 B/s. 0 משבית את המדיניות; השינוי חל כשהטורנט מתחיל או מנסה שוב.',
+ torrentStopTimeoutInvalid: 'זמן העצירה של טורנט תקוע חייב להיות מספר שלם בין 0 ל-604800 שניות',
liveTorrentPeerOptionsFailed: 'לא ניתן לעדכן את בקרות עמיתי הטורנט בזמן אמת: {{detail}}',
category: 'קטגוריה',
lastTry: 'ניסיון אחרון',
@@ -480,6 +484,7 @@ const he = {
seedAfterDownload: 'לשתף לאחר סיום ההורדה',
seedTime: 'זמן שיתוף',
minutes: 'דקות',
+ seconds: 'שניות',
seedRatio: 'יחס שיתוף',
seedRatioHint: '0 פירושו שיתוף לפי זמן בלבד; אחרת השיתוף ייפסק בהגעה למגבלה הראשונה.',
limitTorrentUpload: 'הגבלת העלאת טורנט',
@@ -497,6 +502,9 @@ const he = {
torrentPeerOptionsHint: 'השאר ריק כדי להשתמש בברירות המחדל של Aria2 (55 עמיתים ו-50K). אפס עמיתים פירושו ללא הגבלה.',
torrentMaxPeersInvalid: 'מספר העמיתים המרבי חייב להיות מספר שלם בין 0 ל-1000',
torrentPeerSpeedLimitInvalid: 'סף מהירות העמיתים חייב להיות גדול מאפס',
+ torrentStopTimeout: 'עצירת טורנט תקוע לאחר',
+ torrentStopTimeoutHint: 'Aria2 יעצור את הטורנט לאחר מספר זה של שניות רצופות במהירות 0 B/s. 0 משבית את המדיניות.',
+ torrentStopTimeoutInvalid: 'זמן העצירה של טורנט תקוע חייב להיות מספר שלם בין 0 ל-604800 שניות',
required: 'נדרש',
free: 'פנוי',
preview: 'תצוגה מקדימה',
diff --git a/src/i18n/catalogs/ru.ts b/src/i18n/catalogs/ru.ts
index 1bd338c..40b5a99 100644
--- a/src/i18n/catalogs/ru.ts
+++ b/src/i18n/catalogs/ru.ts
@@ -245,6 +245,10 @@ const ru = {
torrentPeerSpeedLimit: 'Порог скорости пиров',
torrentMaxPeersInvalid: 'Максимум пиров должен быть целым числом от 0 до 1000',
torrentPeerSpeedLimitInvalid: 'Порог скорости пиров должен быть больше нуля',
+ seconds: 'секунд',
+ torrentStopTimeout: 'Останавливать неактивный торрент через',
+ torrentStopTimeoutHint: 'Aria2 остановит этот торрент после указанного числа секунд подряд при скорости 0 Б/с. 0 отключает правило; изменения применяются при запуске или повторной попытке.',
+ torrentStopTimeoutInvalid: 'Тайм-аут неактивного торрента должен быть целым числом от 0 до 604800 секунд',
liveTorrentPeerOptionsFailed: 'Не удалось обновить текущие настройки пиров торрента: {{detail}}',
category: 'Категория',
lastTry: 'Последняя попытка',
@@ -480,6 +484,7 @@ const ru = {
seedAfterDownload: 'Раздавать после завершения загрузки',
seedTime: 'Время раздачи',
minutes: 'минут',
+ seconds: 'секунд',
seedRatio: 'Коэффициент раздачи',
seedRatioHint: '0 означает раздачу только по времени; иначе раздача остановится при достижении первого ограничения.',
limitTorrentUpload: 'Ограничить отдачу торрента',
@@ -497,6 +502,9 @@ const ru = {
torrentPeerOptionsHint: 'Оставьте пустым для параметров Aria2 по умолчанию (55 пиров и 50K). 0 пиров означает без ограничений.',
torrentMaxPeersInvalid: 'Максимум пиров должен быть целым числом от 0 до 1000',
torrentPeerSpeedLimitInvalid: 'Порог скорости пиров должен быть больше нуля',
+ torrentStopTimeout: 'Останавливать неактивный торрент через',
+ torrentStopTimeoutHint: 'Aria2 остановит этот торрент после указанного числа секунд подряд при скорости 0 Б/с. 0 отключает правило.',
+ torrentStopTimeoutInvalid: 'Тайм-аут неактивного торрента должен быть целым числом от 0 до 604800 секунд',
required: 'Требуется',
free: 'Свободно',
preview: 'Предпросмотр',
diff --git a/src/i18n/catalogs/uk.ts b/src/i18n/catalogs/uk.ts
index 5a9ce70..9f4a324 100644
--- a/src/i18n/catalogs/uk.ts
+++ b/src/i18n/catalogs/uk.ts
@@ -245,6 +245,10 @@ const uk = {
torrentPeerSpeedLimit: 'Поріг швидкості пірів',
torrentMaxPeersInvalid: 'Максимум пірів має бути цілим числом від 0 до 1000',
torrentPeerSpeedLimitInvalid: 'Поріг швидкості пірів має бути більшим за нуль',
+ seconds: 'секунд',
+ torrentStopTimeout: 'Зупиняти торрент без швидкості через',
+ torrentStopTimeoutHint: 'Aria2 зупинить цей торрент після вказаної кількості секунд поспіль зі швидкістю 0 Б/с. 0 вимикає правило; зміни застосовуються під час запуску або повторної спроби.',
+ torrentStopTimeoutInvalid: 'Тайм-аут зупинки торрента має бути цілим числом від 0 до 604800 секунд',
liveTorrentPeerOptionsFailed: 'Не вдалося оновити поточні налаштування пірів торрента: {{detail}}',
category: 'Категорія',
lastTry: 'Остання спроба',
@@ -480,6 +484,7 @@ const uk = {
seedAfterDownload: 'Роздавати після завершення завантаження',
seedTime: 'Час роздачі',
minutes: 'хвилин',
+ seconds: 'секунд',
seedRatio: 'Коефіцієнт роздачі',
seedRatioHint: '0 означає роздачу лише за часом; інакше роздача зупиниться після досягнення першого обмеження.',
limitTorrentUpload: 'Обмежити віддачу торрента',
@@ -497,6 +502,9 @@ const uk = {
torrentPeerOptionsHint: 'Залиште порожнім для стандартних параметрів Aria2 (55 пірів і 50K). 0 пірів означає без обмежень.',
torrentMaxPeersInvalid: 'Максимум пірів має бути цілим числом від 0 до 1000',
torrentPeerSpeedLimitInvalid: 'Поріг швидкості пірів має бути більшим за нуль',
+ torrentStopTimeout: 'Зупиняти торрент без швидкості через',
+ torrentStopTimeoutHint: 'Aria2 зупинить цей торрент після вказаної кількості секунд поспіль зі швидкістю 0 Б/с. 0 вимикає правило.',
+ torrentStopTimeoutInvalid: 'Тайм-аут зупинки торрента має бути цілим числом від 0 до 604800 секунд',
required: 'Обов\'язково',
free: 'Вільно',
preview: 'Попередній перегляд',
diff --git a/src/i18n/catalogs/zh-CN.ts b/src/i18n/catalogs/zh-CN.ts
index 566fa46..15481a2 100644
--- a/src/i18n/catalogs/zh-CN.ts
+++ b/src/i18n/catalogs/zh-CN.ts
@@ -245,6 +245,10 @@ const zhCN = {
torrentPeerSpeedLimit: '对等节点速度阈值',
torrentMaxPeersInvalid: 'Torrent 最大对等节点数必须是 0 到 1000 之间的整数',
torrentPeerSpeedLimitInvalid: '对等节点速度阈值必须大于零',
+ seconds: '秒',
+ torrentStopTimeout: '在此时间后停止无速度 Torrent',
+ torrentStopTimeoutHint: 'Aria2 会在速度连续为 0 B/s 达到此秒数后停止该 Torrent。0 表示禁用;更改会在 Torrent 启动或重试时应用。',
+ torrentStopTimeoutInvalid: 'Torrent 停止超时必须是 0 到 604800 秒之间的整数',
liveTorrentPeerOptionsFailed: '无法更新 Torrent 实时对等节点控制:{{detail}}',
category: '类别',
lastTry: '上次尝试',
@@ -480,6 +484,7 @@ const zhCN = {
seedAfterDownload: '下载完成后继续做种',
seedTime: '做种时间',
minutes: '分钟',
+ seconds: '秒',
seedRatio: '做种比率',
seedRatioHint: '0 表示仅按时间做种;否则达到第一个限制时停止做种。',
limitTorrentUpload: '限制种子上传',
@@ -497,6 +502,9 @@ const zhCN = {
torrentPeerOptionsHint: '留空以使用 Aria2 默认值(55 个节点和 50K)。0 个节点表示不限制。',
torrentMaxPeersInvalid: 'Torrent 最大对等节点数必须是 0 到 1000 之间的整数',
torrentPeerSpeedLimitInvalid: '对等节点速度阈值必须大于零',
+ torrentStopTimeout: '在此时间后停止无速度 Torrent',
+ torrentStopTimeoutHint: 'Aria2 会在速度连续为 0 B/s 达到此秒数后停止该 Torrent。0 表示禁用。',
+ torrentStopTimeoutInvalid: 'Torrent 停止超时必须是 0 到 604800 秒之间的整数',
required: '必需',
free: '可用空间',
preview: '预览',
diff --git a/src/store/useDownloadStore.test.ts b/src/store/useDownloadStore.test.ts
index 3781587..9fd6414 100644
--- a/src/store/useDownloadStore.test.ts
+++ b/src/store/useDownloadStore.test.ts
@@ -854,13 +854,15 @@ describe('useDownloadStore', () => {
torrentMaxPeers: 'not-a-number' as unknown as number,
torrentPeerSpeedLimit: 0 as unknown as string,
torrentCheckIntegrity: 'yes' as unknown as boolean,
- torrentTrackers: 123 as unknown as string
+ torrentTrackers: 123 as unknown as string,
+ torrentStopTimeout: 604801
});
expect(normalized.torrentMaxPeers).toBeUndefined();
expect(normalized.torrentPeerSpeedLimit).toBeUndefined();
expect(normalized.torrentCheckIntegrity).toBeUndefined();
expect(normalized.torrentTrackers).toBeUndefined();
+ expect(normalized.torrentStopTimeout).toBeUndefined();
});
it('normalizes proxy settings for download dispatch', async () => {
@@ -1512,7 +1514,8 @@ describe('useDownloadStore', () => {
dateAdded: '',
isTorrent: true,
torrentCheckIntegrity: true,
- torrentTrackers: 'https://tracker.example/announce'
+ torrentTrackers: 'https://tracker.example/announce',
+ torrentStopTimeout: 300
}, { type: 'start-now' });
const item = useDownloadStore.getState().downloads[0];
@@ -1524,7 +1527,8 @@ describe('useDownloadStore', () => {
item: expect.objectContaining({
id: 'start-1',
torrent_check_integrity: true,
- torrent_trackers: 'https://tracker.example/announce'
+ torrent_trackers: 'https://tracker.example/announce',
+ torrent_stop_timeout: 300
})
})
);
diff --git a/src/store/useDownloadStore.ts b/src/store/useDownloadStore.ts
index 37ac813..4e33e9f 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, normalizeSpeedLimitForBackend, redactDownloadForPersistence, resolveDownloadConnections } from '../utils/downloads';
+import { canonicalizeDownloadFileName, categoryForFileName, isActiveDownloadStatus, isTransferActiveStatus, MAX_TORRENT_STOP_TIMEOUT, normalizeSpeedLimitForBackend, redactDownloadForPersistence, resolveDownloadConnections } from '../utils/downloads';
import {
resolveCategoryDestination
} from '../utils/downloadLocations';
@@ -352,6 +352,7 @@ async function dispatchItemInternal(id: string, proxyOverride?: string | null):
torrent_peer_speed_limit: item.torrentPeerSpeedLimit || undefined,
torrent_check_integrity: item.torrentCheckIntegrity,
torrent_trackers: item.torrentTrackers || undefined,
+ torrent_stop_timeout: item.torrentStopTimeout,
lifecycle_generation: lifecycleGeneration.toString(),
};
@@ -636,16 +637,25 @@ export const normalizePersistedDownloadProgress = (download: DownloadItem): Down
const normalizedTrackers = typeof rawTrackers === 'string' && rawTrackers.trim()
? rawTrackers.trim()
: undefined;
+ const rawStopTimeout = download.torrentStopTimeout as unknown;
+ const normalizedStopTimeout = typeof rawStopTimeout === 'number' &&
+ Number.isInteger(rawStopTimeout) &&
+ rawStopTimeout >= 0 &&
+ rawStopTimeout <= MAX_TORRENT_STOP_TIMEOUT
+ ? rawStopTimeout
+ : undefined;
const normalizedOptions = rawMaxPeers !== normalizedMaxPeers ||
rawPeerSpeedLimit !== normalizedPeerSpeedLimit ||
rawCheckIntegrity !== normalizedCheckIntegrity ||
- rawTrackers !== normalizedTrackers
+ rawTrackers !== normalizedTrackers ||
+ rawStopTimeout !== normalizedStopTimeout
? {
...download,
torrentMaxPeers: normalizedMaxPeers,
torrentPeerSpeedLimit: normalizedPeerSpeedLimit,
torrentCheckIntegrity: normalizedCheckIntegrity,
- torrentTrackers: normalizedTrackers
+ torrentTrackers: normalizedTrackers,
+ torrentStopTimeout: normalizedStopTimeout
}
: download;
@@ -2178,6 +2188,7 @@ export const useDownloadStore = create((set, get) => {
torrent_peer_speed_limit: item.torrentPeerSpeedLimit || undefined,
torrent_check_integrity: item.torrentCheckIntegrity,
torrent_trackers: item.torrentTrackers || undefined,
+ torrent_stop_timeout: item.torrentStopTimeout,
lifecycle_generation: currentDownloadLifecycle(item.id).toString(),
});
}
diff --git a/src/utils/downloads.ts b/src/utils/downloads.ts
index fa5bfd5..bb4d50d 100644
--- a/src/utils/downloads.ts
+++ b/src/utils/downloads.ts
@@ -120,6 +120,7 @@ export const normalizeSpeedLimitForBackend = (value?: string | null): string | n
const MAX_TORRENT_TRACKERS = 64;
const MAX_TORRENT_TRACKER_BYTES = 16 * 1024;
+export const MAX_TORRENT_STOP_TIMEOUT = 7 * 24 * 60 * 60;
/**
* Performs the same user-facing safety checks as the native tracker boundary.