@ctrl/qbittorrent 6.1.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # qBittorrent [![npm](https://badgen.net/npm/v/@ctrl/qbittorrent)](https://www.npmjs.com/package/@ctrl/qbittorrent) [![CircleCI](https://badgen.net/circleci/github/scttcper/qbittorrent)](https://circleci.com/gh/scttcper/qbittorrent) [![coverage](https://badgen.net/codecov/c/github/scttcper/qbittorrent)](https://codecov.io/gh/scttcper/qbittorrent)
1
+ # qBittorrent [![npm](https://badgen.net/npm/v/@ctrl/qbittorrent)](https://www.npmjs.com/package/@ctrl/qbittorrent) [![coverage](https://badgen.net/codecov/c/github/scttcper/qbittorrent)](https://codecov.io/gh/scttcper/qbittorrent)
2
2
 
3
3
  > TypeScript api wrapper for [qBittorrent](https://www.qbittorrent.org/) using [got](https://github.com/sindresorhus/got)
4
4
 
@@ -0,0 +1,3 @@
1
+ import { NormalizedTorrent } from '@ctrl/shared-torrent';
2
+ import { Torrent } from './types.js';
3
+ export declare function normalizeTorrentData(torrent: Torrent): NormalizedTorrent;
@@ -0,0 +1,91 @@
1
+ import { TorrentState as NormalizedTorrentState } from '@ctrl/shared-torrent';
2
+ import { TorrentState } from './types.js';
3
+ export function normalizeTorrentData(torrent) {
4
+ let state = NormalizedTorrentState.unknown;
5
+ let stateMessage = '';
6
+ let { eta } = torrent;
7
+ /**
8
+ * Good references https://github.com/qbittorrent/qBittorrent/blob/master/src/webui/www/private/scripts/dynamicTable.js#L933
9
+ * https://github.com/Radarr/Radarr/blob/develop/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs#L242
10
+ */
11
+ switch (torrent.state) {
12
+ case TorrentState.Error:
13
+ state = NormalizedTorrentState.warning;
14
+ stateMessage = 'qBittorrent is reporting an error';
15
+ break;
16
+ case TorrentState.PausedDL:
17
+ state = NormalizedTorrentState.paused;
18
+ break;
19
+ case TorrentState.QueuedDL: // queuing is enabled and torrent is queued for download
20
+ case TorrentState.CheckingDL: // same as checkingUP, but torrent has NOT finished downloading
21
+ case TorrentState.CheckingUP: // torrent has finished downloading and is being checked. Set when `recheck torrent on completion` is enabled. In the event the check fails we shouldn't treat it as completed.
22
+ state = NormalizedTorrentState.queued;
23
+ break;
24
+ case TorrentState.MetaDL: // Metadl could be an error if DHT is not enabled
25
+ case TorrentState.ForcedDL: // torrent is being downloaded, and was forced started
26
+ case TorrentState.ForcedMetaDL: // torrent metadata is being forcibly downloaded
27
+ case TorrentState.Downloading: // torrent is being downloaded and data is being transferred
28
+ state = NormalizedTorrentState.downloading;
29
+ break;
30
+ case TorrentState.Allocating:
31
+ // state = 'stalledDL';
32
+ state = NormalizedTorrentState.queued;
33
+ break;
34
+ case TorrentState.StalledDL:
35
+ state = NormalizedTorrentState.warning;
36
+ stateMessage = 'The download is stalled with no connection';
37
+ break;
38
+ case TorrentState.PausedUP: // torrent is paused and has finished downloading:
39
+ case TorrentState.Uploading: // torrent is being seeded and data is being transferred
40
+ case TorrentState.StalledUP: // torrent is being seeded, but no connection were made
41
+ case TorrentState.QueuedUP: // queuing is enabled and torrent is queued for upload
42
+ case TorrentState.ForcedUP: // torrent has finished downloading and is being forcibly seeded
43
+ // state = 'completed';
44
+ state = NormalizedTorrentState.seeding;
45
+ eta = 0; // qBittorrent sends eta=8640000 for completed torrents
46
+ break;
47
+ case TorrentState.Moving: // torrent is being moved from a folder
48
+ case TorrentState.QueuedForChecking:
49
+ case TorrentState.CheckingResumeData:
50
+ state = NormalizedTorrentState.checking;
51
+ break;
52
+ case TorrentState.Unknown:
53
+ state = NormalizedTorrentState.error;
54
+ break;
55
+ case TorrentState.MissingFiles:
56
+ state = NormalizedTorrentState.error;
57
+ stateMessage = 'The download is missing files';
58
+ break;
59
+ default:
60
+ break;
61
+ }
62
+ const isCompleted = torrent.progress === 1;
63
+ const result = {
64
+ id: torrent.hash,
65
+ name: torrent.name,
66
+ stateMessage,
67
+ state,
68
+ eta,
69
+ dateAdded: new Date(torrent.added_on * 1000).toISOString(),
70
+ isCompleted,
71
+ progress: torrent.progress,
72
+ label: torrent.category,
73
+ tags: torrent.tags.split(', '),
74
+ dateCompleted: new Date(torrent.completion_on * 1000).toISOString(),
75
+ savePath: torrent.save_path,
76
+ uploadSpeed: torrent.upspeed,
77
+ downloadSpeed: torrent.dlspeed,
78
+ queuePosition: torrent.priority,
79
+ connectedPeers: torrent.num_leechs,
80
+ connectedSeeds: torrent.num_seeds,
81
+ totalPeers: torrent.num_incomplete,
82
+ totalSeeds: torrent.num_complete,
83
+ totalSelected: torrent.size,
84
+ totalSize: torrent.total_size,
85
+ totalUploaded: torrent.uploaded,
86
+ totalDownloaded: torrent.downloaded,
87
+ ratio: torrent.ratio,
88
+ raw: torrent,
89
+ };
90
+ return result;
91
+ }
@@ -1,5 +1,4 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
- import type { Options as GotOptions, Response } from 'got';
3
2
  import type { AddTorrentOptions as NormalizedAddTorrentOptions, AllClientData, NormalizedTorrent, TorrentClient, TorrentSettings } from '@ctrl/shared-torrent';
4
3
  import type { AddMagnetOptions, AddTorrentOptions, BuildInfo, Preferences, Torrent, TorrentCategories, TorrentFile, TorrentFilePriority, TorrentFilters, TorrentPeersResponse, TorrentPieceState, TorrentProperties, TorrentTrackers, WebSeed } from './types.js';
5
4
  export declare class QBittorrent implements TorrentClient {
@@ -208,11 +207,5 @@ export declare class QBittorrent implements TorrentClient {
208
207
  */
209
208
  login(): Promise<boolean>;
210
209
  logout(): boolean;
211
- request<T extends object | string>(path: string, method: GotOptions['method'], params?: any, body?: GotOptions['body'], form?: GotOptions['form'], headers?: any, json?: boolean): Promise<Response<T>>;
212
- /**
213
- * Normalizes hashes
214
- * @returns hashes as string seperated by `|`
215
- */
216
- private _normalizeHashes;
217
- private _normalizeTorrentData;
210
+ request<T>(path: string, method: 'GET' | 'POST', params?: Record<string, string | number>, body?: URLSearchParams | FormData, headers?: any, json?: boolean): Promise<T>;
218
211
  }
@@ -1,16 +1,10 @@
1
- /* eslint-disable @typescript-eslint/no-redundant-type-constituents */
2
- /* eslint-disable @typescript-eslint/ban-types */
3
- import { existsSync } from 'fs';
4
- import { URLSearchParams } from 'url';
5
- import { File, FormData } from 'formdata-node';
6
- import { fileFromPath } from 'formdata-node/file-from-path';
7
- import got from 'got';
1
+ import { FormData } from 'node-fetch-native';
2
+ import { ofetch } from 'ofetch';
8
3
  import { Cookie } from 'tough-cookie';
4
+ import { joinURL } from 'ufo';
9
5
  import { magnetDecode } from '@ctrl/magnet-link';
10
- import { TorrentState as NormalizedTorrentState } from '@ctrl/shared-torrent';
11
6
  import { hash } from '@ctrl/torrent-file';
12
- import { urlJoin } from '@ctrl/url-join';
13
- import { TorrentState } from './types.js';
7
+ import { normalizeTorrentData } from './normalizeTorrentData.js';
14
8
  const defaults = {
15
9
  baseUrl: 'http://localhost:9091/',
16
10
  path: '/api/v2',
@@ -19,31 +13,16 @@ const defaults = {
19
13
  timeout: 5000,
20
14
  };
21
15
  export class QBittorrent {
16
+ config;
17
+ /**
18
+ * auth cookie
19
+ */
20
+ _sid;
21
+ /**
22
+ * cookie expiration
23
+ */
24
+ _exp;
22
25
  constructor(options = {}) {
23
- Object.defineProperty(this, "config", {
24
- enumerable: true,
25
- configurable: true,
26
- writable: true,
27
- value: void 0
28
- });
29
- /**
30
- * auth cookie
31
- */
32
- Object.defineProperty(this, "_sid", {
33
- enumerable: true,
34
- configurable: true,
35
- writable: true,
36
- value: void 0
37
- });
38
- /**
39
- * cookie expiration
40
- */
41
- Object.defineProperty(this, "_exp", {
42
- enumerable: true,
43
- configurable: true,
44
- writable: true,
45
- value: void 0
46
- });
47
26
  this.config = { ...defaults, ...options };
48
27
  }
49
28
  /**
@@ -57,19 +36,19 @@ export class QBittorrent {
57
36
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-application-version}
58
37
  */
59
38
  async getAppVersion() {
60
- const res = await this.request('/app/version', 'GET', undefined, undefined, undefined, undefined, false);
61
- return res.body;
39
+ const res = await this.request('/app/version', 'GET', undefined, undefined, undefined, false);
40
+ return res;
62
41
  }
63
42
  async getApiVersion() {
64
- const res = await this.request('/app/webapiVersion', 'GET', undefined, undefined, undefined, undefined, false);
65
- return res.body;
43
+ const res = await this.request('/app/webapiVersion', 'GET', undefined, undefined, undefined, false);
44
+ return res;
66
45
  }
67
46
  /**
68
47
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-build-info}
69
48
  */
70
49
  async getBuildInfo() {
71
50
  const res = await this.request('/app/buildInfo', 'GET');
72
- return res.body;
51
+ return res;
73
52
  }
74
53
  async getTorrent(hash) {
75
54
  const torrentsResponse = await this.listTorrents({ hashes: hash });
@@ -77,22 +56,22 @@ export class QBittorrent {
77
56
  if (!torrentData) {
78
57
  throw new Error('Torrent not found');
79
58
  }
80
- return this._normalizeTorrentData(torrentData);
59
+ return normalizeTorrentData(torrentData);
81
60
  }
82
61
  /**
83
62
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-application-preferences}
84
63
  */
85
64
  async getPreferences() {
86
65
  const res = await this.request('/app/preferences', 'GET');
87
- return res.body;
66
+ return res;
88
67
  }
89
68
  /**
90
69
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#set-application-preferences}
91
70
  */
92
71
  async setPreferences(preferences) {
93
- await this.request('/app/setPreferences', 'POST', undefined, undefined, {
72
+ await this.request('/app/setPreferences', 'POST', undefined, objToUrlSearchParams({
94
73
  json: JSON.stringify(preferences),
95
- });
74
+ }));
96
75
  return true;
97
76
  }
98
77
  /**
@@ -105,7 +84,7 @@ export class QBittorrent {
105
84
  async listTorrents({ hashes, filter, category, sort, offset, reverse, tag, } = {}) {
106
85
  const params = {};
107
86
  if (hashes) {
108
- params.hashes = this._normalizeHashes(hashes);
87
+ params.hashes = normalizeHashes(hashes);
109
88
  }
110
89
  if (filter) {
111
90
  params.filter = filter;
@@ -126,7 +105,7 @@ export class QBittorrent {
126
105
  params.reverse = JSON.stringify(reverse);
127
106
  }
128
107
  const res = await this.request('/torrents/info', 'GET', params);
129
- return res.body;
108
+ return res;
130
109
  }
131
110
  async getAllData() {
132
111
  const listTorrents = await this.listTorrents();
@@ -137,7 +116,7 @@ export class QBittorrent {
137
116
  };
138
117
  const labels = {};
139
118
  for (const torrent of listTorrents) {
140
- const torrentData = this._normalizeTorrentData(torrent);
119
+ const torrentData = normalizeTorrentData(torrent);
141
120
  results.torrents.push(torrentData);
142
121
  // setup label
143
122
  if (torrentData.label) {
@@ -160,40 +139,40 @@ export class QBittorrent {
160
139
  */
161
140
  async torrentProperties(hash) {
162
141
  const res = await this.request('/torrents/properties', 'GET', { hash });
163
- return res.body;
142
+ return res;
164
143
  }
165
144
  /**
166
145
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-trackers}
167
146
  */
168
147
  async torrentTrackers(hash) {
169
148
  const res = await this.request('/torrents/trackers', 'GET', { hash });
170
- return res.body;
149
+ return res;
171
150
  }
172
151
  /**
173
152
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-web-seeds}
174
153
  */
175
154
  async torrentWebSeeds(hash) {
176
155
  const res = await this.request('/torrents/webseeds', 'GET', { hash });
177
- return res.body;
156
+ return res;
178
157
  }
179
158
  async torrentFiles(hash) {
180
159
  const res = await this.request('/torrents/files', 'GET', { hash });
181
- return res.body;
160
+ return res;
182
161
  }
183
162
  async setFilePriority(hash, fileIds, priority) {
184
163
  const res = await this.request('/torrents/filePrio', 'GET', {
185
164
  hash,
186
- id: this._normalizeHashes(fileIds),
165
+ id: normalizeHashes(fileIds),
187
166
  priority,
188
167
  });
189
- return res.body;
168
+ return res;
190
169
  }
191
170
  /**
192
171
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-pieces-states}
193
172
  */
194
173
  async torrentPieceStates(hash) {
195
174
  const res = await this.request('/torrents/pieceStates', 'GET', { hash });
196
- return res.body;
175
+ return res;
197
176
  }
198
177
  /**
199
178
  * Torrents piece hashes
@@ -202,7 +181,7 @@ export class QBittorrent {
202
181
  */
203
182
  async torrentPieceHashes(hash) {
204
183
  const res = await this.request('/torrents/pieceHashes', 'GET', { hash });
205
- return res.body;
184
+ return res;
206
185
  }
207
186
  /**
208
187
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#set-torrent-location}
@@ -210,7 +189,7 @@ export class QBittorrent {
210
189
  async setTorrentLocation(hashes, location) {
211
190
  await this.request('/torrents/setLocation', 'POST', undefined, undefined, {
212
191
  location,
213
- hashes: this._normalizeHashes(hashes),
192
+ hashes: normalizeHashes(hashes),
214
193
  });
215
194
  return true;
216
195
  }
@@ -218,27 +197,24 @@ export class QBittorrent {
218
197
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#set-torrent-name}
219
198
  */
220
199
  async setTorrentName(hash, name) {
221
- await this.request('/torrents/rename', 'POST', undefined, undefined, {
222
- hash,
223
- name,
224
- });
200
+ const data = { hash, name };
201
+ await this.request('/torrents/rename', 'POST', undefined, objToUrlSearchParams(data));
225
202
  return true;
226
203
  }
227
204
  /**
228
205
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-all-tags}
229
206
  */
230
207
  async getTags() {
231
- const res = await this.request('/torrents/tags', 'get');
232
- return res.body;
208
+ const res = await this.request('/torrents/tags', 'GET');
209
+ return res;
233
210
  }
234
211
  /**
235
212
  * @param tags comma separated list
236
213
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#create-tags}
237
214
  */
238
215
  async createTags(tags) {
239
- await this.request('/torrents/createTags', 'POST', undefined, undefined, {
240
- tags,
241
- }, undefined, false);
216
+ const data = { tags };
217
+ await this.request('/torrents/createTags', 'POST', undefined, objToUrlSearchParams(data), undefined, false);
242
218
  return true;
243
219
  }
244
220
  /**
@@ -246,53 +222,47 @@ export class QBittorrent {
246
222
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#delete-tags}
247
223
  */
248
224
  async deleteTags(tags) {
249
- await this.request('/torrents/deleteTags', 'POST', undefined, undefined, { tags }, undefined, false);
225
+ const data = { tags };
226
+ await this.request('/torrents/deleteTags', 'POST', undefined, objToUrlSearchParams(data), undefined, false);
250
227
  return true;
251
228
  }
252
229
  /**
253
230
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-all-categories}
254
231
  */
255
232
  async getCategories() {
256
- const res = await this.request('/torrents/categories', 'get');
257
- return res.body;
233
+ const res = await this.request('/torrents/categories', 'GET');
234
+ return res;
258
235
  }
259
236
  /**
260
237
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#add-new-category}
261
238
  */
262
239
  async createCategory(category, savePath = '') {
263
- await this.request('/torrents/createCategory', 'POST', undefined, undefined, {
264
- category,
265
- savePath,
266
- }, undefined, false);
240
+ const data = { category, savePath };
241
+ await this.request('/torrents/createCategory', 'POST', undefined, objToUrlSearchParams(data), undefined, false);
267
242
  return true;
268
243
  }
269
244
  /**
270
245
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#edit-category}
271
246
  */
272
247
  async editCategory(category, savePath = '') {
273
- await this.request('/torrents/editCategory', 'POST', undefined, undefined, {
274
- category,
275
- savePath,
276
- }, undefined, false);
248
+ const data = { category, savePath };
249
+ await this.request('/torrents/editCategory', 'POST', undefined, objToUrlSearchParams(data), undefined, false);
277
250
  return true;
278
251
  }
279
252
  /**
280
253
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#remove-categories}
281
254
  */
282
255
  async removeCategory(categories) {
283
- await this.request('/torrents/removeCategories', 'POST', undefined, undefined, {
284
- categories,
285
- }, undefined, false);
256
+ const data = { categories };
257
+ await this.request('/torrents/removeCategories', 'POST', undefined, objToUrlSearchParams(data), undefined, false);
286
258
  return true;
287
259
  }
288
260
  /**
289
261
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#add-torrent-tags}
290
262
  */
291
263
  async addTorrentTags(hashes, tags) {
292
- await this.request('/torrents/addTags', 'POST', undefined, undefined, {
293
- hashes: this._normalizeHashes(hashes),
294
- tags,
295
- }, undefined, false);
264
+ const data = { hashes: normalizeHashes(hashes), tags };
265
+ await this.request('/torrents/addTags', 'POST', undefined, objToUrlSearchParams(data), undefined, false);
296
266
  return true;
297
267
  }
298
268
  /**
@@ -300,11 +270,11 @@ export class QBittorrent {
300
270
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#remove-torrent-tags}
301
271
  */
302
272
  async removeTorrentTags(hashes, tags) {
303
- const form = { hashes: this._normalizeHashes(hashes) };
273
+ const data = { hashes: normalizeHashes(hashes) };
304
274
  if (tags) {
305
- form.tags = tags;
275
+ data.tags = tags;
306
276
  }
307
- await this.request('/torrents/removeTags', 'POST', undefined, undefined, form, undefined, false);
277
+ await this.request('/torrents/removeTags', 'POST', undefined, objToUrlSearchParams(data), undefined, false);
308
278
  return true;
309
279
  }
310
280
  /**
@@ -317,61 +287,54 @@ export class QBittorrent {
317
287
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#set-torrent-category}
318
288
  */
319
289
  async setTorrentCategory(hashes, category = '') {
320
- await this.request('/torrents/setCategory', 'POST', undefined, undefined, {
321
- hashes: this._normalizeHashes(hashes),
290
+ const data = {
291
+ hashes: normalizeHashes(hashes),
322
292
  category,
323
- });
293
+ };
294
+ await this.request('/torrents/setCategory', 'POST', undefined, objToUrlSearchParams(data));
324
295
  return true;
325
296
  }
326
297
  /**
327
298
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#pause-torrents}
328
299
  */
329
300
  async pauseTorrent(hashes) {
330
- const params = {
331
- hashes: this._normalizeHashes(hashes),
332
- };
333
- await this.request('/torrents/pause', 'POST', undefined, undefined, params);
301
+ const data = { hashes: normalizeHashes(hashes) };
302
+ await this.request('/torrents/pause', 'POST', undefined, objToUrlSearchParams(data));
334
303
  return true;
335
304
  }
336
305
  /**
337
306
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#resume-torrents}
338
307
  */
339
308
  async resumeTorrent(hashes) {
340
- const params = {
341
- hashes: this._normalizeHashes(hashes),
342
- };
343
- await this.request('/torrents/resume', 'POST', undefined, undefined, params);
309
+ const data = { hashes: normalizeHashes(hashes) };
310
+ await this.request('/torrents/resume', 'POST', undefined, objToUrlSearchParams(data));
344
311
  return true;
345
312
  }
346
313
  /**
347
314
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#delete-torrents}
348
315
  */
349
316
  async removeTorrent(hashes, deleteFiles = true) {
350
- const params = {
351
- hashes: this._normalizeHashes(hashes),
317
+ const data = {
318
+ hashes: normalizeHashes(hashes),
352
319
  deleteFiles,
353
320
  };
354
- await this.request('/torrents/delete', 'POST', undefined, undefined, params);
321
+ await this.request('/torrents/delete', 'POST', undefined, objToUrlSearchParams(data));
355
322
  return true;
356
323
  }
357
324
  /**
358
325
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#recheck-torrents}
359
326
  */
360
327
  async recheckTorrent(hashes) {
361
- const params = {
362
- hashes: this._normalizeHashes(hashes),
363
- };
364
- await this.request('/torrents/recheck', 'POST', undefined, undefined, params);
328
+ const data = { hashes: normalizeHashes(hashes) };
329
+ await this.request('/torrents/recheck', 'POST', undefined, objToUrlSearchParams(data));
365
330
  return true;
366
331
  }
367
332
  /**
368
333
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#reannounce-torrents}
369
334
  */
370
335
  async reannounceTorrent(hashes) {
371
- const params = {
372
- hashes: this._normalizeHashes(hashes),
373
- };
374
- await this.request('/torrents/reannounce', 'POST', undefined, undefined, params);
336
+ const data = { hashes: normalizeHashes(hashes) };
337
+ await this.request('/torrents/reannounce', 'POST', undefined, objToUrlSearchParams(data));
375
338
  return true;
376
339
  }
377
340
  async addTorrent(torrent, options = {}) {
@@ -382,13 +345,7 @@ export class QBittorrent {
382
345
  }
383
346
  const type = { type: 'application/x-bittorrent' };
384
347
  if (typeof torrent === 'string') {
385
- if (existsSync(torrent)) {
386
- const file = await fileFromPath(torrent, options.filename ?? 'torrent', type);
387
- form.set('file', file);
388
- }
389
- else {
390
- form.set('file', new File([Buffer.from(torrent, 'base64')], 'file.torrent', type));
391
- }
348
+ form.set('file', new File([Buffer.from(torrent, 'base64')], 'file.torrent', type));
392
349
  }
393
350
  else {
394
351
  const file = new File([torrent], options.filename ?? 'torrent', type);
@@ -403,11 +360,11 @@ export class QBittorrent {
403
360
  options.useAutoTMM = 'false';
404
361
  }
405
362
  for (const [key, value] of Object.entries(options)) {
406
- form.append(key, value);
363
+ form.append(key, `${value}`);
407
364
  }
408
365
  }
409
- const res = await this.request('/torrents/add', 'POST', undefined, form, undefined, undefined, false);
410
- if (res.body === 'Fails.') {
366
+ const res = await this.request('/torrents/add', 'POST', undefined, form, undefined, false);
367
+ if (res === 'Fails.') {
411
368
  throw new Error('Failed to add torrent');
412
369
  }
413
370
  return true;
@@ -445,9 +402,9 @@ export class QBittorrent {
445
402
  async renameFile(hash, id, name) {
446
403
  const form = new FormData();
447
404
  form.append('hash', hash);
448
- form.append('id', id);
405
+ form.append('id', id.toString());
449
406
  form.append('name', name);
450
- await this.request('/torrents/renameFile', 'POST', undefined, form, undefined, false);
407
+ await this.request('/torrents/renameFile', 'POST', undefined, undefined, form, false);
451
408
  return true;
452
409
  }
453
410
  /**
@@ -458,7 +415,7 @@ export class QBittorrent {
458
415
  form.append('hash', hash);
459
416
  form.append('oldPath', oldPath);
460
417
  form.append('newPath', newPath);
461
- await this.request('/torrents/renameFolder', 'POST', undefined, form, undefined, false);
418
+ await this.request('/torrents/renameFolder', 'POST', undefined, undefined, form, false);
462
419
  return true;
463
420
  }
464
421
  /**
@@ -477,11 +434,11 @@ export class QBittorrent {
477
434
  options.useAutoTMM = 'false';
478
435
  }
479
436
  for (const [key, value] of Object.entries(options)) {
480
- form.append(key, value);
437
+ form.append(key, `${value}`);
481
438
  }
482
439
  }
483
- const res = await this.request('/torrents/add', 'POST', undefined, form, undefined, undefined, false);
484
- if (res.body === 'Fails.') {
440
+ const res = await this.request('/torrents/add', 'POST', undefined, form, undefined, false);
441
+ if (res === 'Fails.') {
485
442
  throw new Error('Failed to add torrent');
486
443
  }
487
444
  return true;
@@ -490,64 +447,56 @@ export class QBittorrent {
490
447
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#add-trackers-to-torrent}
491
448
  */
492
449
  async addTrackers(hash, urls) {
493
- const params = { hash, urls };
494
- await this.request('/torrents/addTrackers', 'POST', undefined, undefined, params);
450
+ const data = { hash, urls };
451
+ await this.request('/torrents/addTrackers', 'POST', undefined, objToUrlSearchParams(data));
495
452
  return true;
496
453
  }
497
454
  /**
498
455
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#edit-trackers}
499
456
  */
500
457
  async editTrackers(hash, origUrl, newUrl) {
501
- const params = { hash, origUrl, newUrl };
502
- await this.request('/torrents/editTrackers', 'POST', undefined, undefined, params);
458
+ const data = { hash, origUrl, newUrl };
459
+ await this.request('/torrents/editTrackers', 'POST', undefined, objToUrlSearchParams(data));
503
460
  return true;
504
461
  }
505
462
  /**
506
463
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#remove-trackers}
507
464
  */
508
465
  async removeTrackers(hash, urls) {
509
- const params = { hash, urls };
510
- await this.request('/torrents/removeTrackers', 'POST', undefined, undefined, params);
466
+ const data = { hash, urls };
467
+ await this.request('/torrents/removeTrackers', 'POST', undefined, objToUrlSearchParams(data));
511
468
  return true;
512
469
  }
513
470
  /**
514
471
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#increase-torrent-priority}
515
472
  */
516
473
  async queueUp(hashes) {
517
- const params = {
518
- hashes: this._normalizeHashes(hashes),
519
- };
520
- await this.request('/torrents/increasePrio', 'POST', undefined, undefined, params);
474
+ const data = { hashes: normalizeHashes(hashes) };
475
+ await this.request('/torrents/increasePrio', 'POST', undefined, objToUrlSearchParams(data));
521
476
  return true;
522
477
  }
523
478
  /**
524
479
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#decrease-torrent-priority}
525
480
  */
526
481
  async queueDown(hashes) {
527
- const params = {
528
- hashes: this._normalizeHashes(hashes),
529
- };
530
- await this.request('/torrents/decreasePrio', 'POST', undefined, undefined, params);
482
+ const data = { hashes: normalizeHashes(hashes) };
483
+ await this.request('/torrents/decreasePrio', 'POST', undefined, objToUrlSearchParams(data));
531
484
  return true;
532
485
  }
533
486
  /**
534
487
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#maximal-torrent-priority}
535
488
  */
536
489
  async topPriority(hashes) {
537
- const params = {
538
- hashes: this._normalizeHashes(hashes),
539
- };
540
- await this.request('/torrents/topPrio', 'POST', undefined, undefined, params);
490
+ const data = { hashes: normalizeHashes(hashes) };
491
+ await this.request('/torrents/topPrio', 'POST', undefined, objToUrlSearchParams(data));
541
492
  return true;
542
493
  }
543
494
  /**
544
495
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#minimal-torrent-priority}
545
496
  */
546
497
  async bottomPriority(hashes) {
547
- const params = {
548
- hashes: this._normalizeHashes(hashes),
549
- };
550
- await this.request('/torrents/bottomPrio', 'POST', undefined, undefined, params);
498
+ const data = { hashes: normalizeHashes(hashes) };
499
+ await this.request('/torrents/bottomPrio', 'POST', undefined, objToUrlSearchParams(data));
551
500
  return true;
552
501
  }
553
502
  /**
@@ -561,31 +510,31 @@ export class QBittorrent {
561
510
  params.rid = rid;
562
511
  }
563
512
  const res = await this.request('/sync/torrentPeers', 'GET', params);
564
- return res.body;
513
+ return res;
565
514
  }
566
515
  /**
567
516
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#login}
568
517
  */
569
518
  async login() {
570
- const url = urlJoin(this.config.baseUrl, this.config.path, '/auth/login');
571
- const res = await got({
572
- url,
519
+ const url = joinURL(this.config.baseUrl, this.config.path, '/auth/login');
520
+ const res = await ofetch.raw(url, {
573
521
  method: 'POST',
574
- form: {
522
+ headers: {
523
+ 'Content-Type': 'application/x-www-form-urlencoded',
524
+ },
525
+ body: new URLSearchParams({
575
526
  username: this.config.username ?? '',
576
527
  password: this.config.password ?? '',
577
- },
578
- followRedirect: false,
579
- retry: { limit: 0 },
580
- timeout: { request: this.config.timeout },
581
- // allow proxy agent
582
- ...(this.config.agent ? { agent: this.config.agent } : {}),
528
+ }),
529
+ redirect: 'manual',
530
+ retry: false,
531
+ timeout: this.config.timeout,
532
+ // ...(this.config.agent ? { agent: this.config.agent } : {}),
583
533
  });
584
- // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
585
- if (!res.headers['set-cookie'] || !res.headers['set-cookie'].length) {
534
+ if (!res.headers.get('set-cookie')?.length) {
586
535
  throw new Error('Cookie not found. Auth Failed.');
587
536
  }
588
- const cookie = Cookie.parse(res.headers['set-cookie'][0]);
537
+ const cookie = Cookie.parse(res.headers.get('set-cookie'));
589
538
  if (!cookie || cookie.key !== 'SID') {
590
539
  throw new Error('Invalid cookie');
591
540
  }
@@ -599,130 +548,46 @@ export class QBittorrent {
599
548
  return true;
600
549
  }
601
550
  // eslint-disable-next-line max-params
602
- async request(path, method, params = {}, body, form, headers = {}, json = true) {
551
+ async request(path, method, params, body, headers = {}, json = true) {
603
552
  if (!this._sid || !this._exp || this._exp.getTime() < new Date().getTime()) {
604
553
  const authed = await this.login();
605
554
  if (!authed) {
606
555
  throw new Error('Auth Failed');
607
556
  }
608
557
  }
609
- const url = urlJoin(this.config.baseUrl, this.config.path, path);
610
- const res = await got(url, {
611
- isStream: false,
612
- resolveBodyOnly: false,
558
+ const url = joinURL(this.config.baseUrl, this.config.path, path);
559
+ const res = await ofetch(url, {
613
560
  method,
614
561
  headers: {
615
562
  Cookie: `SID=${this._sid ?? ''}`,
616
563
  ...headers,
617
564
  },
618
- retry: { limit: 0 },
619
565
  body,
620
- form,
621
- searchParams: new URLSearchParams(params),
566
+ params,
622
567
  // allow proxy agent
623
- timeout: { request: this.config.timeout },
568
+ retry: 0,
569
+ timeout: this.config.timeout,
624
570
  responseType: json ? 'json' : 'text',
625
- ...(this.config.agent ? { agent: this.config.agent } : {}),
571
+ // @ts-expect-error for some reason agent is not in the type
572
+ agent: this.config.agent,
626
573
  });
627
574
  return res;
628
575
  }
629
- /**
630
- * Normalizes hashes
631
- * @returns hashes as string seperated by `|`
632
- */
633
- _normalizeHashes(hashes) {
634
- if (Array.isArray(hashes)) {
635
- return hashes.join('|');
636
- }
637
- return hashes;
638
- }
639
- _normalizeTorrentData(torrent) {
640
- let state = NormalizedTorrentState.unknown;
641
- let stateMessage = '';
642
- let { eta } = torrent;
643
- /**
644
- * Good references https://github.com/qbittorrent/qBittorrent/blob/master/src/webui/www/private/scripts/dynamicTable.js#L933
645
- * https://github.com/Radarr/Radarr/blob/develop/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs#L242
646
- */
647
- switch (torrent.state) {
648
- case TorrentState.Error:
649
- state = NormalizedTorrentState.warning;
650
- stateMessage = 'qBittorrent is reporting an error';
651
- break;
652
- case TorrentState.PausedDL:
653
- state = NormalizedTorrentState.paused;
654
- break;
655
- case TorrentState.QueuedDL: // queuing is enabled and torrent is queued for download
656
- case TorrentState.CheckingDL: // same as checkingUP, but torrent has NOT finished downloading
657
- case TorrentState.CheckingUP: // torrent has finished downloading and is being checked. Set when `recheck torrent on completion` is enabled. In the event the check fails we shouldn't treat it as completed.
658
- state = NormalizedTorrentState.queued;
659
- break;
660
- case TorrentState.MetaDL: // Metadl could be an error if DHT is not enabled
661
- case TorrentState.ForcedDL: // torrent is being downloaded, and was forced started
662
- case TorrentState.ForcedMetaDL: // torrent metadata is being forcibly downloaded
663
- case TorrentState.Downloading: // torrent is being downloaded and data is being transferred
664
- state = NormalizedTorrentState.downloading;
665
- break;
666
- case TorrentState.Allocating:
667
- // state = 'stalledDL';
668
- state = NormalizedTorrentState.queued;
669
- break;
670
- case TorrentState.StalledDL:
671
- state = NormalizedTorrentState.warning;
672
- stateMessage = 'The download is stalled with no connection';
673
- break;
674
- case TorrentState.PausedUP: // torrent is paused and has finished downloading:
675
- case TorrentState.Uploading: // torrent is being seeded and data is being transferred
676
- case TorrentState.StalledUP: // torrent is being seeded, but no connection were made
677
- case TorrentState.QueuedUP: // queuing is enabled and torrent is queued for upload
678
- case TorrentState.ForcedUP: // torrent has finished downloading and is being forcibly seeded
679
- // state = 'completed';
680
- state = NormalizedTorrentState.seeding;
681
- eta = 0; // qBittorrent sends eta=8640000 for completed torrents
682
- break;
683
- case TorrentState.Moving: // torrent is being moved from a folder
684
- case TorrentState.QueuedForChecking:
685
- case TorrentState.CheckingResumeData:
686
- state = NormalizedTorrentState.checking;
687
- break;
688
- case TorrentState.Unknown:
689
- state = NormalizedTorrentState.error;
690
- break;
691
- case TorrentState.MissingFiles:
692
- state = NormalizedTorrentState.error;
693
- stateMessage = 'The download is missing files';
694
- break;
695
- default:
696
- break;
697
- }
698
- const isCompleted = torrent.progress === 1;
699
- const result = {
700
- id: torrent.hash,
701
- name: torrent.name,
702
- stateMessage,
703
- state,
704
- eta,
705
- dateAdded: new Date(torrent.added_on * 1000).toISOString(),
706
- isCompleted,
707
- progress: torrent.progress,
708
- label: torrent.category,
709
- tags: torrent.tags.split(', '),
710
- dateCompleted: new Date(torrent.completion_on * 1000).toISOString(),
711
- savePath: torrent.save_path,
712
- uploadSpeed: torrent.upspeed,
713
- downloadSpeed: torrent.dlspeed,
714
- queuePosition: torrent.priority,
715
- connectedPeers: torrent.num_leechs,
716
- connectedSeeds: torrent.num_seeds,
717
- totalPeers: torrent.num_incomplete,
718
- totalSeeds: torrent.num_complete,
719
- totalSelected: torrent.size,
720
- totalSize: torrent.total_size,
721
- totalUploaded: torrent.uploaded,
722
- totalDownloaded: torrent.downloaded,
723
- ratio: torrent.ratio,
724
- raw: torrent,
725
- };
726
- return result;
576
+ }
577
+ /**
578
+ * Normalizes hashes
579
+ * @returns hashes as string seperated by `|`
580
+ */
581
+ function normalizeHashes(hashes) {
582
+ if (Array.isArray(hashes)) {
583
+ return hashes.join('|');
584
+ }
585
+ return hashes;
586
+ }
587
+ function objToUrlSearchParams(obj) {
588
+ const params = new URLSearchParams();
589
+ for (const [key, value] of Object.entries(obj)) {
590
+ params.append(key, value.toString());
727
591
  }
592
+ return params;
728
593
  }
package/dist/src/types.js CHANGED
@@ -81,7 +81,7 @@ export var TorrentState;
81
81
  * Torrent data files is missing
82
82
  */
83
83
  TorrentState["MissingFiles"] = "missingFiles";
84
- })(TorrentState = TorrentState || (TorrentState = {}));
84
+ })(TorrentState || (TorrentState = {}));
85
85
  export var TorrentTrackerStatus;
86
86
  (function (TorrentTrackerStatus) {
87
87
  /**
@@ -104,7 +104,7 @@ export var TorrentTrackerStatus;
104
104
  * Tracker has been contacted, but it is not working (or doesn't send proper replies)
105
105
  */
106
106
  TorrentTrackerStatus[TorrentTrackerStatus["Errored"] = 4] = "Errored";
107
- })(TorrentTrackerStatus = TorrentTrackerStatus || (TorrentTrackerStatus = {}));
107
+ })(TorrentTrackerStatus || (TorrentTrackerStatus = {}));
108
108
  export var TorrentFilePriority;
109
109
  (function (TorrentFilePriority) {
110
110
  /**
@@ -123,7 +123,7 @@ export var TorrentFilePriority;
123
123
  * Maximal priority
124
124
  */
125
125
  TorrentFilePriority[TorrentFilePriority["MaxPriority"] = 7] = "MaxPriority";
126
- })(TorrentFilePriority = TorrentFilePriority || (TorrentFilePriority = {}));
126
+ })(TorrentFilePriority || (TorrentFilePriority = {}));
127
127
  export var TorrentPieceState;
128
128
  (function (TorrentPieceState) {
129
129
  /**
@@ -138,4 +138,4 @@ export var TorrentPieceState;
138
138
  * Already downloaded
139
139
  */
140
140
  TorrentPieceState[TorrentPieceState["Downloaded"] = 2] = "Downloaded";
141
- })(TorrentPieceState = TorrentPieceState || (TorrentPieceState = {}));
141
+ })(TorrentPieceState || (TorrentPieceState = {}));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctrl/qbittorrent",
3
- "version": "6.1.0",
3
+ "version": "7.0.0",
4
4
  "description": "TypeScript api wrapper for qbittorrent using got",
5
5
  "author": "Scott Cooper <scttcper@gmail.com>",
6
6
  "license": "MIT",
@@ -14,7 +14,8 @@
14
14
  ],
15
15
  "sideEffects": false,
16
16
  "publishConfig": {
17
- "access": "public"
17
+ "access": "public",
18
+ "provenance": true
18
19
  },
19
20
  "keywords": [
20
21
  "typescript",
@@ -31,25 +32,24 @@
31
32
  "test:ci": "vitest run --coverage --reporter=default --reporter=junit --outputFile=./junit.xml"
32
33
  },
33
34
  "dependencies": {
34
- "@ctrl/magnet-link": "^3.1.1",
35
- "@ctrl/shared-torrent": "^4.3.2",
36
- "@ctrl/torrent-file": "^2.0.2",
37
- "@ctrl/url-join": "^2.0.2",
38
- "formdata-node": "^5.0.0",
39
- "got": "^12.6.0",
40
- "tough-cookie": "^4.1.2"
35
+ "@ctrl/magnet-link": "^3.1.2",
36
+ "@ctrl/shared-torrent": "^5.0.0",
37
+ "@ctrl/torrent-file": "^3.0.0",
38
+ "node-fetch-native": "^1.4.0",
39
+ "ofetch": "^1.3.3",
40
+ "tough-cookie": "^4.1.3",
41
+ "ufo": "^1.3.0"
41
42
  },
42
43
  "devDependencies": {
43
- "@ctrl/eslint-config": "3.7.0",
44
- "@sindresorhus/tsconfig": "3.0.1",
45
- "@types/node": "18.16.3",
46
- "@types/tough-cookie": "4.0.2",
47
- "@vitest/coverage-c8": "0.31.0",
48
- "c8": "7.13.0",
44
+ "@ctrl/eslint-config": "4.0.7",
45
+ "@sindresorhus/tsconfig": "4.0.0",
46
+ "@types/node": "20.6.5",
47
+ "@types/tough-cookie": "4.0.3",
48
+ "@vitest/coverage-v8": "0.34.5",
49
49
  "p-wait-for": "5.0.2",
50
- "typedoc": "0.24.6",
51
- "typescript": "5.0.4",
52
- "vitest": "0.31.0"
50
+ "typedoc": "0.25.1",
51
+ "typescript": "5.2.2",
52
+ "vitest": "0.34.5"
53
53
  },
54
54
  "release": {
55
55
  "branches": [
@@ -57,6 +57,6 @@
57
57
  ]
58
58
  },
59
59
  "engines": {
60
- "node": ">=14.16"
60
+ "node": ">=18"
61
61
  }
62
62
  }