mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-07 18:03:23 +00:00
feat(torrent): complete lifecycle controls
This commit is contained in:
@@ -3,4 +3,4 @@ import type { DownloadCategory } from "./DownloadCategory";
|
||||
import type { DownloadStatus } from "./DownloadStatus";
|
||||
import type { TorrentWebSeed } from "./TorrentWebSeed";
|
||||
|
||||
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, torrentSeedRemaining?: number, torrentWebSeeds?: Array<TorrentWebSeed>, 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, };
|
||||
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, torrentSeedRemaining?: number, torrentWebSeeds?: Array<TorrentWebSeed>, 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, torrentFileAllocation?: string, torrentVerifyOnly?: boolean, torrentVerifyRestoreStatus?: 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 DownloadStatus = "ready" | "staged" | "downloading" | "processing" | "seeding" | "waitingToSeed" | "paused" | "completed" | "failed" | "queued" | "retrying";
|
||||
export type DownloadStatus = "ready" | "staged" | "downloading" | "processing" | "seeding" | "waitingToSeed" | "paused" | "completed" | "failed" | "queued" | "retrying" | "verifying";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { TorrentWebSeed } from "./TorrentWebSeed";
|
||||
|
||||
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_seed_remaining?: number, torrent_web_seeds?: Array<TorrentWebSeed>, 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, };
|
||||
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_seed_remaining?: number, torrent_web_seeds?: Array<TorrentWebSeed>, 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, torrent_file_allocation?: string, torrent_verify_only?: boolean, torrent_verify_restore_status?: string, lifecycle_generation?: string, };
|
||||
|
||||
@@ -11,4 +11,4 @@ import type { SiteLogin } from "./SiteLogin";
|
||||
import type { Theme } from "./Theme";
|
||||
import type { WindowControlStyle } from "./WindowControlStyle";
|
||||
|
||||
export type PersistedSettings = { theme: Theme, fontFamily: FontFamily, windowControlStyle: WindowControlStyle, calendarPreference: CalendarPreference, language: string, baseDownloadFolder: string, categorySubfoldersEnabled: boolean, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array<string>, maxConcurrentDownloads: number, globalSpeedLimit: string, torrentOverallUploadLimit: string, speedLimitPresetValues: Array<number>, logsEnabled: boolean, isSidebarVisible: boolean, sidebarPosition: string, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array<string>, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, lastCustomSpeedLimitUnit: string, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, autoAddClipboardLinks: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, torrentEnableDht: boolean, torrentEnableDht6: boolean, torrentEnablePex: boolean, torrentEnableLpd: boolean, torrentMaxOpenFiles: number, torrentDhtMessageTimeout: number, torrentSeparateSeedSlots: boolean, torrentMaxConcurrentSeeds: number, torrentListenPort: string, torrentDhtListenPort: string, torrentExternalIp: string, torrentDhtEntryPoint: string, torrentDhtEntryPoint6: string, torrentDhtListenAddr6: string, torrentLpdInterface: string, torrentPeerIdPrefix: string, torrentPeerAgent: string, customUserAgent: string, askWhereToSaveEachFile: boolean, rememberLastUsedDownloadDirectory: boolean, preventsSleepWhileDownloading: boolean, preventsDisplaySleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array<SiteLogin>, autoCheckUpdates: boolean, keychainAccessGranted: boolean, };
|
||||
export type PersistedSettings = { theme: Theme, fontFamily: FontFamily, windowControlStyle: WindowControlStyle, calendarPreference: CalendarPreference, language: string, baseDownloadFolder: string, categorySubfoldersEnabled: boolean, categorySubfolders: { [key in string]: string }, categoryDirectoryOverrides: { [key in string]: string }, approvedDownloadRoots: Array<string>, maxConcurrentDownloads: number, globalSpeedLimit: string, torrentOverallUploadLimit: string, speedLimitPresetValues: Array<number>, logsEnabled: boolean, isSidebarVisible: boolean, sidebarPosition: string, activeSettingsTab: SettingsTab, scheduler: SchedulerSettings, schedulerRunning: boolean, schedulerActiveDownloadIds: Array<string>, schedulerLastStartKey: string, schedulerLastStopKey: string, lastCustomSpeedLimitKiB: number, lastCustomSpeedLimitUnit: string, perServerConnections: number, maxAutomaticRetries: number, showNotifications: boolean, playCompletionSound: boolean, autoAddClipboardLinks: boolean, appFontSize: AppFontSize, listRowDensity: ListRowDensity, showDockBadge: boolean, showMenuBarIcon: boolean, proxyMode: ProxyMode, proxyHost: string, proxyPort: number, torrentEnableDht: boolean, torrentEnableDht6: boolean, torrentEnablePex: boolean, torrentEnableLpd: boolean, torrentMaxOpenFiles: number, torrentDhtMessageTimeout: number, torrentSeparateSeedSlots: boolean, torrentMaxConcurrentSeeds: number, torrentIpv6Enabled: boolean, torrentListenPort: string, torrentDhtListenPort: string, torrentExternalIp: string, torrentDhtEntryPoint: string, torrentDhtEntryPoint6: string, torrentDhtListenAddr6: string, torrentLpdInterface: string, torrentPeerIdPrefix: string, torrentPeerAgent: string, torrentBindAddress: string, aria2DiskCache: string, customUserAgent: string, askWhereToSaveEachFile: boolean, rememberLastUsedDownloadDirectory: boolean, preventsSleepWhileDownloading: boolean, preventsDisplaySleepWhileDownloading: boolean, mediaCookieSource: MediaCookieSource, siteLogins: Array<SiteLogin>, autoCheckUpdates: boolean, keychainAccessGranted: boolean, };
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type TorrentDetails = { infoHash: string, displayName: string, totalBytes: number, fileCount: number, pieceLength: number, pieceCount: number, private: boolean, creationDate: string | null, creator: string | null, comment: string | null, trackers: Array<string>, webSeeds: Array<string>, };
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type TorrentFileSelectionEntry = { index: number, relativePath: string, length: number, selected: boolean, completedLength?: number, };
|
||||
@@ -0,0 +1,4 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { TorrentFileSelectionEntry } from "./TorrentFileSelectionEntry";
|
||||
|
||||
export type TorrentFileSelectionSnapshot = { files: Array<TorrentFileSelectionEntry>, };
|
||||
@@ -178,20 +178,20 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
};
|
||||
}, [isActionVisible, updateActionPosition]);
|
||||
|
||||
const displayFraction = download.status === 'downloading' || download.status === 'seeding'
|
||||
const displayFraction = download.status === 'downloading' || download.status === 'verifying' || download.status === 'seeding'
|
||||
? liveProgress?.fraction ?? download.fraction ?? 0
|
||||
: download.fraction ?? 0;
|
||||
const displayPercent = `${(displayFraction * 100).toFixed(0)}%`;
|
||||
const displaySpeed = download.status === 'seeding'
|
||||
? liveProgress?.upload_speed ?? '-'
|
||||
: download.status === 'downloading'
|
||||
: download.status === 'downloading' || download.status === 'verifying'
|
||||
? liveProgress?.speed ?? download.speed
|
||||
: download.status === 'processing'
|
||||
? t($ => $.downloads.values.processing)
|
||||
: '-';
|
||||
const displayEta = download.status === 'seeding'
|
||||
? '-'
|
||||
: download.status === 'downloading'
|
||||
: download.status === 'downloading' || download.status === 'verifying'
|
||||
? liveProgress?.eta ?? download.eta
|
||||
: download.status === 'processing'
|
||||
? t($ => $.downloads.values.muxing)
|
||||
@@ -297,6 +297,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
download.status === 'paused' ? 'paused' :
|
||||
download.status === 'seeding' ? 'seeding' :
|
||||
download.status === 'processing' ? 'processing' :
|
||||
download.status === 'verifying' ? 'processing' :
|
||||
download.status === 'queued' || download.status === 'staged' ? 'queued' :
|
||||
download.status === 'retrying' ? 'retrying' : ''
|
||||
}`}
|
||||
@@ -319,7 +320,8 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
download.status === 'paused' ? 'download-status-paused' :
|
||||
download.status === 'seeding' ? 'download-status-seeding' :
|
||||
download.status === 'failed' ? 'download-status-failed' :
|
||||
download.status === 'processing' ? 'download-status-processing' :
|
||||
download.status === 'processing' ? 'download-status-processing' :
|
||||
download.status === 'verifying' ? 'download-status-processing' :
|
||||
download.status === 'downloading' ? 'download-status-downloading' :
|
||||
download.status === 'queued' || download.status === 'staged' ? 'download-status-queued' :
|
||||
download.status === 'retrying' ? 'download-status-retrying' : ''
|
||||
@@ -332,7 +334,7 @@ export const DownloadItem = React.memo<DownloadItemProps>(({
|
||||
{downloadStatusLabel} #{queueIndex + 1}
|
||||
</span>
|
||||
</>
|
||||
) : download.status === 'downloading' ? (
|
||||
) : download.status === 'downloading' || download.status === 'verifying' ? (
|
||||
displayPercent
|
||||
) : download.status === 'seeding' ? (
|
||||
displayPercent
|
||||
|
||||
@@ -6,6 +6,8 @@ import { useSettingsStore } from '../store/useSettingsStore';
|
||||
import type { TorrentPeerDiagnostics } from '../bindings/TorrentPeerDiagnostics';
|
||||
import type { TorrentFileProgressSnapshot } from '../bindings/TorrentFileProgressSnapshot';
|
||||
import type { TorrentPieceProgressSnapshot } from '../bindings/TorrentPieceProgressSnapshot';
|
||||
import type { TorrentFileSelectionSnapshot } from '../bindings/TorrentFileSelectionSnapshot';
|
||||
import type { TorrentDetails } from '../bindings/TorrentDetails';
|
||||
import type { TorrentWebSeed } from '../bindings/TorrentWebSeed';
|
||||
import { invokeCommand as invoke } from '../ipc';
|
||||
import { ChevronDown, ChevronRight, FolderPlus, Info, CheckCircle, AlertCircle, Play, Pause } from 'lucide-react';
|
||||
@@ -22,7 +24,7 @@ import {
|
||||
formatDownloadTotal,
|
||||
resolveDownloadSizeDisplay
|
||||
} from '../utils/downloadProgress';
|
||||
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 { isValidTorrentExcludeTrackerList, isValidTorrentTrackerList, MAX_TORRENT_STOP_TIMEOUT, MAX_TORRENT_TRACKER_INTERVAL, MAX_TORRENT_TRACKER_TIMEOUT, normalizeSpeedLimitForBackend, normalizeTorrentEncryptionPolicy, normalizeTorrentFileAllocation, normalizeTorrentPrioritizePiece, normalizeTorrentTrackerInterval, normalizeTorrentTrackerTimeout, resolveDownloadConnections, TORRENT_ENCRYPTION_POLICY_DISABLED, TORRENT_ENCRYPTION_POLICY_FORCE_ENCRYPTION, TORRENT_ENCRYPTION_POLICY_REQUIRE_CRYPTO, type TorrentEncryptionPolicy, type TorrentFileAllocation } from '../utils/downloads';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { formatDateTime, type CalendarPreference } from '../utils/dateTime';
|
||||
import { isTopmostModal, useModalFocus } from '../hooks/useModalFocus';
|
||||
@@ -43,10 +45,10 @@ const formatLastTry = (
|
||||
};
|
||||
|
||||
const isPeerDiagnosticsStatus = (status: string): boolean =>
|
||||
['downloading', 'seeding', 'retrying'].includes(status);
|
||||
['downloading', 'verifying', 'seeding', 'retrying'].includes(status);
|
||||
|
||||
const isTorrentFileProgressStatus = (status: string): boolean =>
|
||||
['downloading', 'seeding', 'waitingToSeed', 'retrying', 'paused'].includes(status);
|
||||
['downloading', 'verifying', 'seeding', 'waitingToSeed', 'retrying', 'paused'].includes(status);
|
||||
|
||||
const formatPeerSpeed = (bytesPerSecond: number): string =>
|
||||
`${formatDownloadBytes(bytesPerSecond)}/s`;
|
||||
@@ -95,6 +97,7 @@ export const PropertiesModal = () => {
|
||||
const [torrentCheckIntegrity, setTorrentCheckIntegrity] = useState(false);
|
||||
const [torrentRemoveUnselectedFile, setTorrentRemoveUnselectedFile] = useState(false);
|
||||
const [torrentEncryptionPolicy, setTorrentEncryptionPolicy] = useState<TorrentEncryptionPolicy>(TORRENT_ENCRYPTION_POLICY_DISABLED);
|
||||
const [torrentFileAllocation, setTorrentFileAllocation] = useState<TorrentFileAllocation>('prealloc');
|
||||
const [torrentTrackers, setTorrentTrackers] = useState('');
|
||||
const [torrentExcludeTrackers, setTorrentExcludeTrackers] = useState('');
|
||||
const [torrentTrackerConnectTimeout, setTorrentTrackerConnectTimeout] = useState('');
|
||||
@@ -106,6 +109,11 @@ export const PropertiesModal = () => {
|
||||
const [torrentPeerDiagnosticsError, setTorrentPeerDiagnosticsError] = useState(false);
|
||||
const [isTorrentPeerDiagnosticsPending, setIsTorrentPeerDiagnosticsPending] = useState(false);
|
||||
const [torrentFileProgress, setTorrentFileProgress] = useState<TorrentFileProgressSnapshot | null>(null);
|
||||
const [torrentFileSelection, setTorrentFileSelection] = useState<TorrentFileSelectionSnapshot | null>(null);
|
||||
const [torrentDetails, setTorrentDetails] = useState<TorrentDetails | null>(null);
|
||||
const [torrentDetailsError, setTorrentDetailsError] = useState(false);
|
||||
const [isTorrentDetailsPending, setIsTorrentDetailsPending] = useState(false);
|
||||
const [isTorrentVerifyPending, setIsTorrentVerifyPending] = useState(false);
|
||||
const [torrentFileProgressError, setTorrentFileProgressError] = useState(false);
|
||||
const [isTorrentFileProgressPending, setIsTorrentFileProgressPending] = useState(false);
|
||||
const [torrentPieceProgress, setTorrentPieceProgress] = useState<TorrentPieceProgressSnapshot | null>(null);
|
||||
@@ -132,9 +140,14 @@ export const PropertiesModal = () => {
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [isPauseResumePending, setIsPauseResumePending] = useState(false);
|
||||
const torrentFileProgressByIndex = new Map(
|
||||
(torrentFileProgress?.files ?? []).map(file => [file.index, file])
|
||||
);
|
||||
const actionRequestRef = useRef(0);
|
||||
const peerDiagnosticsRequestRef = useRef(0);
|
||||
const torrentFileProgressRequestRef = useRef(0);
|
||||
const torrentFileSelectionRequestRef = useRef(0);
|
||||
const torrentDetailsRequestRef = useRef(0);
|
||||
const torrentPieceProgressRequestRef = useRef(0);
|
||||
const torrentWebSeedsRequestRef = useRef(0);
|
||||
const modalRef = useModalFocus(Boolean(selectedPropertiesDownloadId && item));
|
||||
@@ -154,6 +167,10 @@ export const PropertiesModal = () => {
|
||||
setTorrentFileProgress(null);
|
||||
setTorrentFileProgressError(false);
|
||||
setIsTorrentFileProgressPending(false);
|
||||
torrentDetailsRequestRef.current += 1;
|
||||
setTorrentDetails(null);
|
||||
setTorrentDetailsError(false);
|
||||
setIsTorrentDetailsPending(false);
|
||||
torrentPieceProgressRequestRef.current += 1;
|
||||
setTorrentPieceProgress(null);
|
||||
setTorrentPieceProgressError(false);
|
||||
@@ -227,6 +244,7 @@ export const PropertiesModal = () => {
|
||||
setTorrentCheckIntegrity(activeItem.torrentCheckIntegrity === true);
|
||||
setTorrentRemoveUnselectedFile(activeItem.torrentRemoveUnselectedFile === true);
|
||||
setTorrentEncryptionPolicy(normalizeTorrentEncryptionPolicy(activeItem.torrentEncryptionPolicy) || TORRENT_ENCRYPTION_POLICY_DISABLED);
|
||||
setTorrentFileAllocation(normalizeTorrentFileAllocation(activeItem.torrentFileAllocation) || 'prealloc');
|
||||
setTorrentTrackers(activeItem.torrentTrackers || '');
|
||||
setTorrentExcludeTrackers(activeItem.torrentExcludeTrackers || '');
|
||||
setTorrentTrackerConnectTimeout(activeItem.torrentTrackerConnectTimeout === undefined ? '' : String(activeItem.torrentTrackerConnectTimeout));
|
||||
@@ -244,6 +262,58 @@ export const PropertiesModal = () => {
|
||||
}
|
||||
}, [selectedPropertiesDownloadId, setSelectedPropertiesDownloadId]);
|
||||
|
||||
useEffect(() => {
|
||||
torrentFileSelectionRequestRef.current += 1;
|
||||
setTorrentFileSelection(null);
|
||||
if (!selectedPropertiesDownloadId || !item?.isTorrent) return;
|
||||
const requestId = torrentFileSelectionRequestRef.current;
|
||||
const propertiesDownloadId = item.id;
|
||||
void invoke('get_torrent_file_selection', { id: propertiesDownloadId })
|
||||
.then(snapshot => {
|
||||
if (
|
||||
requestId === torrentFileSelectionRequestRef.current
|
||||
&& useDownloadStore.getState().selectedPropertiesDownloadId === propertiesDownloadId
|
||||
) {
|
||||
setTorrentFileSelection(snapshot);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (requestId === torrentFileSelectionRequestRef.current) setTorrentFileSelection(null);
|
||||
});
|
||||
}, [item?.id, item?.isTorrent, item?.torrentPath, selectedPropertiesDownloadId]);
|
||||
|
||||
useEffect(() => {
|
||||
torrentDetailsRequestRef.current += 1;
|
||||
setTorrentDetails(null);
|
||||
setTorrentDetailsError(false);
|
||||
setIsTorrentDetailsPending(false);
|
||||
if (!selectedPropertiesDownloadId || !item?.isTorrent || !item.torrentPath) return;
|
||||
|
||||
const requestId = torrentDetailsRequestRef.current;
|
||||
const propertiesDownloadId = item.id;
|
||||
setIsTorrentDetailsPending(true);
|
||||
void invoke('get_torrent_details', { id: propertiesDownloadId })
|
||||
.then(details => {
|
||||
if (
|
||||
requestId === torrentDetailsRequestRef.current
|
||||
&& useDownloadStore.getState().selectedPropertiesDownloadId === propertiesDownloadId
|
||||
) {
|
||||
setTorrentDetails(details);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (
|
||||
requestId === torrentDetailsRequestRef.current
|
||||
&& useDownloadStore.getState().selectedPropertiesDownloadId === propertiesDownloadId
|
||||
) {
|
||||
setTorrentDetailsError(true);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (requestId === torrentDetailsRequestRef.current) setIsTorrentDetailsPending(false);
|
||||
});
|
||||
}, [item?.id, item?.isTorrent, item?.torrentPath, selectedPropertiesDownloadId]);
|
||||
|
||||
useEffect(() => {
|
||||
const activeLimit = item?.speedLimit?.trim();
|
||||
setLiveSpeedLimitValue(activeLimit && activeLimit !== '0' ? activeLimit : '');
|
||||
@@ -569,6 +639,32 @@ export const PropertiesModal = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerifyTorrentData = async () => {
|
||||
if (!item?.isTorrent || isTorrentVerifyPending) return;
|
||||
const restoreStatus = item.status;
|
||||
const previousVerifyOnly = item.torrentVerifyOnly;
|
||||
const previousRestoreStatus = item.torrentVerifyRestoreStatus;
|
||||
// Mark the maintenance lifecycle before invoking the command so a very
|
||||
// fast queued/completed event cannot be mistaken for the normal download
|
||||
// lifecycle. The backend persists the same markers before dispatching.
|
||||
useDownloadStore.getState().updateDownload(item.id, {
|
||||
torrentVerifyOnly: true,
|
||||
torrentVerifyRestoreStatus: restoreStatus
|
||||
});
|
||||
setIsTorrentVerifyPending(true);
|
||||
try {
|
||||
await invoke('verify_torrent_data', { id: item.id });
|
||||
} catch (error) {
|
||||
useDownloadStore.getState().updateDownload(item.id, {
|
||||
torrentVerifyOnly: previousVerifyOnly,
|
||||
torrentVerifyRestoreStatus: previousRestoreStatus
|
||||
});
|
||||
setErrorMessage(error instanceof Error ? error.message : String(error));
|
||||
} finally {
|
||||
setIsTorrentVerifyPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!url.trim()) {
|
||||
setErrorMessage(t($ => $.properties.enterValidUrl));
|
||||
@@ -638,6 +734,21 @@ export const PropertiesModal = () => {
|
||||
setErrorMessage(t($ => $.properties.torrentEncryptionPolicyInvalid));
|
||||
return;
|
||||
}
|
||||
const selectedTorrentIndices = torrentFileSelection
|
||||
? torrentFileSelection.files.filter(file => file.selected).map(file => file.index)
|
||||
: [];
|
||||
const allTorrentFilesSelected = Boolean(
|
||||
torrentFileSelection
|
||||
&& selectedTorrentIndices?.length === torrentFileSelection.files.length
|
||||
);
|
||||
if (torrentFileSelection && selectedTorrentIndices?.length === 0) {
|
||||
setErrorMessage(t($ => $.properties.torrentFileSelectionRequired));
|
||||
return;
|
||||
}
|
||||
if (item.isTorrent && torrentRemoveUnselectedFile && torrentFileSelection && allTorrentFilesSelected) {
|
||||
setErrorMessage(t($ => $.properties.torrentRemoveUnselectedFileSelectionRequired));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
item.isTorrent
|
||||
&& torrentRemoveUnselectedFile
|
||||
@@ -676,12 +787,18 @@ export const PropertiesModal = () => {
|
||||
: undefined,
|
||||
torrentStopTimeout: normalizedStopTimeout,
|
||||
torrentPrioritizePiece: normalizeTorrentPrioritizePiece(torrentPrioritizePiece) || undefined,
|
||||
torrentRemoveUnselectedFile: item.torrentFileIndices !== undefined
|
||||
torrentFileIndices: torrentFileSelection
|
||||
? (allTorrentFilesSelected ? undefined : selectedTorrentIndices)
|
||||
: item.torrentFileIndices,
|
||||
torrentRemoveUnselectedFile: (torrentFileSelection
|
||||
? !allTorrentFilesSelected
|
||||
: item.torrentFileIndices !== undefined)
|
||||
? torrentRemoveUnselectedFile
|
||||
: undefined,
|
||||
torrentEncryptionPolicy: torrentEncryptionPolicy !== TORRENT_ENCRYPTION_POLICY_DISABLED
|
||||
? torrentEncryptionPolicy
|
||||
: undefined,
|
||||
torrentFileAllocation,
|
||||
}
|
||||
: {}),
|
||||
...(connectionsDirty
|
||||
@@ -839,6 +956,9 @@ export const PropertiesModal = () => {
|
||||
const liveTorrentUploadLimitAvailable = item.isTorrent && ['downloading', 'seeding', 'retrying'].includes(item.status);
|
||||
const liveTorrentPeerOptionsAvailable = item.isTorrent && ['downloading', 'seeding', 'retrying'].includes(item.status);
|
||||
const torrentPeerDiagnosticsAvailable = item.isTorrent && isPeerDiagnosticsStatus(item.status);
|
||||
const torrentFileSelectionIsEmpty = item.isTorrent
|
||||
&& torrentFileSelection !== null
|
||||
&& !torrentFileSelection.files.some(file => file.selected);
|
||||
const configuredConnections = resolveDownloadConnections(item.connections, perServerConnections);
|
||||
const observedConnectionTotal = Math.max(
|
||||
1,
|
||||
@@ -919,7 +1039,7 @@ export const PropertiesModal = () => {
|
||||
let statusColor = 'text-text-secondary';
|
||||
let StatusIcon = Info;
|
||||
if (item.status === 'completed') { statusColor = 'text-green-500'; StatusIcon = CheckCircle; }
|
||||
else if (item.status === 'downloading' || item.status === 'seeding' || item.status === 'retrying') { statusColor = 'text-blue-500'; StatusIcon = Play; }
|
||||
else if (item.status === 'downloading' || item.status === 'verifying' || item.status === 'seeding' || item.status === 'retrying') { statusColor = 'text-blue-500'; StatusIcon = Play; }
|
||||
else if (item.status === 'processing') { statusColor = 'text-sky-500'; StatusIcon = Play; }
|
||||
else if (item.status === 'paused') { statusColor = 'text-orange-500'; StatusIcon = Pause; }
|
||||
else if (item.status === 'failed') { statusColor = 'text-red-500'; StatusIcon = AlertCircle; }
|
||||
@@ -1105,6 +1225,72 @@ export const PropertiesModal = () => {
|
||||
<div className="col-start-2 text-[11px] text-text-muted">
|
||||
{t($ => $.properties.torrentPeerOptionsSavedHint)}
|
||||
</div>
|
||||
{torrentFileSelection && (
|
||||
<div className="col-start-2 rounded-lg border border-border-modal bg-bg-input/30 p-3 space-y-2">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="text-xs font-semibold text-text-primary">
|
||||
{t($ => $.properties.torrentFileSelection)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="app-button px-2.5 text-[11px] disabled:opacity-50"
|
||||
disabled={transferLocked || torrentFileSelection.files.length === 0}
|
||||
onClick={() => setTorrentFileSelection(current => current
|
||||
? { ...current, files: current.files.map(file => ({ ...file, selected: true })) }
|
||||
: current)}
|
||||
>
|
||||
{t($ => $.properties.torrentFileSelectionAll)}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="app-button px-2.5 text-[11px] disabled:opacity-50"
|
||||
disabled={transferLocked || torrentFileSelection.files.length === 0}
|
||||
onClick={() => setTorrentFileSelection(current => current
|
||||
? { ...current, files: current.files.map(file => ({ ...file, selected: false })) }
|
||||
: current)}
|
||||
>
|
||||
{t($ => $.properties.torrentFileSelectionClear)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[11px] text-text-muted">
|
||||
{t($ => $.properties.torrentFileSelectionHint)}
|
||||
</p>
|
||||
<div className="max-h-48 overflow-auto rounded border border-border-modal/60">
|
||||
{torrentFileSelection.files.map(file => (
|
||||
<label key={file.index} className="flex items-center gap-2 border-b border-border-modal/40 px-2 py-1.5 text-[11px] last:border-b-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={file.selected}
|
||||
disabled={transferLocked}
|
||||
onChange={event => setTorrentFileSelection(current => current
|
||||
? {
|
||||
...current,
|
||||
files: current.files.map(candidate => candidate.index === file.index
|
||||
? { ...candidate, selected: event.currentTarget.checked }
|
||||
: candidate)
|
||||
}
|
||||
: current)}
|
||||
className="accent-accent disabled:opacity-50"
|
||||
/>
|
||||
<span className="font-mono text-text-muted">{file.index}</span>
|
||||
<span className="min-w-0 flex-1 truncate" dir="auto" title={file.relativePath}>{file.relativePath}</span>
|
||||
<span className="text-end text-text-muted whitespace-nowrap">
|
||||
{(() => {
|
||||
const progress = torrentFileProgressByIndex.get(file.index);
|
||||
const completedLength = progress?.completedLength ?? file.completedLength ?? 0;
|
||||
const percentage = file.length === 0
|
||||
? 100
|
||||
: Math.round((completedLength / file.length) * 100);
|
||||
return `${formatDownloadBytes(completedLength)} / ${formatDownloadBytes(file.length)} (${percentage}%)`;
|
||||
})()}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="col-start-2 rounded-lg border border-border-modal bg-bg-input/30 p-3 space-y-2">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="text-xs font-semibold text-text-primary">
|
||||
@@ -1442,6 +1628,25 @@ export const PropertiesModal = () => {
|
||||
{t($ => $.properties.torrentPrioritizePieceHint)}
|
||||
</p>
|
||||
</div>
|
||||
<label className="text-xs text-text-muted text-right" htmlFor="torrent-file-allocation-properties">
|
||||
{t($ => $.properties.torrentFileAllocation)}
|
||||
</label>
|
||||
<div>
|
||||
<select
|
||||
id="torrent-file-allocation-properties"
|
||||
value={torrentFileAllocation}
|
||||
onChange={event => setTorrentFileAllocation(event.currentTarget.value as TorrentFileAllocation)}
|
||||
disabled={transferLocked}
|
||||
aria-describedby="torrent-file-allocation-properties-hint"
|
||||
className="app-control max-w-56 px-2.5 py-1.5 text-xs disabled:opacity-50"
|
||||
>
|
||||
<option value="prealloc">{t($ => $.properties.torrentFileAllocationPrealloc)}</option>
|
||||
<option value="none">{t($ => $.properties.torrentFileAllocationNone)}</option>
|
||||
</select>
|
||||
<p id="torrent-file-allocation-properties-hint" className="mt-1 text-[11px] text-text-muted">
|
||||
{t($ => $.properties.torrentFileAllocationHint)}
|
||||
</p>
|
||||
</div>
|
||||
<label className="text-xs text-text-muted text-right" htmlFor="torrent-encryption-policy-properties">
|
||||
{t($ => $.properties.torrentEncryptionPolicy)}
|
||||
</label>
|
||||
@@ -1485,6 +1690,18 @@ export const PropertiesModal = () => {
|
||||
{t($ => $.properties.torrentVerifyIntegrityHint)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="col-start-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleVerifyTorrentData()}
|
||||
disabled={isTorrentVerifyPending || getTransferLocked(item.status) || !['completed', 'paused', 'failed'].includes(item.status)}
|
||||
className="app-button px-3 text-xs disabled:opacity-50"
|
||||
>
|
||||
{isTorrentVerifyPending
|
||||
? t($ => $.properties.torrentVerifyNowLoading)
|
||||
: t($ => $.properties.torrentVerifyNow)}
|
||||
</button>
|
||||
</div>
|
||||
<label className="text-xs text-text-muted text-right" htmlFor="torrent-remove-unselected-file">
|
||||
{t($ => $.properties.torrentRemoveUnselectedFile)}
|
||||
</label>
|
||||
@@ -1682,6 +1899,73 @@ export const PropertiesModal = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{item.isTorrent && (
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-text-primary mb-4 pb-1 border-b border-border-modal/50">
|
||||
{t($ => $.properties.torrentDetails)}
|
||||
</h3>
|
||||
{isTorrentDetailsPending && (
|
||||
<p className="text-xs text-text-muted">{t($ => $.properties.torrentDetailsLoading)}</p>
|
||||
)}
|
||||
{torrentDetailsError && (
|
||||
<p className="text-xs text-red-400">{t($ => $.properties.torrentDetailsUnavailable)}</p>
|
||||
)}
|
||||
{torrentDetails && (
|
||||
<div className="grid grid-cols-[100px_1fr] gap-x-4 gap-y-2 rounded-lg border border-border-modal bg-bg-input/30 p-3 text-xs">
|
||||
<span className="text-text-muted text-right">{t($ => $.properties.torrentDetailsDisplayName)}</span>
|
||||
<span className="text-text-primary break-words" dir="auto">{torrentDetails.displayName}</span>
|
||||
<span className="text-text-muted text-right">{t($ => $.properties.torrentDetailsInfoHash)}</span>
|
||||
<span className="font-mono text-text-primary break-all">{torrentDetails.infoHash}</span>
|
||||
<span className="text-text-muted text-right">{t($ => $.properties.torrentDetailsSize)}</span>
|
||||
<span className="text-text-primary">{formatDownloadBytes(torrentDetails.totalBytes)}</span>
|
||||
<span className="text-text-muted text-right">{t($ => $.properties.torrentDetailsFiles)}</span>
|
||||
<span className="text-text-primary">{torrentDetails.fileCount}</span>
|
||||
<span className="text-text-muted text-right">{t($ => $.properties.torrentDetailsPieces)}</span>
|
||||
<span className="text-text-primary">{torrentDetails.pieceCount} × {formatDownloadBytes(torrentDetails.pieceLength)}</span>
|
||||
<span className="text-text-muted text-right">{t($ => $.properties.torrentDetailsPrivate)}</span>
|
||||
<span className="text-text-primary">{torrentDetails.private
|
||||
? t($ => $.properties.torrentDetailsPrivateYes)
|
||||
: t($ => $.properties.torrentDetailsPrivateNo)}</span>
|
||||
{torrentDetails.creationDate && (
|
||||
<>
|
||||
<span className="text-text-muted text-right">{t($ => $.properties.torrentDetailsCreated)}</span>
|
||||
<span className="text-text-primary">{formatDateTime(torrentDetails.creationDate, {
|
||||
locale: i18n.language,
|
||||
calendar: calendarPreference,
|
||||
options: { dateStyle: 'medium', timeStyle: 'short' }
|
||||
})}</span>
|
||||
</>
|
||||
)}
|
||||
{torrentDetails.creator && (
|
||||
<>
|
||||
<span className="text-text-muted text-right">{t($ => $.properties.torrentDetailsCreator)}</span>
|
||||
<span className="text-text-primary break-words" dir="auto">{torrentDetails.creator}</span>
|
||||
</>
|
||||
)}
|
||||
{torrentDetails.comment && (
|
||||
<>
|
||||
<span className="text-text-muted text-right">{t($ => $.properties.torrentDetailsComment)}</span>
|
||||
<span className="whitespace-pre-wrap break-words text-text-primary" dir="auto">{torrentDetails.comment}</span>
|
||||
</>
|
||||
)}
|
||||
<span className="text-text-muted text-right">{t($ => $.properties.torrentDetailsTrackers)}</span>
|
||||
<span className="text-text-primary break-words" dir="auto">
|
||||
{torrentDetails.trackers.length > 0 ? torrentDetails.trackers.join(', ') : '—'}
|
||||
</span>
|
||||
<span className="text-text-muted text-right">{t($ => $.properties.torrentDetailsWebSeeds)}</span>
|
||||
<span className="text-text-primary break-words" dir="auto">
|
||||
{torrentDetails.webSeeds.length > 0 ? torrentDetails.webSeeds.join(', ') : '—'}
|
||||
</span>
|
||||
{torrentDetails.private && (
|
||||
<p className="col-span-2 text-[11px] text-text-muted">
|
||||
{t($ => $.properties.torrentDetailsPrivateHint)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{item.isTorrent && (
|
||||
<section>
|
||||
<h3 className="text-sm font-semibold text-text-primary mb-4 pb-1 border-b border-border-modal/50">
|
||||
@@ -1794,8 +2078,8 @@ export const PropertiesModal = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={transferLocked}
|
||||
className={`app-button app-button-primary px-4 text-xs ${transferLocked ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
disabled={transferLocked || torrentFileSelectionIsEmpty}
|
||||
className={`app-button app-button-primary px-4 text-xs ${transferLocked || torrentFileSelectionIsEmpty ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
<CheckCircle size={14} />
|
||||
{t($ => $.properties.save)}
|
||||
|
||||
@@ -1280,6 +1280,19 @@ runEngineChecks(false);
|
||||
type="checkbox"
|
||||
checked={settings.torrentEnableDht6}
|
||||
onChange={(event) => settings.setTorrentEnableDht6(event.target.checked)}
|
||||
disabled={!settings.torrentIpv6Enabled}
|
||||
className="mac-switch disabled:opacity-50"
|
||||
/>
|
||||
</label>
|
||||
<label className="mac-settings-row cursor-default">
|
||||
<div className="settings-row-label">
|
||||
<span>{t($ => $.settings.network.torrentIpv6Enabled)}</span>
|
||||
<small>{t($ => $.settings.network.torrentIpv6EnabledDescription)}</small>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.torrentIpv6Enabled}
|
||||
onChange={(event) => settings.setTorrentIpv6Enabled(event.target.checked)}
|
||||
className="mac-switch"
|
||||
/>
|
||||
</label>
|
||||
@@ -1328,6 +1341,20 @@ runEngineChecks(false);
|
||||
aria-label={t($ => $.settings.network.torrentListenPort)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mac-settings-row settings-network-row">
|
||||
<div className="settings-row-label">
|
||||
<span>{t($ => $.settings.network.torrentBindAddress)}</span>
|
||||
<small>{t($ => $.settings.network.torrentBindAddressDescription)}</small>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.torrentBindAddress}
|
||||
onChange={(event) => settings.setTorrentBindAddress(event.target.value)}
|
||||
placeholder="192.0.2.10 or 2001:db8::10"
|
||||
className="app-control settings-network-input"
|
||||
aria-label={t($ => $.settings.network.torrentBindAddress)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mac-settings-row settings-network-row">
|
||||
<div className="settings-row-label">
|
||||
<span>{t($ => $.settings.network.torrentDhtListenPort)}</span>
|
||||
@@ -1519,6 +1546,20 @@ runEngineChecks(false);
|
||||
aria-label={t($ => $.settings.network.torrentMaxOpenFiles)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mac-settings-row settings-network-row">
|
||||
<div className="settings-row-label">
|
||||
<span>{t($ => $.settings.network.aria2DiskCache)}</span>
|
||||
<small>{t($ => $.settings.network.aria2DiskCacheDescription)}</small>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.aria2DiskCache}
|
||||
onChange={(event) => settings.setAria2DiskCache(event.target.value)}
|
||||
placeholder="16M"
|
||||
className="app-control settings-network-input text-center"
|
||||
aria-label={t($ => $.settings.network.aria2DiskCache)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mac-settings-row settings-network-row">
|
||||
<div className="settings-row-label">
|
||||
<span>{t($ => $.settings.network.torrentOverallUploadLimit)}</span>
|
||||
|
||||
@@ -89,6 +89,7 @@ const common = {
|
||||
queued: 'Queued',
|
||||
downloading: 'Downloading',
|
||||
processing: 'Processing',
|
||||
verifying: 'Verifying',
|
||||
seeding: 'Seeding',
|
||||
waitingToSeed: 'Waiting to seed',
|
||||
paused: 'Paused',
|
||||
@@ -251,6 +252,8 @@ const common = {
|
||||
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.',
|
||||
torrentVerifyNow: 'Verify now',
|
||||
torrentVerifyNowLoading: 'Verifying…',
|
||||
torrentMaxPeers: 'Maximum Torrent peers',
|
||||
torrentPeerSpeedLimit: 'Peer speed threshold',
|
||||
torrentMaxPeersInvalid: 'Torrent maximum peers must be an integer from 0 to 1000',
|
||||
@@ -262,6 +265,11 @@ const common = {
|
||||
torrentPeerDiagnosticsFailed: 'Could not read Torrent peer diagnostics.',
|
||||
torrentPeerDiagnosticsHint: 'Speeds and connection flags only are shown; peer IPs, ports, IDs, and bitfields are not retained.',
|
||||
torrentFileProgress: 'Torrent file progress',
|
||||
torrentFileSelection: 'Torrent file selection',
|
||||
torrentFileSelectionHint: 'Choose which files to download. Selecting every file removes the filter; at least one file must remain selected.',
|
||||
torrentFileSelectionRequired: 'Select at least one Torrent file.',
|
||||
torrentFileSelectionAll: 'Select all',
|
||||
torrentFileSelectionClear: 'Clear',
|
||||
torrentFileProgressRefresh: 'Refresh',
|
||||
torrentFileProgressLoading: 'Loading file progress…',
|
||||
torrentFileProgressUnavailable: 'File progress is available while this Torrent is active or paused.',
|
||||
@@ -300,6 +308,27 @@ const common = {
|
||||
torrentPrioritizePieceHint: 'Optional Aria2 preview policy: head, tail, or both; each may use a size such as 1M. Changes apply when the Torrent starts or retries.',
|
||||
torrentPrioritizePieceInvalid: 'Torrent piece priority must use head and/or tail with optional K or M sizes between 1K and 1024M',
|
||||
torrentEncryptionPolicy: 'Torrent encryption policy',
|
||||
torrentFileAllocation: 'Torrent file allocation',
|
||||
torrentFileAllocationPrealloc: 'Preallocate files',
|
||||
torrentFileAllocationNone: 'Allocate as needed',
|
||||
torrentFileAllocationHint: 'Preallocation reserves the selected files before transfer. Allocation as needed avoids that upfront disk reservation.',
|
||||
torrentDetails: 'Torrent details',
|
||||
torrentDetailsLoading: 'Loading Torrent details…',
|
||||
torrentDetailsUnavailable: 'Torrent details are not available.',
|
||||
torrentDetailsDisplayName: 'Display name',
|
||||
torrentDetailsInfoHash: 'Info hash',
|
||||
torrentDetailsSize: 'Total size',
|
||||
torrentDetailsFiles: 'Files',
|
||||
torrentDetailsPieces: 'Pieces',
|
||||
torrentDetailsPrivate: 'Private',
|
||||
torrentDetailsPrivateYes: 'Yes',
|
||||
torrentDetailsPrivateNo: 'No',
|
||||
torrentDetailsCreated: 'Created',
|
||||
torrentDetailsCreator: 'Creator',
|
||||
torrentDetailsComment: 'Comment',
|
||||
torrentDetailsTrackers: 'Trackers',
|
||||
torrentDetailsWebSeeds: 'Embedded web seeds',
|
||||
torrentDetailsPrivateHint: 'This private Torrent disables DHT, DHT6, PEX, and LPD discovery regardless of broader settings.',
|
||||
torrentEncryptionPolicyHint: 'Applied when this Torrent starts or retries. Choose one policy so the handshake and payload encryption settings stay consistent.',
|
||||
torrentEncryptionDisabled: 'Disabled',
|
||||
torrentEncryptionRequireCrypto: 'Require obfuscated handshake',
|
||||
@@ -819,6 +848,8 @@ const common = {
|
||||
torrentDhtDescription: 'Find peers without relying only on trackers. Disabling this also disables UDP tracker support.',
|
||||
torrentDht6: 'IPv6 DHT',
|
||||
torrentDht6Description: 'Use IPv6 for distributed peer discovery when the network provides a usable IPv6 path.',
|
||||
torrentIpv6Enabled: 'Enable IPv6 for Torrent networking',
|
||||
torrentIpv6EnabledDescription: 'Keep IPv6 available to BitTorrent, DHT, and peer discovery. Disabling this overrides IPv6 DHT even when its preference remains enabled.',
|
||||
torrentPex: 'Peer Exchange (PEX)',
|
||||
torrentPexDescription: 'Allow connected peers to share additional peer addresses.',
|
||||
torrentLpd: 'Local Peer Discovery (LPD)',
|
||||
@@ -834,6 +865,8 @@ const common = {
|
||||
torrentMaxConcurrentSeedsDescription: 'Maximum number of Torrents Firelink lets seed at once when separate capacity is enabled.',
|
||||
torrentListenPort: 'TCP peer ports',
|
||||
torrentListenPortDescription: 'TCP ports for incoming BitTorrent peer connections. Leave blank for Aria2’s default range.',
|
||||
torrentBindAddress: 'Torrent bind address',
|
||||
torrentBindAddressDescription: 'Optional local IPv4 or IPv6 address for Aria2 sockets. Invalid addresses are rejected; changes apply after restart.',
|
||||
torrentDhtListenPort: 'UDP/DHT ports',
|
||||
torrentDhtListenPortDescription: 'UDP ports for DHT and UDP trackers. Leave blank for Aria2’s default range.',
|
||||
torrentExternalIp: 'External IP address',
|
||||
@@ -855,6 +888,8 @@ const common = {
|
||||
torrentResourceLimits: 'BitTorrent resource limits',
|
||||
torrentMaxOpenFiles: 'Maximum open Torrent files',
|
||||
torrentMaxOpenFilesDescription: 'Global Aria2 limit for files open at once in multi-file Torrents. Lower values reduce file-descriptor use; the default is 100. Changes apply to new Torrents without restarting Aria2, and this does not raise your operating system limit.',
|
||||
aria2DiskCache: 'Aria2 disk cache',
|
||||
aria2DiskCacheDescription: 'Cache size for Aria2, using 0 or a positive value such as 16M. Accepts K/M values up to 1024M and applies after restart.',
|
||||
torrentMaxOpenFilesUpdateFailed: 'Could not apply the Torrent open-file limit: {{detail}}',
|
||||
torrentOverallUploadLimit: 'Overall Aria2 upload limit',
|
||||
torrentOverallUploadLimitDescription: 'Caps combined Aria2 upload traffic, primarily active Torrent seeding in Firelink. Leave blank for unlimited; the value is applied live and restored when Firelink restarts.',
|
||||
|
||||
@@ -89,6 +89,7 @@ const fa = {
|
||||
queued: 'در صف',
|
||||
downloading: 'در حال دانلود',
|
||||
processing: 'در حال پردازش',
|
||||
verifying: 'در حال بررسی صحت',
|
||||
seeding: 'در حال اشتراکگذاری',
|
||||
waitingToSeed: 'در انتظار اشتراکگذاری',
|
||||
paused: 'متوقفشده',
|
||||
@@ -251,6 +252,8 @@ const fa = {
|
||||
torrentTrackerIntervalInvalid: 'فاصله Tracker باید عددی صحیح بین ۰ تا ۶۰۴۸۰۰ ثانیه باشد',
|
||||
torrentVerifyIntegrity: 'بررسی صحت تورنت',
|
||||
torrentVerifyIntegrityHint: 'هنگام شروع یا تلاش مجدد این تورنت اعمال میشود. ممکن است قطعهها دوباره بررسی و دادههای خراب دوباره دانلود شوند؛ در انتقال فعال قابل تغییر نیست.',
|
||||
torrentVerifyNow: 'بررسی صحت اکنون',
|
||||
torrentVerifyNowLoading: 'در حال بررسی صحت…',
|
||||
torrentMaxPeers: 'حداکثر همتاهای تورنت',
|
||||
torrentPeerSpeedLimit: 'آستانه سرعت همتا',
|
||||
torrentMaxPeersInvalid: 'حداکثر همتاهای تورنت باید عددی صحیح بین ۰ و ۱۰۰۰ باشد',
|
||||
@@ -262,6 +265,11 @@ const fa = {
|
||||
torrentPeerDiagnosticsFailed: 'خواندن اطلاعات همتاهای تورنت ممکن نیست.',
|
||||
torrentPeerDiagnosticsHint: 'فقط سرعت و وضعیت اتصال نمایش داده میشود؛ IP، پورت، شناسه و بیتفیلد همتاها ذخیره نمیشود.',
|
||||
torrentFileProgress: 'پیشرفت فایلهای تورنت',
|
||||
torrentFileSelection: 'انتخاب فایلهای تورنت',
|
||||
torrentFileSelectionHint: 'فایلهای موردنظر برای دانلود را انتخاب کنید. انتخاب همهٔ فایلها فیلتر را حذف میکند؛ حداقل یک فایل باید انتخاب شود.',
|
||||
torrentFileSelectionRequired: 'حداقل یک فایل تورنت را انتخاب کنید.',
|
||||
torrentFileSelectionAll: 'انتخاب همه',
|
||||
torrentFileSelectionClear: 'پاککردن',
|
||||
torrentFileProgressRefresh: 'تازهسازی',
|
||||
torrentFileProgressLoading: 'در حال دریافت پیشرفت فایلها…',
|
||||
torrentFileProgressUnavailable: 'پیشرفت فایل هنگام فعال یا متوقفبودن تورنت در دسترس است.',
|
||||
@@ -300,6 +308,27 @@ const fa = {
|
||||
torrentPrioritizePieceHint: 'سیاست اختیاری پیشنمایش آریا۲: ابتدا، انتها یا هر دو؛ برای هرکدام میتوان اندازهای مثل 1M نوشت. تغییرات هنگام شروع یا تلاش مجدد اعمال میشوند.',
|
||||
torrentPrioritizePieceInvalid: 'اولویت قطعههای تورنت باید شامل ابتدا یا انتها، با اندازه اختیاری بین 1K و 1024M باشد',
|
||||
torrentEncryptionPolicy: 'سیاست رمزنگاری تورنت',
|
||||
torrentFileAllocation: 'نحوهٔ تخصیص فایل تورنت',
|
||||
torrentFileAllocationPrealloc: 'تخصیص از پیش',
|
||||
torrentFileAllocationNone: 'تخصیص هنگام نیاز',
|
||||
torrentFileAllocationHint: 'تخصیص از پیش فضای فایلهای انتخابشده را قبل از انتقال رزرو میکند؛ تخصیص هنگام نیاز این رزرو اولیه را انجام نمیدهد.',
|
||||
torrentDetails: 'جزئیات تورنت',
|
||||
torrentDetailsLoading: 'در حال دریافت جزئیات تورنت…',
|
||||
torrentDetailsUnavailable: 'جزئیات تورنت در دسترس نیست.',
|
||||
torrentDetailsDisplayName: 'نام نمایشی',
|
||||
torrentDetailsInfoHash: 'هش اطلاعات',
|
||||
torrentDetailsSize: 'حجم کل',
|
||||
torrentDetailsFiles: 'فایلها',
|
||||
torrentDetailsPieces: 'قطعهها',
|
||||
torrentDetailsPrivate: 'خصوصی',
|
||||
torrentDetailsPrivateYes: 'بله',
|
||||
torrentDetailsPrivateNo: 'خیر',
|
||||
torrentDetailsCreated: 'ایجادشده',
|
||||
torrentDetailsCreator: 'سازنده',
|
||||
torrentDetailsComment: 'توضیح',
|
||||
torrentDetailsTrackers: 'ترکرها',
|
||||
torrentDetailsWebSeeds: 'وبسیدهای داخلی',
|
||||
torrentDetailsPrivateHint: 'این تورنت خصوصی، مستقل از تنظیمات کلی، کشف DHT، DHT6، PEX و LPD را غیرفعال میکند.',
|
||||
torrentEncryptionPolicyHint: 'هنگام شروع یا تلاش مجدد اعمال میشود. یک سیاست واحد انتخاب کنید تا تنظیمات handshake و رمزنگاری payload آریا۲ سازگار بمانند.',
|
||||
torrentEncryptionDisabled: 'غیرفعال',
|
||||
torrentEncryptionRequireCrypto: 'الزام handshake مبهمسازیشده',
|
||||
@@ -819,6 +848,8 @@ const fa = {
|
||||
torrentDhtDescription: 'همتاها را بدون تکیه صرف بر ترکرها پیدا میکند. خاموش کردن آن پشتیبانی از ترکرهای UDP را هم خاموش میکند.',
|
||||
torrentDht6: 'DHT نسخه IPv6',
|
||||
torrentDht6Description: 'وقتی مسیر IPv6 قابل استفاده باشد، از آن برای کشف توزیعشده همتاها استفاده میکند.',
|
||||
torrentIpv6Enabled: 'فعالسازی IPv6 برای تورنت',
|
||||
torrentIpv6EnabledDescription: 'IPv6 را برای BitTorrent، DHT و کشف همتاها فعال نگه میدارد. غیرفعالکردن آن IPv6 DHT را خاموش میکند.',
|
||||
torrentPex: 'تبادل همتا (PEX)',
|
||||
torrentPexDescription: 'به همتاهای متصل اجازه میدهد آدرس همتاهای بیشتری را به اشتراک بگذارند.',
|
||||
torrentLpd: 'کشف همتای محلی (LPD)',
|
||||
@@ -834,6 +865,8 @@ const fa = {
|
||||
torrentMaxConcurrentSeedsDescription: 'وقتی ظرفیت جداگانه فعال است، حداکثر تعداد تورنتهایی که Firelink همزمان سید میکند.',
|
||||
torrentListenPort: 'پورتهای همتای TCP',
|
||||
torrentListenPortDescription: 'پورتهای TCP برای اتصالهای ورودی همتاهای بیتتورنت. برای محدوده پیشفرض Aria2 خالی بگذارید.',
|
||||
torrentBindAddress: 'نشانی اتصال تورنت',
|
||||
torrentBindAddressDescription: 'نشانی محلی اختیاری IPv4 یا IPv6 برای سوکتهای Aria2. نشانی نامعتبر رد میشود و تغییر پس از راهاندازی مجدد اعمال میشود.',
|
||||
torrentDhtListenPort: 'پورتهای UDP/DHT',
|
||||
torrentDhtListenPortDescription: 'پورتهای UDP برای DHT و ترکرهای UDP. برای محدوده پیشفرض Aria2 خالی بگذارید.',
|
||||
torrentExternalIp: 'آدرس IP خارجی',
|
||||
@@ -855,6 +888,8 @@ const fa = {
|
||||
torrentResourceLimits: 'محدودیت منابع بیتتورنت',
|
||||
torrentMaxOpenFiles: 'حداکثر فایلهای باز تورنت',
|
||||
torrentMaxOpenFilesDescription: 'حداکثر سراسری Aria2 برای تعداد فایلهای همزمان باز در تورنتهای چندفایلی. مقدار کمتر مصرف file descriptor را کم میکند؛ پیشفرض ۱۰۰ است. تغییرات برای تورنتهای جدید و بدون راهاندازی مجدد Aria2 اعمال میشوند و محدودیت سیستمعامل را افزایش نمیدهند.',
|
||||
aria2DiskCache: 'کش دیسک Aria2',
|
||||
aria2DiskCacheDescription: 'اندازهٔ کش Aria2؛ صفر یا مقداری مانند 16M وارد کنید. مقادیر K/M تا 1024M پذیرفته میشوند و پس از راهاندازی مجدد اعمال میشوند.',
|
||||
torrentMaxOpenFilesUpdateFailed: 'اعمال محدودیت فایلهای باز تورنت ممکن نشد: {{detail}}',
|
||||
torrentOverallUploadLimit: 'محدودیت کلی آپلود Aria2',
|
||||
torrentOverallUploadLimitDescription: 'سرعت کلی آپلود Aria2 را محدود میکند؛ در Firelink این مقدار عمدتاً برای سیدینگ تورنتهاست. برای نامحدود بودن خالی بگذارید؛ مقدار جدید زنده اعمال میشود و پس از راهاندازی مجدد Firelink برمیگردد.',
|
||||
|
||||
@@ -89,6 +89,7 @@ const he = {
|
||||
queued: 'בתור',
|
||||
downloading: 'מוריד',
|
||||
processing: 'מעבד',
|
||||
verifying: 'מאמת',
|
||||
seeding: 'משתף',
|
||||
waitingToSeed: 'ממתין לשיתוף',
|
||||
paused: 'מושהה',
|
||||
@@ -251,6 +252,8 @@ const he = {
|
||||
torrentTrackerIntervalInvalid: 'מרווח ה-Tracker חייב להיות מספר שלם בין 0 ל-604800 שניות',
|
||||
torrentVerifyIntegrity: 'אימות תקינות הטורנט',
|
||||
torrentVerifyIntegrityHint: 'מוחל כשהטורנט מתחיל או מנסה שוב. ייתכן שהחלקים ייבדקו מחדש ונתונים פגומים יורדו שוב; אי אפשר לשנות זאת בהעברה פעילה.',
|
||||
torrentVerifyNow: 'אמת עכשיו',
|
||||
torrentVerifyNowLoading: 'מאמת…',
|
||||
torrentMaxPeers: 'מספר העמיתים המרבי בטורנט',
|
||||
torrentPeerSpeedLimit: 'סף מהירות עמיתים',
|
||||
torrentMaxPeersInvalid: 'מספר העמיתים המרבי חייב להיות מספר שלם בין 0 ל-1000',
|
||||
@@ -262,6 +265,11 @@ const he = {
|
||||
torrentPeerDiagnosticsFailed: 'לא ניתן לקרוא את אבחון עמיתי הטורנט.',
|
||||
torrentPeerDiagnosticsHint: 'מוצגים רק מהירויות ודגלי חיבור; כתובות IP, יציאות, מזהים ושדות ביטים אינם נשמרים.',
|
||||
torrentFileProgress: 'התקדמות קובצי הטורנט',
|
||||
torrentFileSelection: 'בחירת קובצי טורנט',
|
||||
torrentFileSelectionHint: 'בחר אילו קבצים להוריד. בחירת כל הקבצים מסירה את הסינון; יש להשאיר לפחות קובץ אחד.',
|
||||
torrentFileSelectionRequired: 'בחר לפחות קובץ טורנט אחד.',
|
||||
torrentFileSelectionAll: 'בחר הכול',
|
||||
torrentFileSelectionClear: 'נקה',
|
||||
torrentFileProgressRefresh: 'רענון',
|
||||
torrentFileProgressLoading: 'טוען את התקדמות הקבצים…',
|
||||
torrentFileProgressUnavailable: 'התקדמות הקבצים זמינה כשהטורנט פעיל או מושהה.',
|
||||
@@ -300,6 +308,27 @@ const he = {
|
||||
torrentPrioritizePieceHint: 'מדיניות תצוגה מקדימה אופציונלית של Aria2: התחלה, סוף או שניהם; לכל אחד אפשר לציין גודל כמו 1M. השינוי חל בהפעלה או בניסיון חוזר.',
|
||||
torrentPrioritizePieceInvalid: 'תעדוף חלקי טורנט חייב לכלול התחלה ו/או סוף, עם גודל אופציונלי בין 1K ל-1024M',
|
||||
torrentEncryptionPolicy: 'מדיניות הצפנת Torrent',
|
||||
torrentFileAllocation: 'הקצאת קובצי Torrent',
|
||||
torrentFileAllocationPrealloc: 'הקצאה מראש',
|
||||
torrentFileAllocationNone: 'הקצאה לפי הצורך',
|
||||
torrentFileAllocationHint: 'הקצאה מראש שומרת מקום לקבצים לפני ההעברה; הקצאה לפי הצורך נמנעת מהשמירה הראשונית.',
|
||||
torrentDetails: 'פרטי טורנט',
|
||||
torrentDetailsLoading: 'טוען פרטי טורנט…',
|
||||
torrentDetailsUnavailable: 'פרטי הטורנט אינם זמינים.',
|
||||
torrentDetailsDisplayName: 'שם תצוגה',
|
||||
torrentDetailsInfoHash: 'גיבוב מידע',
|
||||
torrentDetailsSize: 'גודל כולל',
|
||||
torrentDetailsFiles: 'קבצים',
|
||||
torrentDetailsPieces: 'חלקים',
|
||||
torrentDetailsPrivate: 'פרטי',
|
||||
torrentDetailsPrivateYes: 'כן',
|
||||
torrentDetailsPrivateNo: 'לא',
|
||||
torrentDetailsCreated: 'נוצר',
|
||||
torrentDetailsCreator: 'יוצר',
|
||||
torrentDetailsComment: 'הערה',
|
||||
torrentDetailsTrackers: 'עוקבים',
|
||||
torrentDetailsWebSeeds: 'זרעי Web משובצים',
|
||||
torrentDetailsPrivateHint: 'טורנט פרטי זה משבית גילוי DHT, DHT6, PEX ו-LPD ללא קשר להגדרות הכלליות.',
|
||||
torrentEncryptionPolicyHint: 'מוחלת כשה-Torrent מתחיל או מנסה שוב. בחרו מדיניות אחת כדי לשמור על הגדרות handshake והצפנת payload עקביות.',
|
||||
torrentEncryptionDisabled: 'מושבתת',
|
||||
torrentEncryptionRequireCrypto: 'דרישת handshake מוסווה',
|
||||
@@ -819,6 +848,8 @@ const he = {
|
||||
torrentDhtDescription: 'מאתר עמיתים בלי להסתמך רק על מעקבים. השבתה מכבה גם תמיכה במעקבי UDP.',
|
||||
torrentDht6: 'DHT של IPv6',
|
||||
torrentDht6Description: 'משתמש ב-IPv6 לגילוי מבוזר של עמיתים כשיש נתיב IPv6 זמין.',
|
||||
torrentIpv6Enabled: 'הפעלת IPv6 עבור טורנטים',
|
||||
torrentIpv6EnabledDescription: 'משאיר את IPv6 זמין עבור BitTorrent, DHT וגילוי עמיתים. השבתה זו מכבה גם IPv6 DHT.',
|
||||
torrentPex: 'החלפת עמיתים (PEX)',
|
||||
torrentPexDescription: 'מאפשר לעמיתים מחוברים לשתף כתובות של עמיתים נוספים.',
|
||||
torrentLpd: 'גילוי עמיתים מקומיים (LPD)',
|
||||
@@ -834,6 +865,8 @@ const he = {
|
||||
torrentMaxConcurrentSeedsDescription: 'מספר הטורנטים המרבי ש-Firelink יזריע בו-זמנית כשהקיבולת הנפרדת פעילה.',
|
||||
torrentListenPort: 'יציאות עמיתי TCP',
|
||||
torrentListenPortDescription: 'יציאות TCP לחיבורי עמיתים נכנסים של BitTorrent. השאר ריק כדי להשתמש בטווח ברירת המחדל של Aria2.',
|
||||
torrentBindAddress: 'כתובת קישור לטורנטים',
|
||||
torrentBindAddressDescription: 'כתובת IPv4 או IPv6 מקומית ואופציונלית לשקעי Aria2. כתובות לא תקינות נדחות; השינוי חל לאחר הפעלה מחדש.',
|
||||
torrentDhtListenPort: 'יציאות UDP/DHT',
|
||||
torrentDhtListenPortDescription: 'יציאות UDP עבור DHT ועוקבי UDP. השאר ריק כדי להשתמש בטווח ברירת המחדל של Aria2.',
|
||||
torrentExternalIp: 'כתובת IP חיצונית',
|
||||
@@ -855,6 +888,8 @@ const he = {
|
||||
torrentResourceLimits: 'מגבלות משאבי BitTorrent',
|
||||
torrentMaxOpenFiles: 'מספר קובצי Torrent פתוחים מרבי',
|
||||
torrentMaxOpenFilesDescription: 'מגבלה כללית של Aria2 על מספר הקבצים הפתוחים בו-זמנית בטורנטים מרובי קבצים. ערך נמוך יותר מפחית שימוש ב-file descriptors; ברירת המחדל היא 100. השינויים חלים על טורנטים חדשים ללא הפעלה מחדש של Aria2, ואינם מגדילים את מגבלת מערכת ההפעלה.',
|
||||
aria2DiskCache: 'מטמון דיסק של Aria2',
|
||||
aria2DiskCacheDescription: 'גודל מטמון Aria2: 0 או ערך כמו 16M. ערכי K/M עד 1024M מתקבלים; חל לאחר הפעלה מחדש.',
|
||||
torrentMaxOpenFilesUpdateFailed: 'לא ניתן להחיל את מגבלת הקבצים הפתוחים של Torrent: {{detail}}',
|
||||
torrentOverallUploadLimit: 'מגבלת העלאה כוללת של Aria2',
|
||||
torrentOverallUploadLimitDescription: 'מגבילה את מהירות ההעלאה המשולבת של Aria2, בעיקר עבור העלאת טורנטים פעילים ב-Firelink. השאר ריק ללא הגבלה; הערך מוחל מיד ומשוחזר לאחר הפעלה מחדש של Firelink.',
|
||||
|
||||
@@ -89,6 +89,7 @@ const ru = {
|
||||
queued: 'В очереди',
|
||||
downloading: 'Загрузка',
|
||||
processing: 'Обработка',
|
||||
verifying: 'Проверка',
|
||||
seeding: 'Раздача',
|
||||
waitingToSeed: 'Ожидание раздачи',
|
||||
paused: 'Приостановлено',
|
||||
@@ -251,6 +252,8 @@ const ru = {
|
||||
torrentTrackerIntervalInvalid: 'Интервал трекера должен быть целым числом от 0 до 604800 секунд',
|
||||
torrentVerifyIntegrity: 'Проверять целостность торрента',
|
||||
torrentVerifyIntegrityHint: 'Применяется при запуске или повторной попытке. Может повторно проверить части и скачать повреждённые данные; во время активной передачи изменить нельзя.',
|
||||
torrentVerifyNow: 'Проверить сейчас',
|
||||
torrentVerifyNowLoading: 'Проверка…',
|
||||
torrentMaxPeers: 'Максимум пиров торрента',
|
||||
torrentPeerSpeedLimit: 'Порог скорости пиров',
|
||||
torrentMaxPeersInvalid: 'Максимум пиров должен быть целым числом от 0 до 1000',
|
||||
@@ -262,6 +265,11 @@ const ru = {
|
||||
torrentPeerDiagnosticsFailed: 'Не удалось получить диагностику пиров торрента.',
|
||||
torrentPeerDiagnosticsHint: 'Показываются только скорости и флаги соединения; IP-адреса, порты, идентификаторы и битовые поля не сохраняются.',
|
||||
torrentFileProgress: 'Прогресс файлов торрента',
|
||||
torrentFileSelection: 'Выбор файлов торрента',
|
||||
torrentFileSelectionHint: 'Выберите файлы для загрузки. Выбор всех файлов снимает фильтр; должен остаться хотя бы один файл.',
|
||||
torrentFileSelectionRequired: 'Выберите хотя бы один файл торрента.',
|
||||
torrentFileSelectionAll: 'Выбрать все',
|
||||
torrentFileSelectionClear: 'Очистить',
|
||||
torrentFileProgressRefresh: 'Обновить',
|
||||
torrentFileProgressLoading: 'Загрузка прогресса файлов…',
|
||||
torrentFileProgressUnavailable: 'Прогресс файлов доступен, пока торрент активен или приостановлен.',
|
||||
@@ -300,7 +308,28 @@ const ru = {
|
||||
torrentPrioritizePieceHint: 'Необязательная политика предпросмотра Aria2: начало, конец или оба варианта; для каждого можно указать размер, например 1M. Применяется при запуске или повторной попытке.',
|
||||
torrentPrioritizePieceInvalid: 'Приоритет частей торрента должен содержать начало и/или конец с необязательным размером от 1K до 1024M',
|
||||
torrentEncryptionPolicy: 'Политика шифрования Torrent',
|
||||
torrentFileAllocation: 'Выделение места для файлов Torrent',
|
||||
torrentFileAllocationPrealloc: 'Предварительное выделение',
|
||||
torrentFileAllocationNone: 'Выделять по мере необходимости',
|
||||
torrentFileAllocationHint: 'Предварительное выделение резервирует место до передачи; выделение по мере необходимости не делает начальное резервирование.',
|
||||
torrentEncryptionPolicyHint: 'Применяется при запуске или повторной попытке Torrent. Выберите одну политику, чтобы параметры handshake и шифрования payload оставались согласованными.',
|
||||
torrentDetails: 'Сведения о Torrent',
|
||||
torrentDetailsLoading: 'Загрузка сведений о Torrent…',
|
||||
torrentDetailsUnavailable: 'Сведения о Torrent недоступны.',
|
||||
torrentDetailsDisplayName: 'Отображаемое имя',
|
||||
torrentDetailsInfoHash: 'Инфохеш',
|
||||
torrentDetailsSize: 'Общий размер',
|
||||
torrentDetailsFiles: 'Файлы',
|
||||
torrentDetailsPieces: 'Части',
|
||||
torrentDetailsPrivate: 'Приватный',
|
||||
torrentDetailsPrivateYes: 'Да',
|
||||
torrentDetailsPrivateNo: 'Нет',
|
||||
torrentDetailsCreated: 'Создан',
|
||||
torrentDetailsCreator: 'Создатель',
|
||||
torrentDetailsComment: 'Комментарий',
|
||||
torrentDetailsTrackers: 'Трекеры',
|
||||
torrentDetailsWebSeeds: 'Встроенные веб-сиды',
|
||||
torrentDetailsPrivateHint: 'Этот приватный Torrent отключает обнаружение через DHT, DHT6, PEX и LPD независимо от общих настроек.',
|
||||
torrentEncryptionDisabled: 'Отключено',
|
||||
torrentEncryptionRequireCrypto: 'Требовать зашифрованное рукопожатие',
|
||||
torrentEncryptionForceEncryption: 'Принудительно шифровать payload (ARC4)',
|
||||
@@ -819,6 +848,8 @@ const ru = {
|
||||
torrentDhtDescription: 'Ищет пиры не только через трекеры. Отключение также отключает поддержку UDP-трекеров.',
|
||||
torrentDht6: 'DHT по IPv6',
|
||||
torrentDht6Description: 'Использует IPv6 для распределённого поиска пиров, если доступен рабочий IPv6-маршрут.',
|
||||
torrentIpv6Enabled: 'Использовать IPv6 для торрентов',
|
||||
torrentIpv6EnabledDescription: 'Оставляет IPv6 доступным для BitTorrent, DHT и поиска пиров. Отключение также выключает IPv6 DHT.',
|
||||
torrentPex: 'Обмен пирами (PEX)',
|
||||
torrentPexDescription: 'Позволяет подключённым пирам передавать адреса дополнительных пиров.',
|
||||
torrentLpd: 'Локальное обнаружение пиров (LPD)',
|
||||
@@ -834,6 +865,8 @@ const ru = {
|
||||
torrentMaxConcurrentSeedsDescription: 'Максимальное число торрентов, которые Firelink раздаёт одновременно при включённой отдельной ёмкости.',
|
||||
torrentListenPort: 'TCP-порты пиров',
|
||||
torrentListenPortDescription: 'TCP-порты для входящих соединений BitTorrent. Оставьте пустым, чтобы использовать диапазон Aria2 по умолчанию.',
|
||||
torrentBindAddress: 'Адрес привязки торрентов',
|
||||
torrentBindAddressDescription: 'Необязательный локальный IPv4- или IPv6-адрес для сокетов Aria2. Недопустимые адреса отклоняются; применяется после перезапуска.',
|
||||
torrentDhtListenPort: 'Порты UDP/DHT',
|
||||
torrentDhtListenPortDescription: 'UDP-порты для DHT и UDP-трекеров. Оставьте пустым, чтобы использовать диапазон Aria2 по умолчанию.',
|
||||
torrentExternalIp: 'Внешний IP-адрес',
|
||||
@@ -855,6 +888,8 @@ const ru = {
|
||||
torrentResourceLimits: 'Ограничения ресурсов BitTorrent',
|
||||
torrentMaxOpenFiles: 'Максимум открытых файлов Torrent',
|
||||
torrentMaxOpenFilesDescription: 'Глобальный лимит Aria2 на одновременно открытые файлы в многофайловых торрентах. Меньшие значения снижают расход дескрипторов; по умолчанию 100. Изменения применяются к новым торрентам без перезапуска Aria2 и не повышают лимит операционной системы.',
|
||||
aria2DiskCache: 'Дисковый кэш Aria2',
|
||||
aria2DiskCacheDescription: 'Размер кэша Aria2: 0 или значение вроде 16M. Допустимы K/M до 1024M; применяется после перезапуска.',
|
||||
torrentMaxOpenFilesUpdateFailed: 'Не удалось применить лимит открытых файлов Torrent: {{detail}}',
|
||||
torrentOverallUploadLimit: 'Общий лимит отдачи Aria2',
|
||||
torrentOverallUploadLimitDescription: 'Ограничивает суммарную скорость отдачи Aria2; в Firelink это в основном раздача активных торрентов. Оставьте поле пустым для снятия ограничения; значение применяется сразу и восстанавливается после перезапуска Firelink.',
|
||||
|
||||
@@ -89,6 +89,7 @@ const uk = {
|
||||
queued: 'У черзі',
|
||||
downloading: 'Завантаження',
|
||||
processing: 'Обробка',
|
||||
verifying: 'Перевірка',
|
||||
seeding: 'Роздача',
|
||||
waitingToSeed: 'Очікування роздачі',
|
||||
paused: 'Призупинено',
|
||||
@@ -251,6 +252,8 @@ const uk = {
|
||||
torrentTrackerIntervalInvalid: 'Інтервал трекера має бути цілим числом від 0 до 604800 секунд',
|
||||
torrentVerifyIntegrity: 'Перевіряти цілісність торрента',
|
||||
torrentVerifyIntegrityHint: 'Застосовується під час запуску або повторної спроби. Частини можуть перевірятися повторно, а пошкоджені дані — завантажуватися знову; під час активної передачі змінити не можна.',
|
||||
torrentVerifyNow: 'Перевірити зараз',
|
||||
torrentVerifyNowLoading: 'Перевірка…',
|
||||
torrentMaxPeers: 'Максимум пірів торрента',
|
||||
torrentPeerSpeedLimit: 'Поріг швидкості пірів',
|
||||
torrentMaxPeersInvalid: 'Максимум пірів має бути цілим числом від 0 до 1000',
|
||||
@@ -262,6 +265,11 @@ const uk = {
|
||||
torrentPeerDiagnosticsFailed: 'Не вдалося отримати діагностику пірів торрента.',
|
||||
torrentPeerDiagnosticsHint: 'Показуються лише швидкості та прапорці з’єднання; IP-адреси, порти, ідентифікатори й бітові поля не зберігаються.',
|
||||
torrentFileProgress: 'Прогрес файлів торрента',
|
||||
torrentFileSelection: 'Вибір файлів торрента',
|
||||
torrentFileSelectionHint: 'Виберіть файли для завантаження. Вибір усіх файлів прибирає фільтр; має залишитися хоча б один файл.',
|
||||
torrentFileSelectionRequired: 'Виберіть хоча б один файл торрента.',
|
||||
torrentFileSelectionAll: 'Вибрати все',
|
||||
torrentFileSelectionClear: 'Очистити',
|
||||
torrentFileProgressRefresh: 'Оновити',
|
||||
torrentFileProgressLoading: 'Завантаження прогресу файлів…',
|
||||
torrentFileProgressUnavailable: 'Прогрес файлів доступний, коли торрент активний або призупинений.',
|
||||
@@ -300,7 +308,28 @@ const uk = {
|
||||
torrentPrioritizePieceHint: 'Необов’язкова політика попереднього перегляду Aria2: початок, кінець або обидва варіанти; для кожного можна вказати розмір, наприклад 1M. Застосовується під час запуску або повторної спроби.',
|
||||
torrentPrioritizePieceInvalid: 'Пріоритет частин торрента має містити початок і/або кінець із необов’язковим розміром від 1K до 1024M',
|
||||
torrentEncryptionPolicy: 'Політика шифрування Torrent',
|
||||
torrentFileAllocation: 'Виділення місця для файлів Torrent',
|
||||
torrentFileAllocationPrealloc: 'Попереднє виділення',
|
||||
torrentFileAllocationNone: 'Виділяти за потреби',
|
||||
torrentFileAllocationHint: 'Попереднє виділення резервує місце до передачі; виділення за потреби не робить початкового резервування.',
|
||||
torrentEncryptionPolicyHint: 'Застосовується під час запуску або повторної спроби Torrent. Виберіть одну політику, щоб параметри handshake і шифрування payload залишалися узгодженими.',
|
||||
torrentDetails: 'Відомості про Torrent',
|
||||
torrentDetailsLoading: 'Завантаження відомостей про Torrent…',
|
||||
torrentDetailsUnavailable: 'Відомості про Torrent недоступні.',
|
||||
torrentDetailsDisplayName: 'Назва',
|
||||
torrentDetailsInfoHash: 'Інфохеш',
|
||||
torrentDetailsSize: 'Загальний розмір',
|
||||
torrentDetailsFiles: 'Файли',
|
||||
torrentDetailsPieces: 'Частини',
|
||||
torrentDetailsPrivate: 'Приватний',
|
||||
torrentDetailsPrivateYes: 'Так',
|
||||
torrentDetailsPrivateNo: 'Ні',
|
||||
torrentDetailsCreated: 'Створено',
|
||||
torrentDetailsCreator: 'Автор',
|
||||
torrentDetailsComment: 'Коментар',
|
||||
torrentDetailsTrackers: 'Трекери',
|
||||
torrentDetailsWebSeeds: 'Вбудовані веб-сиди',
|
||||
torrentDetailsPrivateHint: 'Цей приватний Torrent вимикає виявлення через DHT, DHT6, PEX і LPD незалежно від загальних налаштувань.',
|
||||
torrentEncryptionDisabled: 'Вимкнено',
|
||||
torrentEncryptionRequireCrypto: 'Вимагати зашифроване рукостискання',
|
||||
torrentEncryptionForceEncryption: 'Примусово шифрувати payload (ARC4)',
|
||||
@@ -819,6 +848,8 @@ const uk = {
|
||||
torrentDhtDescription: 'Шукає пірів не лише через трекери. Вимкнення також вимикає підтримку UDP-трекерів.',
|
||||
torrentDht6: 'DHT через IPv6',
|
||||
torrentDht6Description: 'Використовує IPv6 для розподіленого пошуку пірів, якщо доступний робочий маршрут IPv6.',
|
||||
torrentIpv6Enabled: 'Використовувати IPv6 для торентів',
|
||||
torrentIpv6EnabledDescription: 'Залишає IPv6 доступним для BitTorrent, DHT і пошуку пірів. Вимкнення також вимикає IPv6 DHT.',
|
||||
torrentPex: 'Обмін пірами (PEX)',
|
||||
torrentPexDescription: 'Дозволяє підключеним пірам передавати адреси додаткових пірів.',
|
||||
torrentLpd: 'Локальний пошук пірів (LPD)',
|
||||
@@ -834,6 +865,8 @@ const uk = {
|
||||
torrentMaxConcurrentSeedsDescription: 'Максимальна кількість торентів, які Firelink роздає одночасно за ввімкненої окремої місткості.',
|
||||
torrentListenPort: 'TCP-порти пірів',
|
||||
torrentListenPortDescription: 'TCP-порти для вхідних з’єднань BitTorrent. Залиште порожнім, щоб використати типовий діапазон Aria2.',
|
||||
torrentBindAddress: 'Адреса прив’язки торентів',
|
||||
torrentBindAddressDescription: 'Необов’язкова локальна IPv4- або IPv6-адреса для сокетів Aria2. Некоректні адреси відхиляються; застосовується після перезапуску.',
|
||||
torrentDhtListenPort: 'Порти UDP/DHT',
|
||||
torrentDhtListenPortDescription: 'UDP-порти для DHT і UDP-трекерів. Залиште порожнім, щоб використати типовий діапазон Aria2.',
|
||||
torrentExternalIp: 'Зовнішня IP-адреса',
|
||||
@@ -855,6 +888,8 @@ const uk = {
|
||||
torrentResourceLimits: 'Обмеження ресурсів BitTorrent',
|
||||
torrentMaxOpenFiles: 'Максимум відкритих файлів Torrent',
|
||||
torrentMaxOpenFilesDescription: 'Глобальне обмеження Aria2 на одночасно відкриті файли в багатофайлових торрентах. Менші значення зменшують використання дескрипторів; типове значення — 100. Зміни застосовуються до нових торрентів без перезапуску Aria2 і не підвищують обмеження операційної системи.',
|
||||
aria2DiskCache: 'Дисковий кеш Aria2',
|
||||
aria2DiskCacheDescription: 'Розмір кешу Aria2: 0 або значення на кшталт 16M. Допустимі K/M до 1024M; застосовується після перезапуску.',
|
||||
torrentMaxOpenFilesUpdateFailed: 'Не вдалося застосувати обмеження відкритих файлів Torrent: {{detail}}',
|
||||
torrentOverallUploadLimit: 'Загальне обмеження віддачі Aria2',
|
||||
torrentOverallUploadLimitDescription: 'Обмежує сумарну швидкість віддачі Aria2; у Firelink це переважно роздача активних торрентів. Залиште поле порожнім без обмеження; значення застосовується одразу й відновлюється після перезапуску Firelink.',
|
||||
|
||||
@@ -89,6 +89,7 @@ const zhCN = {
|
||||
queued: '已排队',
|
||||
downloading: '下载中',
|
||||
processing: '处理中',
|
||||
verifying: '校验中',
|
||||
seeding: '做种中',
|
||||
waitingToSeed: '等待做种',
|
||||
paused: '已暂停',
|
||||
@@ -251,6 +252,8 @@ const zhCN = {
|
||||
torrentTrackerIntervalInvalid: 'Tracker 间隔必须是 0 到 604800 秒之间的整数',
|
||||
torrentVerifyIntegrity: '验证 Torrent 完整性',
|
||||
torrentVerifyIntegrityHint: '在 Torrent 启动或重试时应用。可能会重新检查分片并重新下载损坏的数据;活动传输期间无法更改。',
|
||||
torrentVerifyNow: '立即验证',
|
||||
torrentVerifyNowLoading: '验证中…',
|
||||
torrentMaxPeers: 'Torrent 最大对等节点数',
|
||||
torrentPeerSpeedLimit: '对等节点速度阈值',
|
||||
torrentMaxPeersInvalid: 'Torrent 最大对等节点数必须是 0 到 1000 之间的整数',
|
||||
@@ -262,6 +265,11 @@ const zhCN = {
|
||||
torrentPeerDiagnosticsFailed: '无法读取 Torrent 对等节点诊断。',
|
||||
torrentPeerDiagnosticsHint: '仅显示速度和连接状态;不会保留对等节点 IP、端口、ID 或位域。',
|
||||
torrentFileProgress: 'Torrent 文件进度',
|
||||
torrentFileSelection: 'Torrent 文件选择',
|
||||
torrentFileSelectionHint: '选择要下载的文件。选择全部文件会移除筛选;至少要保留一个文件。',
|
||||
torrentFileSelectionRequired: '请至少选择一个 Torrent 文件。',
|
||||
torrentFileSelectionAll: '全选',
|
||||
torrentFileSelectionClear: '清除',
|
||||
torrentFileProgressRefresh: '刷新',
|
||||
torrentFileProgressLoading: '正在加载文件进度…',
|
||||
torrentFileProgressUnavailable: 'Torrent 活跃或暂停时可查看文件进度。',
|
||||
@@ -300,7 +308,28 @@ const zhCN = {
|
||||
torrentPrioritizePieceHint: '可选的 Aria2 预览策略:开头、结尾或两者;每项可使用 1M 等大小。Torrent 启动或重试时应用。',
|
||||
torrentPrioritizePieceInvalid: 'Torrent 片段优先级必须使用开头和/或结尾,并可选 1K 到 1024M 的大小',
|
||||
torrentEncryptionPolicy: 'Torrent 加密策略',
|
||||
torrentFileAllocation: 'Torrent 文件分配',
|
||||
torrentFileAllocationPrealloc: '预分配文件',
|
||||
torrentFileAllocationNone: '按需分配',
|
||||
torrentFileAllocationHint: '预分配会在传输前为选中文件预留空间;按需分配不会进行初始磁盘预留。',
|
||||
torrentEncryptionPolicyHint: '在 Torrent 启动或重试时应用。选择单一策略,确保握手和 payload 加密设置保持一致。',
|
||||
torrentDetails: 'Torrent 详细信息',
|
||||
torrentDetailsLoading: '正在加载 Torrent 详细信息…',
|
||||
torrentDetailsUnavailable: 'Torrent 详细信息不可用。',
|
||||
torrentDetailsDisplayName: '显示名称',
|
||||
torrentDetailsInfoHash: '信息哈希',
|
||||
torrentDetailsSize: '总大小',
|
||||
torrentDetailsFiles: '文件',
|
||||
torrentDetailsPieces: '分片',
|
||||
torrentDetailsPrivate: '私有',
|
||||
torrentDetailsPrivateYes: '是',
|
||||
torrentDetailsPrivateNo: '否',
|
||||
torrentDetailsCreated: '创建时间',
|
||||
torrentDetailsCreator: '创建者',
|
||||
torrentDetailsComment: '备注',
|
||||
torrentDetailsTrackers: 'Tracker',
|
||||
torrentDetailsWebSeeds: '内嵌 Web seed',
|
||||
torrentDetailsPrivateHint: '此私有 Torrent 会独立于全局设置禁用 DHT、DHT6、PEX 和 LPD 发现。',
|
||||
torrentEncryptionDisabled: '已禁用',
|
||||
torrentEncryptionRequireCrypto: '要求加密握手',
|
||||
torrentEncryptionForceEncryption: '强制加密 payload(ARC4)',
|
||||
@@ -819,6 +848,8 @@ const zhCN = {
|
||||
torrentDhtDescription: '不只依赖 Tracker 查找节点。关闭后也会禁用 UDP Tracker 支持。',
|
||||
torrentDht6: 'IPv6 分布式哈希表',
|
||||
torrentDht6Description: '当网络提供可用的 IPv6 路径时,使用 IPv6 进行分布式节点发现。',
|
||||
torrentIpv6Enabled: '为种子网络启用 IPv6',
|
||||
torrentIpv6EnabledDescription: '为 BitTorrent、DHT 和节点发现保留 IPv6。禁用后也会关闭 IPv6 DHT。',
|
||||
torrentPex: '节点交换(PEX)',
|
||||
torrentPexDescription: '允许已连接的节点共享其他节点的地址。',
|
||||
torrentLpd: '本地节点发现(LPD)',
|
||||
@@ -834,6 +865,8 @@ const zhCN = {
|
||||
torrentMaxConcurrentSeedsDescription: '启用独立容量后,Firelink 同时做种的 Torrent 数量上限。',
|
||||
torrentListenPort: 'TCP 节点端口',
|
||||
torrentListenPortDescription: '用于传入 BitTorrent 节点连接的 TCP 端口。留空以使用 Aria2 的默认范围。',
|
||||
torrentBindAddress: '种子绑定地址',
|
||||
torrentBindAddressDescription: '可选的本地 IPv4 或 IPv6 地址,用于 Aria2 套接字。无效地址会被拒绝;重启后生效。',
|
||||
torrentDhtListenPort: 'UDP/DHT 端口',
|
||||
torrentDhtListenPortDescription: '用于 DHT 和 UDP 跟踪器的 UDP 端口。留空以使用 Aria2 的默认范围。',
|
||||
torrentExternalIp: '外部 IP 地址',
|
||||
@@ -855,6 +888,8 @@ const zhCN = {
|
||||
torrentResourceLimits: 'BitTorrent 资源限制',
|
||||
torrentMaxOpenFiles: 'Torrent 最大打开文件数',
|
||||
torrentMaxOpenFilesDescription: 'Aria2 对多文件 Torrent 同时打开文件数的全局限制。较低的值可减少文件描述符占用;默认值为 100。修改会在不重启 Aria2 的情况下应用于新 Torrent,且不会提高操作系统的限制。',
|
||||
aria2DiskCache: 'Aria2 磁盘缓存',
|
||||
aria2DiskCacheDescription: 'Aria2 缓存大小:0 或类似 16M 的值。接受最大 1024M 的 K/M 值,重启后生效。',
|
||||
torrentMaxOpenFilesUpdateFailed: '无法应用 Torrent 打开文件数限制:{{detail}}',
|
||||
torrentOverallUploadLimit: 'Aria2 总上传限制',
|
||||
torrentOverallUploadLimitDescription: '限制 Aria2 的总上传速度,在 Firelink 中主要用于活动 Torrent 做种。留空表示不限速;新值会立即应用,并在 Firelink 重启后恢复。',
|
||||
|
||||
@@ -23,6 +23,8 @@ import type { TorrentPeerDiagnostics } from './bindings/TorrentPeerDiagnostics';
|
||||
import type { TorrentFileProgressSnapshot } from './bindings/TorrentFileProgressSnapshot';
|
||||
import type { TorrentPieceProgressSnapshot } from './bindings/TorrentPieceProgressSnapshot';
|
||||
import type { TorrentWebSeed } from './bindings/TorrentWebSeed';
|
||||
import type { TorrentDetails } from './bindings/TorrentDetails';
|
||||
import type { TorrentFileSelectionSnapshot } from './bindings/TorrentFileSelectionSnapshot';
|
||||
|
||||
type CommandMap = {
|
||||
fetch_metadata: {
|
||||
@@ -84,6 +86,10 @@ type CommandMap = {
|
||||
get_torrent_peers: { args: { id: string }; result: TorrentPeerDiagnostics };
|
||||
get_torrent_file_progress: { args: { id: string }; result: TorrentFileProgressSnapshot };
|
||||
get_torrent_piece_progress: { args: { id: string }; result: TorrentPieceProgressSnapshot };
|
||||
get_torrent_file_selection: { args: { id: string }; result: TorrentFileSelectionSnapshot };
|
||||
set_torrent_file_selection: { args: { id: string; selected_indices: number[] | null }; result: TorrentFileSelectionSnapshot };
|
||||
get_torrent_details: { args: { id: string }; result: TorrentDetails };
|
||||
verify_torrent_data: { args: { id: string }; result: void };
|
||||
get_torrent_web_seeds: { args: { id: string }; result: TorrentWebSeed[] };
|
||||
set_torrent_web_seeds: { args: { id: string; seeds: TorrentWebSeed[] }; result: TorrentWebSeed[] };
|
||||
set_torrent_max_open_files: { args: { max_open_files: number }; result: void };
|
||||
|
||||
@@ -45,14 +45,14 @@ const startDownloadListeners = async () => {
|
||||
// A sidecar can flush one last progress chunk after a pause, failure,
|
||||
// completion, or lifecycle reset. Do not let that stale chunk repopulate
|
||||
// the live progress map or overwrite a later lifecycle's first frame.
|
||||
if (!['downloading', 'processing', 'seeding'].includes(current.status)) {
|
||||
if (!['downloading', 'processing', 'verifying', 'seeding'].includes(current.status)) {
|
||||
useDownloadProgressStore.getState().clearDownloadProgress(payload.id);
|
||||
return;
|
||||
}
|
||||
useDownloadProgressStore.getState().updateDownloadProgress(payload.id, payload);
|
||||
const shouldUpdateSize = Boolean(payload.size && (!current.isMedia || payload.size_is_final));
|
||||
const updates: Partial<DownloadItem> = {};
|
||||
if (current.status === 'downloading' || current.status === 'processing' || current.status === 'seeding') {
|
||||
if (current.status === 'downloading' || current.status === 'processing' || current.status === 'verifying' || current.status === 'seeding') {
|
||||
updates.fraction = payload.fraction;
|
||||
updates.speed = current.status === 'seeding'
|
||||
? payload.upload_speed ?? '-'
|
||||
@@ -121,7 +121,7 @@ const startDownloadListeners = async () => {
|
||||
return;
|
||||
}
|
||||
if (status === 'downloading' || status === 'processing' ||
|
||||
status === 'seeding' || status === 'waitingToSeed' ||
|
||||
status === 'verifying' || status === 'seeding' || status === 'waitingToSeed' ||
|
||||
status === 'completed' || status === 'failed') {
|
||||
clearDownloadControlIntent(payload.id, 'resume');
|
||||
}
|
||||
@@ -173,7 +173,7 @@ const startDownloadListeners = async () => {
|
||||
: {})
|
||||
} : {}),
|
||||
...(payload.error ? { lastError: payload.error } : {}),
|
||||
...((status === 'downloading' || status === 'retrying')
|
||||
...((status === 'downloading' || status === 'verifying' || status === 'retrying')
|
||||
? { lastTry: new Date().toISOString() }
|
||||
: {})
|
||||
};
|
||||
@@ -189,10 +189,20 @@ const startDownloadListeners = async () => {
|
||||
updates.fileName = payload.fileName;
|
||||
updates.category = categoryForFileName(payload.fileName);
|
||||
}
|
||||
if (status !== 'downloading') {
|
||||
if (status !== 'downloading' && status !== 'verifying') {
|
||||
updates.speed = '-';
|
||||
updates.eta = '-';
|
||||
}
|
||||
if (
|
||||
current.torrentVerifyOnly === true &&
|
||||
['ready', 'staged', 'paused', 'completed', 'failed'].includes(status)
|
||||
) {
|
||||
// Verification is a maintenance lifecycle layered over the existing
|
||||
// row. Clear its markers once Aria2 has reached the restored terminal
|
||||
// state so restart cannot replay verification indefinitely.
|
||||
updates.torrentVerifyOnly = undefined;
|
||||
updates.torrentVerifyRestoreStatus = undefined;
|
||||
}
|
||||
mainStore.updateDownload(payload.id, updates);
|
||||
|
||||
if (status === 'completed' || status === 'failed' || status === 'paused' || status === 'seeding' || status === 'waitingToSeed') {
|
||||
@@ -205,7 +215,7 @@ const startDownloadListeners = async () => {
|
||||
: { pendingOrder: [...state.pendingOrder, payload.id] });
|
||||
}
|
||||
|
||||
if (status === 'queued' || status === 'downloading' || status === 'processing' || status === 'seeding' || status === 'waitingToSeed' || status === 'retrying') {
|
||||
if (status === 'queued' || status === 'downloading' || status === 'processing' || status === 'verifying' || status === 'seeding' || status === 'waitingToSeed' || status === 'retrying') {
|
||||
mainStore.registerBackendIds([payload.id]);
|
||||
} else if (status === 'completed' || status === 'failed') {
|
||||
mainStore.unregisterBackendIds([payload.id]);
|
||||
|
||||
@@ -167,6 +167,33 @@ describe('useDownloadStore', () => {
|
||||
expect(useDownloadStore.getState().downloads[0].torrentRemoveUnselectedFile).toBe(false);
|
||||
});
|
||||
|
||||
it('detaches a paused backend lifecycle even when the frontend registration set is stale', async () => {
|
||||
useDownloadStore.setState({
|
||||
downloads: [{
|
||||
id: 'paused-stale-registration',
|
||||
url: 'magnet:?xt=urn:btih:abc',
|
||||
fileName: 'torrent',
|
||||
status: 'paused',
|
||||
category: 'Other',
|
||||
dateAdded: '',
|
||||
isTorrent: true,
|
||||
torrentFileIndices: [1]
|
||||
}] as any[],
|
||||
backendRegisteredIds: new Set()
|
||||
});
|
||||
vi.mocked(ipc.invokeCommand).mockResolvedValue(undefined as never);
|
||||
|
||||
await useDownloadStore.getState().applyProperties('paused-stale-registration', {
|
||||
torrentFileIndices: [2]
|
||||
});
|
||||
|
||||
expect(ipc.invokeCommand).toHaveBeenCalledWith(
|
||||
'detach_download_for_reconfigure',
|
||||
{ id: 'paused-stale-registration' }
|
||||
);
|
||||
expect(useDownloadStore.getState().downloads[0].torrentFileIndices).toEqual([2]);
|
||||
});
|
||||
|
||||
it('replaces stale media intent when an appended handoff reuses a URL', () => {
|
||||
useDownloadStore.getState().openAddModalWithUrls(
|
||||
'https://example.com/file.bin', '', '', '', '', true
|
||||
|
||||
@@ -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, normalizeTorrentTrackerInterval, normalizeTorrentTrackerTimeout, redactDownloadForPersistence, resolveDownloadConnections } from '../utils/downloads';
|
||||
import { canonicalizeDownloadFileName, categoryForFileName, isActiveDownloadStatus, isTransferActiveStatus, isValidTorrentExcludeTrackerList, isValidTorrentTrackerList, MAX_TORRENT_STOP_TIMEOUT, normalizeSpeedLimitForBackend, normalizeTorrentEncryptionPolicy, normalizeTorrentFileAllocation, normalizeTorrentPrioritizePiece, normalizeTorrentTrackerInterval, normalizeTorrentTrackerTimeout, redactDownloadForPersistence, resolveDownloadConnections } from '../utils/downloads';
|
||||
import {
|
||||
resolveCategoryDestination
|
||||
} from '../utils/downloadLocations';
|
||||
@@ -362,6 +362,9 @@ async function dispatchItemInternal(id: string, proxyOverride?: string | null):
|
||||
torrent_prioritize_piece: item.torrentPrioritizePiece || undefined,
|
||||
torrent_remove_unselected_file: item.torrentRemoveUnselectedFile,
|
||||
torrent_encryption_policy: item.torrentEncryptionPolicy || undefined,
|
||||
torrent_file_allocation: item.torrentFileAllocation || undefined,
|
||||
torrent_verify_only: item.torrentVerifyOnly,
|
||||
torrent_verify_restore_status: item.torrentVerifyRestoreStatus,
|
||||
lifecycle_generation: lifecycleGeneration.toString(),
|
||||
};
|
||||
|
||||
@@ -690,6 +693,16 @@ export const normalizePersistedDownloadProgress = (download: DownloadItem): Down
|
||||
: undefined;
|
||||
const rawEncryptionPolicy = download.torrentEncryptionPolicy as unknown;
|
||||
const normalizedEncryptionPolicy = normalizeTorrentEncryptionPolicy(rawEncryptionPolicy);
|
||||
const rawFileAllocation = download.torrentFileAllocation as unknown;
|
||||
const normalizedFileAllocation = normalizeTorrentFileAllocation(rawFileAllocation);
|
||||
const rawVerifyOnly = download.torrentVerifyOnly as unknown;
|
||||
const normalizedVerifyOnly = rawVerifyOnly === true ? true : undefined;
|
||||
const rawVerifyRestoreStatus = download.torrentVerifyRestoreStatus as unknown;
|
||||
const normalizedVerifyRestoreStatus = normalizedVerifyOnly === true
|
||||
&& typeof rawVerifyRestoreStatus === 'string'
|
||||
&& ['paused', 'failed', 'completed'].includes(rawVerifyRestoreStatus)
|
||||
? rawVerifyRestoreStatus
|
||||
: undefined;
|
||||
const normalizedOptions = rawSeedRemaining !== normalizedSeedRemaining ||
|
||||
rawWebSeeds !== normalizedWebSeeds ||
|
||||
rawMaxPeers !== normalizedMaxPeers ||
|
||||
@@ -703,7 +716,10 @@ export const normalizePersistedDownloadProgress = (download: DownloadItem): Down
|
||||
rawStopTimeout !== normalizedStopTimeout ||
|
||||
rawPrioritizePiece !== normalizedPrioritizePiece ||
|
||||
rawRemoveUnselectedFile !== normalizedRemoveUnselectedFile ||
|
||||
rawEncryptionPolicy !== normalizedEncryptionPolicy
|
||||
rawEncryptionPolicy !== normalizedEncryptionPolicy ||
|
||||
rawFileAllocation !== normalizedFileAllocation ||
|
||||
rawVerifyOnly !== normalizedVerifyOnly ||
|
||||
rawVerifyRestoreStatus !== normalizedVerifyRestoreStatus
|
||||
? {
|
||||
...download,
|
||||
torrentSeedRemaining: normalizedSeedRemaining,
|
||||
@@ -719,7 +735,10 @@ export const normalizePersistedDownloadProgress = (download: DownloadItem): Down
|
||||
torrentStopTimeout: normalizedStopTimeout,
|
||||
torrentPrioritizePiece: normalizedPrioritizePiece,
|
||||
torrentRemoveUnselectedFile: normalizedRemoveUnselectedFile,
|
||||
torrentEncryptionPolicy: normalizedEncryptionPolicy
|
||||
torrentEncryptionPolicy: normalizedEncryptionPolicy,
|
||||
torrentFileAllocation: normalizedFileAllocation,
|
||||
torrentVerifyOnly: normalizedVerifyOnly,
|
||||
torrentVerifyRestoreStatus: normalizedVerifyRestoreStatus
|
||||
}
|
||||
: download;
|
||||
|
||||
@@ -940,7 +959,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
|
||||
&& normalizedUpdates.torrentRemoveUnselectedFile === false
|
||||
&& item.torrentRemoveUnselectedFile !== false;
|
||||
|
||||
if (item.status === 'downloading' || item.status === 'processing' || item.status === 'seeding' || item.status === 'retrying') {
|
||||
if (item.status === 'downloading' || item.status === 'processing' || item.status === 'verifying' || item.status === 'seeding' || item.status === 'retrying') {
|
||||
throw new Error(i18n.t($ => $.downloadTable.transferActive));
|
||||
}
|
||||
|
||||
@@ -974,15 +993,18 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
|
||||
}
|
||||
}
|
||||
} else if (item.status === 'paused') {
|
||||
if (isRegistered) {
|
||||
try {
|
||||
await invoke('detach_download_for_reconfigure', { id });
|
||||
} catch (e) {
|
||||
console.error("Failed to detach for reconfigure:", e);
|
||||
throw e; // Preserve old properties if detach fails
|
||||
}
|
||||
state.unregisterBackendIds([id]);
|
||||
// The frontend deliberately removes paused rows from
|
||||
// backendRegisteredIds, but the backend keeps a paused Aria2 GID and
|
||||
// its old payload alive for an in-place resume. Any property change,
|
||||
// especially Torrent selection/output changes, must retire that
|
||||
// lifecycle or resume will silently use stale daemon options.
|
||||
try {
|
||||
await invoke('detach_download_for_reconfigure', { id });
|
||||
} catch (e) {
|
||||
console.error("Failed to detach for reconfigure:", e);
|
||||
throw e; // Preserve old properties if detach fails
|
||||
}
|
||||
if (isRegistered) state.unregisterBackendIds([id]);
|
||||
if (disablingTorrentRemoval) {
|
||||
await invoke('clear_torrent_removal_paths', { id });
|
||||
}
|
||||
@@ -2288,6 +2310,9 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
|
||||
torrent_prioritize_piece: item.torrentPrioritizePiece || undefined,
|
||||
torrent_remove_unselected_file: item.torrentRemoveUnselectedFile,
|
||||
torrent_encryption_policy: item.torrentEncryptionPolicy || undefined,
|
||||
torrent_file_allocation: item.torrentFileAllocation || undefined,
|
||||
torrent_verify_only: item.torrentVerifyOnly,
|
||||
torrent_verify_restore_status: item.torrentVerifyRestoreStatus,
|
||||
lifecycle_generation: currentDownloadLifecycle(item.id).toString(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -254,6 +254,7 @@ export interface SettingsState {
|
||||
torrentDhtMessageTimeout: number;
|
||||
torrentSeparateSeedSlots: boolean;
|
||||
torrentMaxConcurrentSeeds: number;
|
||||
torrentIpv6Enabled: boolean;
|
||||
torrentListenPort: string;
|
||||
torrentDhtListenPort: string;
|
||||
torrentExternalIp: string;
|
||||
@@ -263,6 +264,8 @@ export interface SettingsState {
|
||||
torrentLpdInterface: string;
|
||||
torrentPeerIdPrefix: string;
|
||||
torrentPeerAgent: string;
|
||||
torrentBindAddress: string;
|
||||
aria2DiskCache: string;
|
||||
customUserAgent: string;
|
||||
askWhereToSaveEachFile: boolean;
|
||||
preventsSleepWhileDownloading: boolean;
|
||||
@@ -322,6 +325,7 @@ export interface SettingsState {
|
||||
setTorrentDhtMessageTimeout: (value: number) => void;
|
||||
setTorrentSeparateSeedSlots: (enabled: boolean) => void;
|
||||
setTorrentMaxConcurrentSeeds: (value: number) => void;
|
||||
setTorrentIpv6Enabled: (enabled: boolean) => void;
|
||||
setTorrentListenPort: (value: string) => void;
|
||||
setTorrentDhtListenPort: (value: string) => void;
|
||||
setTorrentExternalIp: (value: string) => void;
|
||||
@@ -331,6 +335,8 @@ export interface SettingsState {
|
||||
setTorrentLpdInterface: (value: string) => void;
|
||||
setTorrentPeerIdPrefix: (value: string) => void;
|
||||
setTorrentPeerAgent: (value: string) => void;
|
||||
setTorrentBindAddress: (value: string) => void;
|
||||
setAria2DiskCache: (value: string) => void;
|
||||
setCustomUserAgent: (userAgent: string) => void;
|
||||
setAskWhereToSaveEachFile: (ask: boolean) => void;
|
||||
setPreventsSleepWhileDownloading: (prevent: boolean) => void;
|
||||
@@ -416,6 +422,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
torrentDhtMessageTimeout: DEFAULT_TORRENT_DHT_MESSAGE_TIMEOUT,
|
||||
torrentSeparateSeedSlots: false,
|
||||
torrentMaxConcurrentSeeds: DEFAULT_TORRENT_MAX_CONCURRENT_SEEDS,
|
||||
torrentIpv6Enabled: true,
|
||||
torrentListenPort: '',
|
||||
torrentDhtListenPort: '',
|
||||
torrentExternalIp: '',
|
||||
@@ -425,6 +432,8 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
torrentLpdInterface: '',
|
||||
torrentPeerIdPrefix: '',
|
||||
torrentPeerAgent: '',
|
||||
torrentBindAddress: '',
|
||||
aria2DiskCache: '16M',
|
||||
customUserAgent: '',
|
||||
askWhereToSaveEachFile: false,
|
||||
preventsSleepWhileDownloading: true,
|
||||
@@ -549,6 +558,8 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
setTorrentLpdInterface: (torrentLpdInterface) => set({ torrentLpdInterface }),
|
||||
setTorrentPeerIdPrefix: (torrentPeerIdPrefix) => set({ torrentPeerIdPrefix }),
|
||||
setTorrentPeerAgent: (torrentPeerAgent) => set({ torrentPeerAgent }),
|
||||
setTorrentBindAddress: (torrentBindAddress) => set({ torrentBindAddress }),
|
||||
setAria2DiskCache: (aria2DiskCache) => set({ aria2DiskCache }),
|
||||
setTorrentMaxOpenFiles: (value) => {
|
||||
const normalized = normalizeTorrentMaxOpenFiles(value);
|
||||
if (normalized === undefined) {
|
||||
@@ -578,6 +589,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
? value
|
||||
: DEFAULT_TORRENT_MAX_CONCURRENT_SEEDS
|
||||
}),
|
||||
setTorrentIpv6Enabled: (torrentIpv6Enabled) => set({ torrentIpv6Enabled }),
|
||||
setCustomUserAgent: (customUserAgent) => set({ customUserAgent }),
|
||||
setAskWhereToSaveEachFile: (askWhereToSaveEachFile) => set({ askWhereToSaveEachFile }),
|
||||
setPreventsSleepWhileDownloading: (preventsSleepWhileDownloading) => {
|
||||
@@ -768,6 +780,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
torrentDhtMessageTimeout: state.torrentDhtMessageTimeout,
|
||||
torrentSeparateSeedSlots: state.torrentSeparateSeedSlots,
|
||||
torrentMaxConcurrentSeeds: state.torrentMaxConcurrentSeeds,
|
||||
torrentIpv6Enabled: state.torrentIpv6Enabled,
|
||||
torrentListenPort: state.torrentListenPort,
|
||||
torrentDhtListenPort: state.torrentDhtListenPort,
|
||||
torrentExternalIp: state.torrentExternalIp,
|
||||
@@ -777,6 +790,8 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
torrentLpdInterface: state.torrentLpdInterface,
|
||||
torrentPeerIdPrefix: state.torrentPeerIdPrefix,
|
||||
torrentPeerAgent: state.torrentPeerAgent,
|
||||
torrentBindAddress: state.torrentBindAddress,
|
||||
aria2DiskCache: state.aria2DiskCache,
|
||||
customUserAgent: state.customUserAgent,
|
||||
askWhereToSaveEachFile: state.askWhereToSaveEachFile,
|
||||
preventsSleepWhileDownloading: state.preventsSleepWhileDownloading,
|
||||
@@ -840,6 +855,10 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
&& persisted.torrentMaxConcurrentSeeds <= 64
|
||||
? persisted.torrentMaxConcurrentSeeds
|
||||
: currentState.torrentMaxConcurrentSeeds,
|
||||
torrentIpv6Enabled: persistedBoolean(
|
||||
persisted.torrentIpv6Enabled,
|
||||
currentState.torrentIpv6Enabled
|
||||
),
|
||||
torrentListenPort: typeof persisted.torrentListenPort === 'string'
|
||||
? persisted.torrentListenPort
|
||||
: currentState.torrentListenPort,
|
||||
@@ -867,6 +886,12 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
torrentPeerAgent: typeof persisted.torrentPeerAgent === 'string'
|
||||
? persisted.torrentPeerAgent
|
||||
: currentState.torrentPeerAgent,
|
||||
torrentBindAddress: typeof persisted.torrentBindAddress === 'string'
|
||||
? persisted.torrentBindAddress
|
||||
: currentState.torrentBindAddress,
|
||||
aria2DiskCache: typeof persisted.aria2DiskCache === 'string'
|
||||
? persisted.aria2DiskCache
|
||||
: currentState.aria2DiskCache,
|
||||
sidebarPosition: isAllowedSetting(SIDEBAR_POSITION_VALUES, persisted.sidebarPosition)
|
||||
? persisted.sidebarPosition
|
||||
: currentState.sidebarPosition,
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('download action policy', () => {
|
||||
expect(canStartDownload(status)).toBe(true);
|
||||
expect(canPauseDownload(status)).toBe(false);
|
||||
}
|
||||
for (const status of ['staged', 'queued', 'downloading', 'seeding', 'processing', 'retrying'] as const) {
|
||||
for (const status of ['staged', 'queued', 'downloading', 'seeding', 'processing', 'verifying', 'retrying'] as const) {
|
||||
expect(canPauseDownload(status)).toBe(true);
|
||||
}
|
||||
for (const status of ['queued', 'downloading', 'processing', 'retrying'] as const) {
|
||||
@@ -40,6 +40,7 @@ describe('download action policy', () => {
|
||||
expect(getPauseResumeAction('queued')).toBe('pause');
|
||||
expect(getPauseResumeAction('downloading')).toBe('pause');
|
||||
expect(getPauseResumeAction('processing')).toBe('pause');
|
||||
expect(getPauseResumeAction('verifying')).toBe('pause');
|
||||
expect(getPauseResumeAction('seeding')).toBe('pause');
|
||||
expect(getPauseResumeAction('retrying')).toBe('pause');
|
||||
expect(getPauseResumeAction('paused')).toBe('resume');
|
||||
|
||||
@@ -15,6 +15,7 @@ const PAUSABLE_STATUSES: ReadonlySet<DownloadStatus> = new Set([
|
||||
'seeding',
|
||||
'waitingToSeed',
|
||||
'processing',
|
||||
'verifying',
|
||||
'retrying',
|
||||
]);
|
||||
|
||||
@@ -66,7 +67,7 @@ export const startActionLabel = (status: DownloadStatus): 'Start' | 'Resume' =>
|
||||
status === 'ready' || status === 'staged' || status === 'failed' ? 'Start' : 'Resume';
|
||||
|
||||
export const isTransferLocked = (status: DownloadStatus): boolean =>
|
||||
status === 'downloading' || status === 'processing' || status === 'seeding' || status === 'waitingToSeed' || status === 'retrying';
|
||||
status === 'downloading' || status === 'processing' || status === 'verifying' || status === 'seeding' || status === 'waitingToSeed' || status === 'retrying';
|
||||
|
||||
export const isIdentityLocked = (status: DownloadStatus): boolean =>
|
||||
isTransferLocked(status) || status === 'completed';
|
||||
|
||||
@@ -73,6 +73,8 @@ export const downloadProgressColorClass = (status: string): string => {
|
||||
return 'download-status-failed';
|
||||
case 'processing':
|
||||
return 'download-status-processing';
|
||||
case 'verifying':
|
||||
return 'download-status-processing';
|
||||
case 'seeding':
|
||||
return 'download-status-seeding';
|
||||
case 'queued':
|
||||
|
||||
@@ -22,6 +22,7 @@ const isFreshDownloadStatus = (status: DownloadItem['status']): boolean =>
|
||||
status === 'downloading' ||
|
||||
status === 'seeding' ||
|
||||
status === 'processing' ||
|
||||
status === 'verifying' ||
|
||||
status === 'retrying';
|
||||
|
||||
const hasPositiveProgress = (download: DownloadItem): boolean =>
|
||||
|
||||
@@ -56,6 +56,14 @@ describe('download persistence progress snapshots', () => {
|
||||
expect(persisted.totalIsEstimate).toBe(false);
|
||||
}
|
||||
);
|
||||
|
||||
it('does not persist verification byte counters across restart', () => {
|
||||
const persisted = redactDownloadForPersistence(item('verifying'));
|
||||
|
||||
expect(persisted.downloadedBytes).toBeUndefined();
|
||||
expect(persisted.totalBytes).toBeUndefined();
|
||||
expect(persisted.totalIsEstimate).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Torrent tracker input validation', () => {
|
||||
|
||||
@@ -30,6 +30,7 @@ const ACTIVE_DOWNLOAD_STATUSES: ReadonlySet<DownloadStatus> = new Set([
|
||||
'queued',
|
||||
'downloading',
|
||||
'processing',
|
||||
'verifying',
|
||||
'seeding',
|
||||
'waitingToSeed',
|
||||
'retrying',
|
||||
@@ -40,7 +41,7 @@ export const isActiveDownloadStatus = (status: DownloadStatus): boolean =>
|
||||
|
||||
/** Transfer states that consume a worker/permit. Queued is intentionally excluded. */
|
||||
export const isTransferActiveStatus = (status: DownloadStatus): boolean =>
|
||||
status === 'downloading' || status === 'processing' || status === 'seeding' || status === 'retrying';
|
||||
status === 'downloading' || status === 'processing' || status === 'verifying' || status === 'seeding' || status === 'retrying';
|
||||
|
||||
export const DOWNLOAD_CONNECTIONS_MIN = 1;
|
||||
export const DOWNLOAD_CONNECTIONS_MAX = 16;
|
||||
@@ -66,6 +67,11 @@ export const normalizeTorrentEncryptionPolicy = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export type TorrentFileAllocation = 'prealloc' | 'none';
|
||||
|
||||
export const normalizeTorrentFileAllocation = (value: unknown): TorrentFileAllocation | undefined =>
|
||||
value === 'prealloc' || value === 'none' ? value : undefined;
|
||||
|
||||
export const MAX_TORRENT_TRACKER_TIMEOUT = 604800;
|
||||
export const MAX_TORRENT_TRACKER_INTERVAL = 604800;
|
||||
export const DEFAULT_TORRENT_MAX_OPEN_FILES = 100;
|
||||
@@ -456,6 +462,7 @@ export const isMediaUrl = (rawUrl: string): boolean => {
|
||||
const DOWNLOAD_SECRET_FIELDS = ['password', 'cookies', 'headers'] as const;
|
||||
const VOLATILE_PROGRESS_STATUSES = new Set([
|
||||
'downloading',
|
||||
'verifying',
|
||||
'seeding'
|
||||
]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user