feat(torrents): add live upload limit control

This commit is contained in:
NimBold
2026-08-01 21:41:04 +03:30
parent dea6ad1974
commit 9247c5cf9c
13 changed files with 515 additions and 1 deletions
+65
View File
@@ -396,6 +396,71 @@ describe('useDownloadStore', () => {
expect(useDownloadStore.getState().downloads[0].speedLimit).toBeUndefined();
});
it('updates an active Torrent upload limit while seeding and clears it', async () => {
useDownloadStore.setState({
downloads: [{
id: 'live-torrent-upload',
status: 'seeding',
isMedia: false,
isTorrent: true,
torrentUploadLimit: '512K'
}] as any[]
});
vi.mocked(ipc.invokeCommand).mockResolvedValue(undefined as never);
await useDownloadStore.getState().setTorrentUploadLimit('live-torrent-upload', '2M');
expect(ipc.invokeCommand).toHaveBeenCalledWith('set_torrent_upload_limit', {
id: 'live-torrent-upload',
limit: '2M'
});
expect(useDownloadStore.getState().downloads[0].torrentUploadLimit).toBe('2M');
await useDownloadStore.getState().setTorrentUploadLimit('live-torrent-upload', null);
const uploadLimitCalls = vi.mocked(ipc.invokeCommand).mock.calls
.filter(([command]) => command === 'set_torrent_upload_limit');
expect(uploadLimitCalls[uploadLimitCalls.length - 1]).toEqual(['set_torrent_upload_limit', {
id: 'live-torrent-upload',
limit: null
}]);
expect(useDownloadStore.getState().downloads[0].torrentUploadLimit).toBeUndefined();
});
it('rejects live Torrent upload control for ordinary or inactive downloads', async () => {
useDownloadStore.setState({
downloads: [
{ id: 'ordinary-upload', status: 'downloading', isMedia: false, isTorrent: false },
{ id: 'paused-upload', status: 'paused', isMedia: false, isTorrent: true }
] as any[]
});
await expect(useDownloadStore.getState().setTorrentUploadLimit('ordinary-upload', '2M'))
.rejects.toThrow('only for Torrent');
await expect(useDownloadStore.getState().setTorrentUploadLimit('paused-upload', '2M'))
.rejects.toThrow('active Torrent');
expect(ipc.invokeCommand).not.toHaveBeenCalledWith('set_torrent_upload_limit', expect.anything());
});
it('keeps the prior Torrent upload limit when the backend rejects the update', async () => {
useDownloadStore.setState({
downloads: [{
id: 'live-torrent-upload-failure',
status: 'downloading',
isMedia: false,
isTorrent: true,
torrentUploadLimit: '512K'
}] as any[]
});
vi.mocked(ipc.invokeCommand).mockImplementation(async command => {
if (command === 'set_torrent_upload_limit') throw new Error('aria2 unavailable');
return undefined;
});
await expect(useDownloadStore.getState().setTorrentUploadLimit('live-torrent-upload-failure', '2M'))
.rejects.toThrow('aria2 unavailable');
expect(useDownloadStore.getState().downloads[0].torrentUploadLimit).toBe('512K');
});
it('rejects live speed changes for media and inactive downloads', async () => {
useDownloadStore.setState({
downloads: [
+34
View File
@@ -800,6 +800,7 @@ interface DownloadState {
pauseAll: () => Promise<number>;
assignToQueue: (ids: string[], queueId: string) => Promise<void>;
setDownloadSpeedLimit: (id: string, limit: string | null) => Promise<void>;
setTorrentUploadLimit: (id: string, limit: string | null) => Promise<void>;
setQueueConcurrency: (id: string, maxConcurrent: number | null) => Promise<void>;
addQueue: (name: string) => boolean;
renameQueue: (id: string, name: string) => boolean;
@@ -1886,6 +1887,39 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
true,
preemptDispatch
),
setTorrentUploadLimit: (id, limit) => runDownloadLifecycleOperation(
id,
'torrent-upload-limit',
async () => {
await waitForPendingStartupResume();
const item = get().downloads.find(download => download.id === id);
if (!item) throw new Error('Download no longer exists.');
if (!item.isTorrent) {
throw new Error('Live upload control is available only for Torrent downloads.');
}
if (!['downloading', 'seeding', 'retrying'].includes(item.status)) {
throw new Error('Live upload control requires an active Torrent.');
}
const trimmed = limit?.trim() || '';
const normalizedLimit = trimmed
? normalizeSpeedLimitForBackend(trimmed)
: null;
if (trimmed && normalizedLimit === null) {
throw new Error('Enter a valid Torrent upload limit.');
}
await invoke('set_torrent_upload_limit', {
id,
limit: normalizedLimit
});
if (get().downloads.some(download => download.id === id)) {
get().updateDownload(id, { torrentUploadLimit: normalizedLimit ?? undefined });
}
},
true,
preemptDispatch
),
setQueueConcurrency: (id, maxConcurrent) => {
const operation = queueConfigurationQueue.then(async () => {
if (