mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-22 08:56:44 +00:00
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:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user