mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-02 07:30:18 +00:00
feat(torrents): add tracker timing controls
This commit is contained in:
+8
-6
@@ -41,6 +41,11 @@ belong in the download UI. The Aria2 reference is the [1.37.0 manual](https://ar
|
||||
`bt-force-encryption`, `bt-require-crypto`, and `bt-min-crypto-level`:
|
||||
disabled, required obfuscated handshake, or forced ARC4 payload encryption.
|
||||
The policy is persisted and reapplied when a Torrent starts or retries.
|
||||
- Optional tracker timing controls through `bt-tracker-connect-timeout`,
|
||||
`bt-tracker-timeout`, and `bt-tracker-interval`. Connect and request
|
||||
timeouts are bounded to 1–604800 seconds; interval 0 restores Aria2's
|
||||
response/progress-driven scheduling. Timing is persisted and reapplied when
|
||||
a Torrent starts or retries.
|
||||
- Optional `bt-remove-unselected-file` cleanup after completion when a
|
||||
selected-file subset is configured. Firelink requires explicit confirmation,
|
||||
reserves the unselected paths against competing downloads, keeps those
|
||||
@@ -48,8 +53,8 @@ belong in the download UI. The Aria2 reference is the [1.37.0 manual](https://ar
|
||||
after observing Aria2's completion cleanup (or on terminal failure,
|
||||
cancellation, or reconfiguration).
|
||||
- Deterministic local Aria2 smoke coverage for metadata resolution, selected
|
||||
output, piece priority, encryption policy, pause/resume, ownership,
|
||||
cancellation/removal, unavailable trackers, daemon failure, and
|
||||
output, piece priority, encryption policy, tracker timing, pause/resume,
|
||||
ownership, cancellation/removal, unavailable trackers, daemon failure, and
|
||||
`bt-stop-timeout` terminal behavior; RPC-boundary coverage is separate.
|
||||
|
||||
## Priority tiers for remaining work
|
||||
@@ -65,9 +70,6 @@ No remaining Tier 0 items.
|
||||
per-file priority option. Firelink therefore does not pretend that
|
||||
`select-file` is file priority; this remains pending an engine capability or
|
||||
a safe product-level model.
|
||||
2. **Tracker timing controls** — expose tracker connect timeout, request
|
||||
timeout, and interval only when their effect on battery/network behavior is
|
||||
explained and persisted.
|
||||
|
||||
### Tier 2 — advanced networking and daemon tuning
|
||||
|
||||
@@ -81,4 +83,4 @@ No remaining Tier 0 items.
|
||||
The first implementation in this task was remote `.torrent` metadata intake;
|
||||
follow-up implementations add stall-timeout control, bounded peer diagnostics,
|
||||
persisted tracker exclusion, piece-preview priority, safe unselected-file
|
||||
removal, and the validated encryption policy.
|
||||
removal, the validated encryption policy, and tracker timing controls.
|
||||
|
||||
@@ -716,6 +716,9 @@ async function main() {
|
||||
{
|
||||
dir: finalDir,
|
||||
'bt-tracker': `http://127.0.0.1:${trackerPort}/announce`,
|
||||
'bt-tracker-connect-timeout': '5',
|
||||
'bt-tracker-timeout': '7',
|
||||
'bt-tracker-interval': '2',
|
||||
'select-file': '1',
|
||||
'bt-prioritize-piece': 'head=32K,tail=16K',
|
||||
'index-out': indexOut,
|
||||
@@ -730,6 +733,9 @@ async function main() {
|
||||
finalOptions['bt-prioritize-piece'] === 'head=32K,tail=16K',
|
||||
`Aria2 did not retain the piece-priority option: ${JSON.stringify(finalOptions['bt-prioritize-piece'])}`,
|
||||
);
|
||||
assert(finalOptions['bt-tracker-connect-timeout'] === '5', 'Aria2 did not retain tracker connect timeout');
|
||||
assert(finalOptions['bt-tracker-timeout'] === '7', 'Aria2 did not retain tracker request timeout');
|
||||
assert(finalOptions['bt-tracker-interval'] === '2', 'Aria2 did not retain tracker interval');
|
||||
await rpc(client.rpcPort, client.secret, 'aria2.forcePause', [finalGid]);
|
||||
await waitForStatus(client, finalGid, 'paused', 10000);
|
||||
await rpc(client.rpcPort, client.secret, 'aria2.unpause', [finalGid]);
|
||||
|
||||
@@ -202,6 +202,15 @@ pub struct DownloadItem {
|
||||
pub torrent_exclude_trackers: Option<String>,
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub torrent_tracker_connect_timeout: Option<u32>,
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub torrent_tracker_timeout: Option<u32>,
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub torrent_tracker_interval: Option<u32>,
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub torrent_stop_timeout: Option<u32>,
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
|
||||
@@ -5802,6 +5802,13 @@ async fn validate_torrent_enqueue(
|
||||
item.torrent_trackers = queue::normalize_torrent_trackers(item.torrent_trackers.as_deref())?;
|
||||
item.torrent_exclude_trackers =
|
||||
queue::normalize_torrent_exclude_trackers(item.torrent_exclude_trackers.as_deref())?;
|
||||
item.torrent_tracker_connect_timeout = queue::normalize_torrent_tracker_connect_timeout(
|
||||
item.torrent_tracker_connect_timeout,
|
||||
)?;
|
||||
item.torrent_tracker_timeout =
|
||||
queue::normalize_torrent_tracker_request_timeout(item.torrent_tracker_timeout)?;
|
||||
item.torrent_tracker_interval =
|
||||
queue::normalize_torrent_tracker_interval(item.torrent_tracker_interval)?;
|
||||
item.torrent_stop_timeout = queue::normalize_torrent_stop_timeout(item.torrent_stop_timeout)?;
|
||||
item.torrent_prioritize_piece = queue::normalize_torrent_prioritize_piece(
|
||||
item.torrent_prioritize_piece.as_deref(),
|
||||
|
||||
@@ -22,6 +22,8 @@ pub const MEDIA_RUN_CANCELLED: &str = "__firelink_media_run_cancelled__";
|
||||
pub const DOWNLOAD_CONNECTIONS_MIN: i32 = 1;
|
||||
pub const DOWNLOAD_CONNECTIONS_MAX: i32 = 16;
|
||||
pub const MAX_TORRENT_PIECE_PRIORITY_SIZE_MIB: u64 = 1024;
|
||||
pub const MAX_TORRENT_TRACKER_TIMEOUT: u32 = 604_800;
|
||||
pub const MAX_TORRENT_TRACKER_INTERVAL: u32 = 604_800;
|
||||
|
||||
pub fn clamp_download_connections(connections: i32) -> i32 {
|
||||
connections.clamp(DOWNLOAD_CONNECTIONS_MIN, DOWNLOAD_CONNECTIONS_MAX)
|
||||
@@ -218,6 +220,9 @@ pub struct SpawnPayload {
|
||||
pub torrent_check_integrity: bool,
|
||||
pub torrent_trackers: Option<String>,
|
||||
pub torrent_exclude_trackers: Option<String>,
|
||||
pub torrent_tracker_connect_timeout: Option<u32>,
|
||||
pub torrent_tracker_timeout: Option<u32>,
|
||||
pub torrent_tracker_interval: Option<u32>,
|
||||
pub torrent_stop_timeout: Option<u32>,
|
||||
pub torrent_prioritize_piece: Option<String>,
|
||||
pub torrent_remove_unselected_file: bool,
|
||||
@@ -3507,6 +3512,44 @@ pub(crate) fn normalize_torrent_exclude_trackers(
|
||||
normalize_torrent_tracker_list(value, true)
|
||||
}
|
||||
|
||||
fn normalize_torrent_tracker_timeout(value: Option<u32>, field: &str) -> Result<Option<u32>, String> {
|
||||
let Some(value) = value else {
|
||||
return Ok(None);
|
||||
};
|
||||
if !(1..=MAX_TORRENT_TRACKER_TIMEOUT).contains(&value) {
|
||||
return Err(format!(
|
||||
"torrent {field} must be between 1 and {MAX_TORRENT_TRACKER_TIMEOUT} seconds"
|
||||
));
|
||||
}
|
||||
Ok(Some(value))
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_torrent_tracker_connect_timeout(
|
||||
value: Option<u32>,
|
||||
) -> Result<Option<u32>, String> {
|
||||
normalize_torrent_tracker_timeout(value, "tracker connect timeout")
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_torrent_tracker_request_timeout(
|
||||
value: Option<u32>,
|
||||
) -> Result<Option<u32>, String> {
|
||||
normalize_torrent_tracker_timeout(value, "tracker timeout")
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_torrent_tracker_interval(
|
||||
value: Option<u32>,
|
||||
) -> Result<Option<u32>, String> {
|
||||
let Some(value) = value else {
|
||||
return Ok(None);
|
||||
};
|
||||
if value > MAX_TORRENT_TRACKER_INTERVAL {
|
||||
return Err(format!(
|
||||
"torrent tracker interval must be between 0 and {MAX_TORRENT_TRACKER_INTERVAL} seconds"
|
||||
));
|
||||
}
|
||||
Ok(Some(value))
|
||||
}
|
||||
|
||||
pub(crate) fn normalize_torrent_encryption_policy(
|
||||
value: Option<&str>,
|
||||
) -> Result<Option<String>, String> {
|
||||
@@ -3614,6 +3657,28 @@ fn apply_aria2_torrent_options(
|
||||
{
|
||||
options.insert("bt-exclude-tracker".to_string(), serde_json::json!(trackers));
|
||||
}
|
||||
if let Some(timeout) = normalize_torrent_tracker_connect_timeout(
|
||||
payload.torrent_tracker_connect_timeout,
|
||||
)? {
|
||||
options.insert(
|
||||
"bt-tracker-connect-timeout".to_string(),
|
||||
serde_json::json!(timeout.to_string()),
|
||||
);
|
||||
}
|
||||
if let Some(timeout) =
|
||||
normalize_torrent_tracker_request_timeout(payload.torrent_tracker_timeout)?
|
||||
{
|
||||
options.insert(
|
||||
"bt-tracker-timeout".to_string(),
|
||||
serde_json::json!(timeout.to_string()),
|
||||
);
|
||||
}
|
||||
if let Some(interval) = normalize_torrent_tracker_interval(payload.torrent_tracker_interval)? {
|
||||
options.insert(
|
||||
"bt-tracker-interval".to_string(),
|
||||
serde_json::json!(interval.to_string()),
|
||||
);
|
||||
}
|
||||
if let Some(stop_timeout) = normalize_torrent_stop_timeout(payload.torrent_stop_timeout)? {
|
||||
options.insert(
|
||||
"bt-stop-timeout".to_string(),
|
||||
@@ -4257,6 +4322,15 @@ pub struct EnqueueItem {
|
||||
pub torrent_exclude_trackers: Option<String>,
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub torrent_tracker_connect_timeout: Option<u32>,
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub torrent_tracker_timeout: Option<u32>,
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub torrent_tracker_interval: Option<u32>,
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
pub torrent_stop_timeout: Option<u32>,
|
||||
#[serde(default)]
|
||||
#[ts(optional)]
|
||||
@@ -4319,6 +4393,9 @@ impl EnqueueItem {
|
||||
torrent_check_integrity: self.torrent_check_integrity.unwrap_or(false),
|
||||
torrent_trackers: self.torrent_trackers,
|
||||
torrent_exclude_trackers: self.torrent_exclude_trackers,
|
||||
torrent_tracker_connect_timeout: self.torrent_tracker_connect_timeout,
|
||||
torrent_tracker_timeout: self.torrent_tracker_timeout,
|
||||
torrent_tracker_interval: self.torrent_tracker_interval,
|
||||
torrent_stop_timeout: self.torrent_stop_timeout,
|
||||
torrent_prioritize_piece: self.torrent_prioritize_piece,
|
||||
torrent_remove_unselected_file: self
|
||||
@@ -4619,6 +4696,28 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enqueue_item_preserves_torrent_tracker_timing() {
|
||||
let item: EnqueueItem = serde_json::from_value(serde_json::json!({
|
||||
"id": "torrent-tracker-timing",
|
||||
"queue_id": "main",
|
||||
"url": "magnet:?xt=urn:btih:0123456789012345678901234567890123456789",
|
||||
"destination": "/tmp/downloads",
|
||||
"filename": "payload",
|
||||
"is_media": false,
|
||||
"is_torrent": true,
|
||||
"torrent_tracker_connect_timeout": 11,
|
||||
"torrent_tracker_timeout": 22,
|
||||
"torrent_tracker_interval": 33
|
||||
}))
|
||||
.expect("frontend enqueue payload should deserialize");
|
||||
|
||||
let payload = item.into_task().payload;
|
||||
assert_eq!(payload.torrent_tracker_connect_timeout, Some(11));
|
||||
assert_eq!(payload.torrent_tracker_timeout, Some(22));
|
||||
assert_eq!(payload.torrent_tracker_interval, Some(33));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn torrent_trackers_are_normalized_and_deduplicated() {
|
||||
assert_eq!(
|
||||
@@ -4676,6 +4775,62 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn torrent_tracker_timing_is_bounded_and_preserves_aria2_defaults() {
|
||||
assert_eq!(normalize_torrent_tracker_connect_timeout(None).unwrap(), None);
|
||||
assert_eq!(
|
||||
normalize_torrent_tracker_connect_timeout(Some(1)).unwrap(),
|
||||
Some(1)
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_torrent_tracker_request_timeout(Some(MAX_TORRENT_TRACKER_TIMEOUT)).unwrap(),
|
||||
Some(MAX_TORRENT_TRACKER_TIMEOUT)
|
||||
);
|
||||
assert_eq!(normalize_torrent_tracker_interval(Some(0)).unwrap(), Some(0));
|
||||
assert!(normalize_torrent_tracker_connect_timeout(Some(0)).is_err());
|
||||
assert!(normalize_torrent_tracker_request_timeout(Some(0)).is_err());
|
||||
assert!(normalize_torrent_tracker_interval(Some(MAX_TORRENT_TRACKER_INTERVAL + 1)).is_err());
|
||||
|
||||
let mut options = serde_json::Map::new();
|
||||
let payload = SpawnPayload {
|
||||
is_torrent: true,
|
||||
torrent_tracker_connect_timeout: Some(11),
|
||||
torrent_tracker_timeout: Some(22),
|
||||
torrent_tracker_interval: Some(33),
|
||||
..Default::default()
|
||||
};
|
||||
apply_aria2_torrent_options(&mut options, &payload).unwrap();
|
||||
assert_eq!(
|
||||
options.get("bt-tracker-connect-timeout"),
|
||||
Some(&serde_json::json!("11"))
|
||||
);
|
||||
assert_eq!(
|
||||
options.get("bt-tracker-timeout"),
|
||||
Some(&serde_json::json!("22"))
|
||||
);
|
||||
assert_eq!(
|
||||
options.get("bt-tracker-interval"),
|
||||
Some(&serde_json::json!("33"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn torrent_tracker_timing_is_not_applied_to_non_torrent_payloads() {
|
||||
let mut options = serde_json::Map::new();
|
||||
let payload = SpawnPayload {
|
||||
torrent_tracker_connect_timeout: Some(11),
|
||||
torrent_tracker_timeout: Some(22),
|
||||
torrent_tracker_interval: Some(33),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
apply_aria2_torrent_options(&mut options, &payload).unwrap();
|
||||
|
||||
assert!(!options.contains_key("bt-tracker-connect-timeout"));
|
||||
assert!(!options.contains_key("bt-tracker-timeout"));
|
||||
assert!(!options.contains_key("bt-tracker-interval"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn torrent_trackers_are_emitted_as_the_aria2_tracker_option() {
|
||||
let mut options = serde_json::Map::new();
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
import type { DownloadCategory } from "./DownloadCategory";
|
||||
import type { DownloadStatus } from "./DownloadStatus";
|
||||
|
||||
export type DownloadItem = { id: string, url: string, fileName: string, status: DownloadStatus, fraction?: number, speed?: string, eta?: string, size?: string, downloadedBytes?: number, totalBytes?: number, totalIsEstimate?: boolean, category: DownloadCategory, dateAdded: string, resumable?: boolean, connections?: number, speedLimit?: string, username?: string, password?: string, headers?: string, checksum?: string, cookies?: string, mirrors?: string, destination?: string, isMedia?: boolean, mediaFormatSelector?: string, mediaQuality?: string, queueId?: string, queuePosition?: number, hasBeenDispatched?: boolean, lastError?: string, lastTry?: string, isTorrent?: boolean, torrentPath?: string, torrentFileIndices?: Array<number>, torrentInfoHash?: string, torrentSeedTime?: number, torrentSeedRatio?: number, torrentUploadLimit?: string, torrentMaxPeers?: number, torrentPeerSpeedLimit?: string, torrentCheckIntegrity?: boolean, torrentTrackers?: string, torrentExcludeTrackers?: string, torrentStopTimeout?: number, torrentPrioritizePiece?: string, torrentRemoveUnselectedFile?: boolean, torrentEncryptionPolicy?: string, };
|
||||
export type DownloadItem = { id: string, url: string, fileName: string, status: DownloadStatus, fraction?: number, speed?: string, eta?: string, size?: string, downloadedBytes?: number, totalBytes?: number, totalIsEstimate?: boolean, category: DownloadCategory, dateAdded: string, resumable?: boolean, connections?: number, speedLimit?: string, username?: string, password?: string, headers?: string, checksum?: string, cookies?: string, mirrors?: string, destination?: string, isMedia?: boolean, mediaFormatSelector?: string, mediaQuality?: string, queueId?: string, queuePosition?: number, hasBeenDispatched?: boolean, lastError?: string, lastTry?: string, isTorrent?: boolean, torrentPath?: string, torrentFileIndices?: Array<number>, torrentInfoHash?: string, torrentSeedTime?: number, torrentSeedRatio?: number, torrentUploadLimit?: string, torrentMaxPeers?: number, torrentPeerSpeedLimit?: string, torrentCheckIntegrity?: boolean, torrentTrackers?: string, torrentExcludeTrackers?: string, torrentTrackerConnectTimeout?: number, torrentTrackerTimeout?: number, torrentTrackerInterval?: number, torrentStopTimeout?: number, torrentPrioritizePiece?: string, torrentRemoveUnselectedFile?: boolean, torrentEncryptionPolicy?: string, };
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type EnqueueItem = { id: string, queue_id: string, url: string, destination: string, filename: string, connections: number | null, speed_limit: string | null, username: string | null, password: string | null, headers: string | null, checksum: string | null, cookies: string | null, mirrors: string | null, user_agent: string | null, max_tries: number | null, proxy: string | null, format_selector: string | null, cookie_source: string | null, is_media: boolean | null, is_torrent?: boolean, torrent_path?: string, torrent_file_indices?: Array<number>, torrent_info_hash?: string, torrent_seed_time?: number, torrent_seed_ratio?: number, torrent_upload_limit?: string, torrent_max_peers?: number, torrent_peer_speed_limit?: string, torrent_check_integrity?: boolean, torrent_trackers?: string, torrent_exclude_trackers?: string, torrent_stop_timeout?: number, torrent_prioritize_piece?: string, torrent_remove_unselected_file?: boolean, torrent_encryption_policy?: string, lifecycle_generation?: string, };
|
||||
export type EnqueueItem = { id: string, queue_id: string, url: string, destination: string, filename: string, connections: number | null, speed_limit: string | null, username: string | null, password: string | null, headers: string | null, checksum: string | null, cookies: string | null, mirrors: string | null, user_agent: string | null, max_tries: number | null, proxy: string | null, format_selector: string | null, cookie_source: string | null, is_media: boolean | null, is_torrent?: boolean, torrent_path?: string, torrent_file_indices?: Array<number>, torrent_info_hash?: string, torrent_seed_time?: number, torrent_seed_ratio?: number, torrent_upload_limit?: string, torrent_max_peers?: number, torrent_peer_speed_limit?: string, torrent_check_integrity?: boolean, torrent_trackers?: string, torrent_exclude_trackers?: string, torrent_tracker_connect_timeout?: number, torrent_tracker_timeout?: number, torrent_tracker_interval?: number, torrent_stop_timeout?: number, torrent_prioritize_piece?: string, torrent_remove_unselected_file?: boolean, torrent_encryption_policy?: string, lifecycle_generation?: string, };
|
||||
|
||||
@@ -13,7 +13,7 @@ import { FolderPlus, Save, Settings, Shield, RefreshCw, FileText, HardDrive, Dat
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import { invokeCommand as invoke } from '../ipc';
|
||||
import { DuplicateResolutionModal, DuplicateConflict } from './DuplicateResolutionModal';
|
||||
import { canonicalizeDownloadFileName, categoryForFileName, downloadFileNameWithSuffix, downloadFileNamesMatch, downloadMediaKindsMatch, isValidTorrentExcludeTrackerList, isValidTorrentTrackerList, MAX_TORRENT_STOP_TIMEOUT, normalizeSpeedLimitForBackend, normalizeTorrentPrioritizePiece, TORRENT_ENCRYPTION_POLICY_DISABLED, TORRENT_ENCRYPTION_POLICY_FORCE_ENCRYPTION, TORRENT_ENCRYPTION_POLICY_REQUIRE_CRYPTO, type TorrentEncryptionPolicy } from '../utils/downloads';
|
||||
import { canonicalizeDownloadFileName, categoryForFileName, downloadFileNameWithSuffix, downloadFileNamesMatch, downloadMediaKindsMatch, isValidTorrentExcludeTrackerList, isValidTorrentTrackerList, MAX_TORRENT_STOP_TIMEOUT, MAX_TORRENT_TRACKER_INTERVAL, MAX_TORRENT_TRACKER_TIMEOUT, normalizeSpeedLimitForBackend, normalizeTorrentPrioritizePiece, normalizeTorrentTrackerInterval, normalizeTorrentTrackerTimeout, TORRENT_ENCRYPTION_POLICY_DISABLED, TORRENT_ENCRYPTION_POLICY_FORCE_ENCRYPTION, TORRENT_ENCRYPTION_POLICY_REQUIRE_CRYPTO, type TorrentEncryptionPolicy } from '../utils/downloads';
|
||||
import { fetchMediaMetadataDeduped, fetchMediaPlaylistMetadataDeduped } from '../utils/mediaMetadata';
|
||||
import {
|
||||
expandTilde,
|
||||
@@ -237,6 +237,9 @@ export const AddDownloadsModal = () => {
|
||||
const [torrentEncryptionPolicy, setTorrentEncryptionPolicy] = useState<TorrentEncryptionPolicy>(TORRENT_ENCRYPTION_POLICY_DISABLED);
|
||||
const [torrentTrackers, setTorrentTrackers] = useState('');
|
||||
const [torrentExcludeTrackers, setTorrentExcludeTrackers] = useState('');
|
||||
const [torrentTrackerConnectTimeout, setTorrentTrackerConnectTimeout] = useState('');
|
||||
const [torrentTrackerTimeout, setTorrentTrackerTimeout] = useState('');
|
||||
const [torrentTrackerInterval, setTorrentTrackerInterval] = useState('0');
|
||||
const [torrentStopTimeout, setTorrentStopTimeout] = useState('0');
|
||||
const [torrentPrioritizePiece, setTorrentPrioritizePiece] = useState('');
|
||||
const [freeSpace, setFreeSpace] = useState('Unknown');
|
||||
@@ -382,6 +385,9 @@ export const AddDownloadsModal = () => {
|
||||
setTorrentCheckIntegrity(false);
|
||||
setTorrentTrackers('');
|
||||
setTorrentExcludeTrackers('');
|
||||
setTorrentTrackerConnectTimeout('');
|
||||
setTorrentTrackerTimeout('');
|
||||
setTorrentTrackerInterval('0');
|
||||
setTorrentStopTimeout('0');
|
||||
setUseAuth(false);
|
||||
setUsername('');
|
||||
@@ -987,6 +993,18 @@ export const AddDownloadsModal = () => {
|
||||
addToast({ message: t($ => $.addDownloads.torrentExcludeTrackersInvalid), variant: 'error', isActionable: true });
|
||||
return;
|
||||
}
|
||||
if (hasSelectedTorrent && torrentTrackerConnectTimeout.trim() && !normalizeTorrentTrackerTimeout(torrentTrackerConnectTimeout)) {
|
||||
addToast({ message: t($ => $.addDownloads.torrentTrackerTimeoutInvalid), variant: 'error', isActionable: true });
|
||||
return;
|
||||
}
|
||||
if (hasSelectedTorrent && torrentTrackerTimeout.trim() && !normalizeTorrentTrackerTimeout(torrentTrackerTimeout)) {
|
||||
addToast({ message: t($ => $.addDownloads.torrentTrackerTimeoutInvalid), variant: 'error', isActionable: true });
|
||||
return;
|
||||
}
|
||||
if (hasSelectedTorrent && torrentTrackerInterval.trim() && normalizeTorrentTrackerInterval(torrentTrackerInterval) === undefined) {
|
||||
addToast({ message: t($ => $.addDownloads.torrentTrackerIntervalInvalid), variant: 'error', isActionable: true });
|
||||
return;
|
||||
}
|
||||
if (hasSelectedTorrent && torrentPrioritizePiece.trim() && !normalizeTorrentPrioritizePiece(torrentPrioritizePiece)) {
|
||||
addToast({ message: t($ => $.addDownloads.torrentPrioritizePieceInvalid), variant: 'error', isActionable: true });
|
||||
return;
|
||||
@@ -1501,6 +1519,15 @@ export const AddDownloadsModal = () => {
|
||||
: undefined,
|
||||
torrentTrackers: item.isTorrent ? torrentTrackers.trim() || undefined : undefined,
|
||||
torrentExcludeTrackers: item.isTorrent ? torrentExcludeTrackers.trim() || undefined : undefined,
|
||||
torrentTrackerConnectTimeout: item.isTorrent && torrentTrackerConnectTimeout.trim()
|
||||
? Number(torrentTrackerConnectTimeout)
|
||||
: undefined,
|
||||
torrentTrackerTimeout: item.isTorrent && torrentTrackerTimeout.trim()
|
||||
? Number(torrentTrackerTimeout)
|
||||
: undefined,
|
||||
torrentTrackerInterval: item.isTorrent && torrentTrackerInterval.trim()
|
||||
? Number(torrentTrackerInterval)
|
||||
: undefined,
|
||||
torrentStopTimeout: item.isTorrent && torrentStopTimeout.trim() ? Number(torrentStopTimeout) : undefined,
|
||||
torrentPrioritizePiece: item.isTorrent ? normalizeTorrentPrioritizePiece(torrentPrioritizePiece) || undefined : undefined,
|
||||
size: item.size || (item.sizeBytes ? formatBytes(item.sizeBytes) : undefined),
|
||||
@@ -2242,6 +2269,64 @@ export const AddDownloadsModal = () => {
|
||||
{t($ => $.addDownloads.torrentExcludeTrackersHint)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-[1fr_auto] gap-2 items-center pt-2 border-t border-border-modal/50">
|
||||
<label htmlFor="torrent-tracker-connect-timeout" className="text-text-muted">
|
||||
{t($ => $.addDownloads.torrentTrackerConnectTimeout)}
|
||||
</label>
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
id="torrent-tracker-connect-timeout"
|
||||
type="number"
|
||||
min={1}
|
||||
max={MAX_TORRENT_TRACKER_TIMEOUT}
|
||||
step={1}
|
||||
value={torrentTrackerConnectTimeout}
|
||||
onChange={event => setTorrentTrackerConnectTimeout(event.currentTarget.value)}
|
||||
placeholder="60"
|
||||
className="app-control w-24 px-2 py-1 text-end font-mono"
|
||||
aria-describedby="torrent-tracker-timing-hint"
|
||||
/>
|
||||
<span className="text-[10px] text-text-muted">{t($ => $.addDownloads.seconds)}</span>
|
||||
</div>
|
||||
<label htmlFor="torrent-tracker-timeout" className="text-text-muted">
|
||||
{t($ => $.addDownloads.torrentTrackerTimeout)}
|
||||
</label>
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
id="torrent-tracker-timeout"
|
||||
type="number"
|
||||
min={1}
|
||||
max={MAX_TORRENT_TRACKER_TIMEOUT}
|
||||
step={1}
|
||||
value={torrentTrackerTimeout}
|
||||
onChange={event => setTorrentTrackerTimeout(event.currentTarget.value)}
|
||||
placeholder="60"
|
||||
className="app-control w-24 px-2 py-1 text-end font-mono"
|
||||
aria-describedby="torrent-tracker-timing-hint"
|
||||
/>
|
||||
<span className="text-[10px] text-text-muted">{t($ => $.addDownloads.seconds)}</span>
|
||||
</div>
|
||||
<label htmlFor="torrent-tracker-interval" className="text-text-muted">
|
||||
{t($ => $.addDownloads.torrentTrackerInterval)}
|
||||
</label>
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
id="torrent-tracker-interval"
|
||||
type="number"
|
||||
min={0}
|
||||
max={MAX_TORRENT_TRACKER_INTERVAL}
|
||||
step={1}
|
||||
value={torrentTrackerInterval}
|
||||
onChange={event => setTorrentTrackerInterval(event.currentTarget.value)}
|
||||
className="app-control w-24 px-2 py-1 text-end font-mono"
|
||||
aria-describedby="torrent-tracker-timing-hint"
|
||||
/>
|
||||
<span className="text-[10px] text-text-muted">{t($ => $.addDownloads.seconds)}</span>
|
||||
</div>
|
||||
<p id="torrent-tracker-timing-hint" className="col-span-2 text-[10px] text-text-muted">
|
||||
{t($ => $.addDownloads.torrentTrackerTimingHint)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-[1fr_auto] gap-2 items-center pt-2 border-t border-border-modal/50">
|
||||
<label htmlFor="torrent-max-peers" className="text-text-muted">
|
||||
{t($ => $.addDownloads.torrentMaxPeers)}
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
formatDownloadTotal,
|
||||
resolveDownloadSizeDisplay
|
||||
} from '../utils/downloadProgress';
|
||||
import { isValidTorrentExcludeTrackerList, isValidTorrentTrackerList, MAX_TORRENT_STOP_TIMEOUT, normalizeSpeedLimitForBackend, normalizeTorrentEncryptionPolicy, normalizeTorrentPrioritizePiece, resolveDownloadConnections, TORRENT_ENCRYPTION_POLICY_DISABLED, TORRENT_ENCRYPTION_POLICY_FORCE_ENCRYPTION, TORRENT_ENCRYPTION_POLICY_REQUIRE_CRYPTO, type TorrentEncryptionPolicy } from '../utils/downloads';
|
||||
import { isValidTorrentExcludeTrackerList, isValidTorrentTrackerList, MAX_TORRENT_STOP_TIMEOUT, MAX_TORRENT_TRACKER_INTERVAL, MAX_TORRENT_TRACKER_TIMEOUT, normalizeSpeedLimitForBackend, normalizeTorrentEncryptionPolicy, normalizeTorrentPrioritizePiece, normalizeTorrentTrackerInterval, normalizeTorrentTrackerTimeout, resolveDownloadConnections, TORRENT_ENCRYPTION_POLICY_DISABLED, TORRENT_ENCRYPTION_POLICY_FORCE_ENCRYPTION, TORRENT_ENCRYPTION_POLICY_REQUIRE_CRYPTO, type TorrentEncryptionPolicy } from '../utils/downloads';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { formatDateTime, type CalendarPreference } from '../utils/dateTime';
|
||||
import { isTopmostModal, useModalFocus } from '../hooks/useModalFocus';
|
||||
@@ -91,6 +91,9 @@ export const PropertiesModal = () => {
|
||||
const [torrentEncryptionPolicy, setTorrentEncryptionPolicy] = useState<TorrentEncryptionPolicy>(TORRENT_ENCRYPTION_POLICY_DISABLED);
|
||||
const [torrentTrackers, setTorrentTrackers] = useState('');
|
||||
const [torrentExcludeTrackers, setTorrentExcludeTrackers] = useState('');
|
||||
const [torrentTrackerConnectTimeout, setTorrentTrackerConnectTimeout] = useState('');
|
||||
const [torrentTrackerTimeout, setTorrentTrackerTimeout] = useState('');
|
||||
const [torrentTrackerInterval, setTorrentTrackerInterval] = useState('0');
|
||||
const [torrentStopTimeout, setTorrentStopTimeout] = useState('0');
|
||||
const [torrentPrioritizePiece, setTorrentPrioritizePiece] = useState('');
|
||||
const [torrentPeerDiagnostics, setTorrentPeerDiagnostics] = useState<TorrentPeerDiagnostics | null>(null);
|
||||
@@ -197,6 +200,9 @@ export const PropertiesModal = () => {
|
||||
setTorrentEncryptionPolicy(normalizeTorrentEncryptionPolicy(activeItem.torrentEncryptionPolicy) || TORRENT_ENCRYPTION_POLICY_DISABLED);
|
||||
setTorrentTrackers(activeItem.torrentTrackers || '');
|
||||
setTorrentExcludeTrackers(activeItem.torrentExcludeTrackers || '');
|
||||
setTorrentTrackerConnectTimeout(activeItem.torrentTrackerConnectTimeout === undefined ? '' : String(activeItem.torrentTrackerConnectTimeout));
|
||||
setTorrentTrackerTimeout(activeItem.torrentTrackerTimeout === undefined ? '' : String(activeItem.torrentTrackerTimeout));
|
||||
setTorrentTrackerInterval(activeItem.torrentTrackerInterval === undefined ? '0' : String(activeItem.torrentTrackerInterval));
|
||||
setTorrentStopTimeout(activeItem.torrentStopTimeout === undefined ? '0' : String(activeItem.torrentStopTimeout));
|
||||
setTorrentPrioritizePiece(activeItem.torrentPrioritizePiece || '');
|
||||
setErrorMessage('');
|
||||
@@ -349,6 +355,18 @@ export const PropertiesModal = () => {
|
||||
setErrorMessage(t($ => $.properties.torrentExcludeTrackersInvalid));
|
||||
return;
|
||||
}
|
||||
if (item.isTorrent && torrentTrackerConnectTimeout.trim() && !normalizeTorrentTrackerTimeout(torrentTrackerConnectTimeout)) {
|
||||
setErrorMessage(t($ => $.properties.torrentTrackerTimeoutInvalid));
|
||||
return;
|
||||
}
|
||||
if (item.isTorrent && torrentTrackerTimeout.trim() && !normalizeTorrentTrackerTimeout(torrentTrackerTimeout)) {
|
||||
setErrorMessage(t($ => $.properties.torrentTrackerTimeoutInvalid));
|
||||
return;
|
||||
}
|
||||
if (item.isTorrent && torrentTrackerInterval.trim() && normalizeTorrentTrackerInterval(torrentTrackerInterval) === undefined) {
|
||||
setErrorMessage(t($ => $.properties.torrentTrackerIntervalInvalid));
|
||||
return;
|
||||
}
|
||||
if (item.isTorrent && torrentPrioritizePiece.trim() && !normalizeTorrentPrioritizePiece(torrentPrioritizePiece)) {
|
||||
setErrorMessage(t($ => $.properties.torrentPrioritizePieceInvalid));
|
||||
return;
|
||||
@@ -399,6 +417,15 @@ export const PropertiesModal = () => {
|
||||
torrentCheckIntegrity,
|
||||
torrentTrackers: torrentTrackers.trim() || undefined,
|
||||
torrentExcludeTrackers: torrentExcludeTrackers.trim() || undefined,
|
||||
torrentTrackerConnectTimeout: torrentTrackerConnectTimeout.trim()
|
||||
? Number(torrentTrackerConnectTimeout)
|
||||
: undefined,
|
||||
torrentTrackerTimeout: torrentTrackerTimeout.trim()
|
||||
? Number(torrentTrackerTimeout)
|
||||
: undefined,
|
||||
torrentTrackerInterval: torrentTrackerInterval.trim()
|
||||
? Number(torrentTrackerInterval)
|
||||
: undefined,
|
||||
torrentStopTimeout: normalizedStopTimeout,
|
||||
torrentPrioritizePiece: normalizeTorrentPrioritizePiece(torrentPrioritizePiece) || undefined,
|
||||
torrentRemoveUnselectedFile: item.torrentFileIndices !== undefined
|
||||
@@ -940,6 +967,71 @@ export const PropertiesModal = () => {
|
||||
{t($ => $.properties.torrentExcludeTrackersHint)}
|
||||
</p>
|
||||
</div>
|
||||
<label className="text-xs text-text-muted text-right" htmlFor="torrent-tracker-connect-timeout-properties">
|
||||
{t($ => $.properties.torrentTrackerConnectTimeout)}
|
||||
</label>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
id="torrent-tracker-connect-timeout-properties"
|
||||
type="number"
|
||||
min={1}
|
||||
max={MAX_TORRENT_TRACKER_TIMEOUT}
|
||||
step={1}
|
||||
value={torrentTrackerConnectTimeout}
|
||||
onChange={event => setTorrentTrackerConnectTimeout(event.currentTarget.value)}
|
||||
placeholder="60"
|
||||
disabled={transferLocked}
|
||||
aria-describedby="torrent-tracker-timing-properties-hint"
|
||||
className="app-control w-24 px-2.5 py-1.5 text-end text-xs font-mono disabled:opacity-50"
|
||||
/>
|
||||
<span className="text-[11px] text-text-muted">{t($ => $.properties.seconds)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<label className="text-xs text-text-muted text-right" htmlFor="torrent-tracker-timeout-properties">
|
||||
{t($ => $.properties.torrentTrackerTimeout)}
|
||||
</label>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
id="torrent-tracker-timeout-properties"
|
||||
type="number"
|
||||
min={1}
|
||||
max={MAX_TORRENT_TRACKER_TIMEOUT}
|
||||
step={1}
|
||||
value={torrentTrackerTimeout}
|
||||
onChange={event => setTorrentTrackerTimeout(event.currentTarget.value)}
|
||||
placeholder="60"
|
||||
disabled={transferLocked}
|
||||
aria-describedby="torrent-tracker-timing-properties-hint"
|
||||
className="app-control w-24 px-2.5 py-1.5 text-end text-xs font-mono disabled:opacity-50"
|
||||
/>
|
||||
<span className="text-[11px] text-text-muted">{t($ => $.properties.seconds)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<label className="text-xs text-text-muted text-right" htmlFor="torrent-tracker-interval-properties">
|
||||
{t($ => $.properties.torrentTrackerInterval)}
|
||||
</label>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
id="torrent-tracker-interval-properties"
|
||||
type="number"
|
||||
min={0}
|
||||
max={MAX_TORRENT_TRACKER_INTERVAL}
|
||||
step={1}
|
||||
value={torrentTrackerInterval}
|
||||
onChange={event => setTorrentTrackerInterval(event.currentTarget.value)}
|
||||
disabled={transferLocked}
|
||||
aria-describedby="torrent-tracker-timing-properties-hint"
|
||||
className="app-control w-24 px-2.5 py-1.5 text-end text-xs font-mono disabled:opacity-50"
|
||||
/>
|
||||
<span className="text-[11px] text-text-muted">{t($ => $.properties.seconds)}</span>
|
||||
</div>
|
||||
<p id="torrent-tracker-timing-properties-hint" className="mt-1 text-[11px] text-text-muted">
|
||||
{t($ => $.properties.torrentTrackerTimingHint)}
|
||||
</p>
|
||||
</div>
|
||||
<label className="text-xs text-text-muted text-right" htmlFor="torrent-stop-timeout-properties">
|
||||
{t($ => $.properties.torrentStopTimeout)}
|
||||
</label>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: 'حداکثر همتاهای تورنت',
|
||||
|
||||
@@ -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: 'מספר העמיתים המרבי בטורנט',
|
||||
|
||||
@@ -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: 'Максимум пиров торрента',
|
||||
|
||||
@@ -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: 'Максимум пірів торрента',
|
||||
|
||||
@@ -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 最大对等节点数',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<DownloadState>((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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user