mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-17 06:47:48 +00:00
feat(torrents): add seeding lifecycle and upload controls
This commit is contained in:
@@ -125,6 +125,84 @@ describe('useDownloadProgressStore', () => {
|
||||
release();
|
||||
});
|
||||
|
||||
it('projects torrent seeding state and upload telemetry', async () => {
|
||||
const handlers: Record<string, (event: any) => void> = {};
|
||||
vi.mocked(ipc.listenEvent).mockImplementation((event, handler) => {
|
||||
handlers[event] = handler as (event: any) => void;
|
||||
return Promise.resolve(vi.fn());
|
||||
});
|
||||
useDownloadStore.setState({
|
||||
downloads: [{
|
||||
id: 'torrent-seeding',
|
||||
url: 'magnet:?xt=urn:btih:test',
|
||||
fileName: 'ubuntu.iso',
|
||||
status: 'downloading',
|
||||
category: 'Other',
|
||||
dateAdded: ''
|
||||
}]
|
||||
});
|
||||
|
||||
const release = await initDownloadListener();
|
||||
handlers['download-state']({ payload: {
|
||||
id: 'torrent-seeding',
|
||||
status: 'seeding'
|
||||
} });
|
||||
handlers['download-progress']({ payload: {
|
||||
id: 'torrent-seeding',
|
||||
fraction: 1,
|
||||
speed: '0 B/s',
|
||||
eta: '-',
|
||||
size: '2 GB',
|
||||
size_is_final: false,
|
||||
uploaded_bytes: 1048576,
|
||||
upload_speed: '512 KiB/s',
|
||||
num_seeders: 4,
|
||||
active_connections: 6,
|
||||
requested_connections: 8
|
||||
} });
|
||||
|
||||
expect(useDownloadStore.getState().downloads[0]).toMatchObject({
|
||||
status: 'seeding',
|
||||
fraction: 1,
|
||||
speed: '512 KiB/s',
|
||||
eta: '-'
|
||||
});
|
||||
expect(useDownloadProgressStore.getState().progressMap['torrent-seeding'])
|
||||
.toMatchObject({ uploaded_bytes: 1048576, upload_speed: '512 KiB/s', num_seeders: 4 });
|
||||
release();
|
||||
});
|
||||
|
||||
it('does not regress a seeding row from a delayed active state event', async () => {
|
||||
const handlers: Record<string, (event: any) => void> = {};
|
||||
vi.mocked(ipc.listenEvent).mockImplementation((event, handler) => {
|
||||
handlers[event] = handler as (event: any) => void;
|
||||
return Promise.resolve(vi.fn());
|
||||
});
|
||||
useDownloadStore.setState({
|
||||
downloads: [{
|
||||
id: 'torrent-seeding-race',
|
||||
url: 'magnet:?xt=urn:btih:test',
|
||||
fileName: 'ubuntu.iso',
|
||||
status: 'seeding',
|
||||
category: 'Other',
|
||||
dateAdded: ''
|
||||
}]
|
||||
});
|
||||
|
||||
const release = await initDownloadListener();
|
||||
handlers['download-state']({ payload: {
|
||||
id: 'torrent-seeding-race',
|
||||
status: 'downloading'
|
||||
} });
|
||||
handlers['download-state']({ payload: {
|
||||
id: 'torrent-seeding-race',
|
||||
status: 'queued'
|
||||
} });
|
||||
|
||||
expect(useDownloadStore.getState().downloads[0].status).toBe('seeding');
|
||||
release();
|
||||
});
|
||||
|
||||
it('clears progress when events arrive after a download row was removed', async () => {
|
||||
const handlers: Record<string, (event: any) => void> = {};
|
||||
vi.mocked(ipc.listenEvent).mockImplementation((event, handler) => {
|
||||
|
||||
@@ -45,17 +45,19 @@ 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'].includes(current.status)) {
|
||||
if (!['downloading', 'processing', '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') {
|
||||
if (current.status === 'downloading' || current.status === 'processing' || current.status === 'seeding') {
|
||||
updates.fraction = payload.fraction;
|
||||
updates.speed = payload.speed;
|
||||
updates.eta = payload.eta;
|
||||
updates.speed = current.status === 'seeding'
|
||||
? payload.upload_speed ?? '-'
|
||||
: payload.speed;
|
||||
updates.eta = current.status === 'seeding' ? '-' : payload.eta;
|
||||
}
|
||||
if (shouldUpdateSize && current.size !== payload.size) {
|
||||
updates.size = payload.size!;
|
||||
@@ -119,7 +121,7 @@ const startDownloadListeners = async () => {
|
||||
return;
|
||||
}
|
||||
if (status === 'downloading' || status === 'processing' ||
|
||||
status === 'completed' || status === 'failed') {
|
||||
status === 'seeding' || status === 'completed' || status === 'failed') {
|
||||
clearDownloadControlIntent(payload.id, 'resume');
|
||||
}
|
||||
if (status === 'paused') {
|
||||
@@ -142,6 +144,13 @@ const startDownloadListeners = async () => {
|
||||
status !== 'failed') {
|
||||
return;
|
||||
}
|
||||
if (current.status === 'seeding' &&
|
||||
status !== 'seeding' &&
|
||||
status !== 'paused' &&
|
||||
status !== 'completed' &&
|
||||
status !== 'failed') {
|
||||
return;
|
||||
}
|
||||
|
||||
const progress = useDownloadProgressStore.getState().progressMap[payload.id];
|
||||
if (['queued', 'retrying', 'completed', 'failed', 'paused'].includes(status)) {
|
||||
@@ -179,7 +188,7 @@ const startDownloadListeners = async () => {
|
||||
}
|
||||
mainStore.updateDownload(payload.id, updates);
|
||||
|
||||
if (status === 'completed' || status === 'failed' || status === 'paused') {
|
||||
if (status === 'completed' || status === 'failed' || status === 'paused' || status === 'seeding') {
|
||||
useDownloadStore.setState(state => ({
|
||||
pendingOrder: state.pendingOrder.filter(id => id !== payload.id)
|
||||
}));
|
||||
@@ -189,7 +198,7 @@ const startDownloadListeners = async () => {
|
||||
: { pendingOrder: [...state.pendingOrder, payload.id] });
|
||||
}
|
||||
|
||||
if (status === 'queued' || status === 'downloading' || status === 'processing' || status === 'retrying') {
|
||||
if (status === 'queued' || status === 'downloading' || status === 'processing' || status === 'seeding' || status === 'retrying') {
|
||||
mainStore.registerBackendIds([payload.id]);
|
||||
} else if (status === 'completed' || status === 'failed') {
|
||||
mainStore.unregisterBackendIds([payload.id]);
|
||||
|
||||
@@ -345,6 +345,9 @@ async function dispatchItemInternal(id: string, proxyOverride?: string | null):
|
||||
torrent_path: item.torrentPath || undefined,
|
||||
torrent_file_indices: item.torrentFileIndices || undefined,
|
||||
torrent_info_hash: item.torrentInfoHash || undefined,
|
||||
torrent_seed_time: item.torrentSeedTime,
|
||||
torrent_seed_ratio: item.torrentSeedRatio,
|
||||
torrent_upload_limit: item.torrentUploadLimit || undefined,
|
||||
lifecycle_generation: lifecycleGeneration.toString(),
|
||||
};
|
||||
|
||||
@@ -817,7 +820,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
|
||||
? updates
|
||||
: { ...updates, fileName: canonicalizeDownloadFileName(updates.fileName) };
|
||||
|
||||
if (item.status === 'downloading' || item.status === 'processing' || item.status === 'retrying') {
|
||||
if (item.status === 'downloading' || item.status === 'processing' || item.status === 'seeding' || item.status === 'retrying') {
|
||||
throw new Error(i18n.t($ => $.downloadTable.transferActive));
|
||||
}
|
||||
|
||||
@@ -1411,8 +1414,8 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
|
||||
if (updates.status && ['completed', 'failed', 'paused'].includes(updates.status)) {
|
||||
info(`Download ${id} status changed to ${updates.status}`);
|
||||
syncSystemIntegrations();
|
||||
} else if (updates.status === 'downloading') {
|
||||
info(`Download ${id} status changed to downloading`);
|
||||
} else if (updates.status === 'downloading' || updates.status === 'seeding') {
|
||||
info(`Download ${id} status changed to ${updates.status}`);
|
||||
syncSystemIntegrations();
|
||||
}
|
||||
},
|
||||
@@ -2048,6 +2051,9 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
|
||||
torrent_path: item.torrentPath || undefined,
|
||||
torrent_file_indices: item.torrentFileIndices || undefined,
|
||||
torrent_info_hash: item.torrentInfoHash || undefined,
|
||||
torrent_seed_time: item.torrentSeedTime,
|
||||
torrent_seed_ratio: item.torrentSeedRatio,
|
||||
torrent_upload_limit: item.torrentUploadLimit || undefined,
|
||||
lifecycle_generation: currentDownloadLifecycle(item.id).toString(),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user