fix(torrent): harden metadata reuse and live peer telemetry

- reuse validated tracker-bearing metainfo without restoring direct web seeds
- preserve Torrent file selection while making long paths scrollable and copyable
- add lifecycle-fenced peer and seeder summaries to Properties telemetry
- validate cache tracker metadata and cover malformed, stale, and path-copy cases
This commit is contained in:
NimBold
2026-08-14 13:13:06 +03:30
parent 314f4e2e00
commit de41dd55d6
23 changed files with 536 additions and 81 deletions
+23
View File
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest';
import { isCurrentTorrentPeerSummary, isTorrentPeerSummaryStatus } from './propertiesPeerSummary';
describe('Torrent peer summary lifecycle', () => {
it('accepts only the current active Torrent lifecycle', () => {
const request = {
currentDownloadId: 'torrent-1',
requestDownloadId: 'torrent-1',
currentLifecycleEpoch: 8,
requestLifecycleEpoch: 8,
currentStatus: 'downloading',
};
expect(isCurrentTorrentPeerSummary(request)).toBe(true);
expect(isCurrentTorrentPeerSummary({ ...request, currentLifecycleEpoch: 9 })).toBe(false);
expect(isCurrentTorrentPeerSummary({ ...request, requestDownloadId: 'torrent-2' })).toBe(false);
});
it('stops live summary polling for paused and completed lifecycles', () => {
expect(isTorrentPeerSummaryStatus('paused')).toBe(false);
expect(isTorrentPeerSummaryStatus('completed')).toBe(false);
expect(isTorrentPeerSummaryStatus('seeding')).toBe(true);
});
});
+26
View File
@@ -0,0 +1,26 @@
const TORRENT_PEER_SUMMARY_ACTIVE_STATUSES = [
'downloading',
'verifying',
'seeding',
'waitingToSeed',
'retrying',
] as const;
export const isTorrentPeerSummaryStatus = (status: string): boolean =>
(TORRENT_PEER_SUMMARY_ACTIVE_STATUSES as readonly string[]).includes(status);
export const isCurrentTorrentPeerSummary = ({
currentDownloadId,
requestDownloadId,
currentLifecycleEpoch,
requestLifecycleEpoch,
currentStatus,
}: {
currentDownloadId: string | null;
requestDownloadId: string;
currentLifecycleEpoch: number;
requestLifecycleEpoch: number;
currentStatus: string;
}): boolean => currentDownloadId === requestDownloadId
&& currentLifecycleEpoch === requestLifecycleEpoch
&& isTorrentPeerSummaryStatus(currentStatus);
+21 -3
View File
@@ -58,16 +58,34 @@ describe('Properties connection presentation', () => {
});
});
it('uses connected peers for Torrents', () => {
it('does not use tellActive connections for the Torrent header', () => {
expect(getPropertiesConnectionPresentation({
isMedia: false,
isTorrent: true,
connectedPeers: 4,
})).toEqual({
kind: 'torrent',
showHeaderMetric: true,
labelKey: 'torrentConnectedPeers',
value: '4',
value: '',
});
});
it('exposes the explicit live peer and seeder summary values', () => {
expect(getPropertiesConnectionPresentation({
isMedia: false,
isTorrent: true,
}, {
totalPeers: 41,
totalSeeders: 2,
})).toEqual({
kind: 'torrent',
showHeaderMetric: true,
labelKey: 'torrentConnectedPeers',
value: '—',
torrentPeerSummary: {
totalPeers: 41,
totalSeeders: 2,
},
});
});
});
+9 -2
View File
@@ -3,12 +3,17 @@ import { resolveDownloadFraction } from './downloadProgress';
export type PropertiesConnectionKind = 'media' | 'torrent' | 'aria2';
export type PropertiesConnectionLabelKey = 'fragmentConcurrency' | 'torrentConnectedPeers' | 'connections';
export type PropertiesTorrentPeerSummary = {
totalPeers: number;
totalSeeders: number;
};
export type PropertiesConnectionPresentation = {
kind: PropertiesConnectionKind;
showHeaderMetric: boolean;
labelKey: PropertiesConnectionLabelKey;
value: string;
torrentPeerSummary?: PropertiesTorrentPeerSummary;
};
const displayCount = (value: number | undefined): string => value == null ? '—' : String(value);
@@ -20,7 +25,8 @@ export const getPropertiesProgress = (
: resolveDownloadFraction(snapshot);
export const getPropertiesConnectionPresentation = (
snapshot: Pick<PropertiesSnapshot, 'isMedia' | 'isTorrent' | 'connections' | 'activeConnections' | 'requestedConnections' | 'connectedPeers'>,
snapshot: Pick<PropertiesSnapshot, 'isMedia' | 'isTorrent' | 'connections' | 'activeConnections' | 'requestedConnections'>,
torrentPeerSummary?: PropertiesTorrentPeerSummary | null,
): PropertiesConnectionPresentation => {
if (snapshot.isMedia === true) {
return {
@@ -36,7 +42,8 @@ export const getPropertiesConnectionPresentation = (
kind: 'torrent',
showHeaderMetric: true,
labelKey: 'torrentConnectedPeers',
value: displayCount(snapshot.connectedPeers),
value: '—',
...(torrentPeerSummary ? { torrentPeerSummary } : {}),
};
}
+14
View File
@@ -0,0 +1,14 @@
import { describe, expect, it, vi } from 'vitest';
import { copyTorrentFilePath } from './torrentFilePath';
describe('Torrent file paths', () => {
it('copies the complete long path without changing its separators or characters', async () => {
const path = 'Season 03/Scenes/This-is-a-deliberately-long-file-name-with-unicode-字幕.mkv';
const writeText = vi.fn(async () => undefined);
await copyTorrentFilePath(path, writeText);
expect(writeText).toHaveBeenCalledOnce();
expect(writeText).toHaveBeenCalledWith(path);
});
});
+9
View File
@@ -0,0 +1,9 @@
export type ClipboardWriter = (text: string) => Promise<void>;
/** Copy the exact Torrent-relative path without normalizing or truncating it. */
export const copyTorrentFilePath = async (
path: string,
writeText: ClipboardWriter,
): Promise<void> => {
await writeText(path);
};