@ctrl/qbittorrent 6.1.0 → 7.0.1

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,6 +1,6 @@
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
- > TypeScript api wrapper for [qBittorrent](https://www.qbittorrent.org/) using [got](https://github.com/sindresorhus/got)
3
+ > TypeScript api wrapper for [qBittorrent](https://www.qbittorrent.org/) using [ofetch](https://github.com/unjs/ofetch)
4
4
 
5
5
  ### Install
6
6
 
@@ -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,43 +181,41 @@ 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}
209
188
  */
210
189
  async setTorrentLocation(hashes, location) {
211
- await this.request('/torrents/setLocation', 'POST', undefined, undefined, {
190
+ const data = {
212
191
  location,
213
- hashes: this._normalizeHashes(hashes),
214
- });
192
+ hashes: normalizeHashes(hashes),
193
+ };
194
+ await this.request('/torrents/setLocation', 'POST', undefined, objToUrlSearchParams(data));
215
195
  return true;
216
196
  }
217
197
  /**
218
198
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#set-torrent-name}
219
199
  */
220
200
  async setTorrentName(hash, name) {
221
- await this.request('/torrents/rename', 'POST', undefined, undefined, {
222
- hash,
223
- name,
224
- });
201
+ const data = { hash, name };
202
+ await this.request('/torrents/rename', 'POST', undefined, objToUrlSearchParams(data));
225
203
  return true;
226
204
  }
227
205
  /**
228
206
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-all-tags}
229
207
  */
230
208
  async getTags() {
231
- const res = await this.request('/torrents/tags', 'get');
232
- return res.body;
209
+ const res = await this.request('/torrents/tags', 'GET');
210
+ return res;
233
211
  }
234
212
  /**
235
213
  * @param tags comma separated list
236
214
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#create-tags}
237
215
  */
238
216
  async createTags(tags) {
239
- await this.request('/torrents/createTags', 'POST', undefined, undefined, {
240
- tags,
241
- }, undefined, false);
217
+ const data = { tags };
218
+ await this.request('/torrents/createTags', 'POST', undefined, objToUrlSearchParams(data), undefined, false);
242
219
  return true;
243
220
  }
244
221
  /**
@@ -246,53 +223,47 @@ export class QBittorrent {
246
223
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#delete-tags}
247
224
  */
248
225
  async deleteTags(tags) {
249
- await this.request('/torrents/deleteTags', 'POST', undefined, undefined, { tags }, undefined, false);
226
+ const data = { tags };
227
+ await this.request('/torrents/deleteTags', 'POST', undefined, objToUrlSearchParams(data), undefined, false);
250
228
  return true;
251
229
  }
252
230
  /**
253
231
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-all-categories}
254
232
  */
255
233
  async getCategories() {
256
- const res = await this.request('/torrents/categories', 'get');
257
- return res.body;
234
+ const res = await this.request('/torrents/categories', 'GET');
235
+ return res;
258
236
  }
259
237
  /**
260
238
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#add-new-category}
261
239
  */
262
240
  async createCategory(category, savePath = '') {
263
- await this.request('/torrents/createCategory', 'POST', undefined, undefined, {
264
- category,
265
- savePath,
266
- }, undefined, false);
241
+ const data = { category, savePath };
242
+ await this.request('/torrents/createCategory', 'POST', undefined, objToUrlSearchParams(data), undefined, false);
267
243
  return true;
268
244
  }
269
245
  /**
270
246
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#edit-category}
271
247
  */
272
248
  async editCategory(category, savePath = '') {
273
- await this.request('/torrents/editCategory', 'POST', undefined, undefined, {
274
- category,
275
- savePath,
276
- }, undefined, false);
249
+ const data = { category, savePath };
250
+ await this.request('/torrents/editCategory', 'POST', undefined, objToUrlSearchParams(data), undefined, false);
277
251
  return true;
278
252
  }
279
253
  /**
280
254
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#remove-categories}
281
255
  */
282
256
  async removeCategory(categories) {
283
- await this.request('/torrents/removeCategories', 'POST', undefined, undefined, {
284
- categories,
285
- }, undefined, false);
257
+ const data = { categories };
258
+ await this.request('/torrents/removeCategories', 'POST', undefined, objToUrlSearchParams(data), undefined, false);
286
259
  return true;
287
260
  }
288
261
  /**
289
262
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#add-torrent-tags}
290
263
  */
291
264
  async addTorrentTags(hashes, tags) {
292
- await this.request('/torrents/addTags', 'POST', undefined, undefined, {
293
- hashes: this._normalizeHashes(hashes),
294
- tags,
295
- }, undefined, false);
265
+ const data = { hashes: normalizeHashes(hashes), tags };
266
+ await this.request('/torrents/addTags', 'POST', undefined, objToUrlSearchParams(data), undefined, false);
296
267
  return true;
297
268
  }
298
269
  /**
@@ -300,11 +271,11 @@ export class QBittorrent {
300
271
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#remove-torrent-tags}
301
272
  */
302
273
  async removeTorrentTags(hashes, tags) {
303
- const form = { hashes: this._normalizeHashes(hashes) };
274
+ const data = { hashes: normalizeHashes(hashes) };
304
275
  if (tags) {
305
- form.tags = tags;
276
+ data.tags = tags;
306
277
  }
307
- await this.request('/torrents/removeTags', 'POST', undefined, undefined, form, undefined, false);
278
+ await this.request('/torrents/removeTags', 'POST', undefined, objToUrlSearchParams(data), undefined, false);
308
279
  return true;
309
280
  }
310
281
  /**
@@ -317,61 +288,54 @@ export class QBittorrent {
317
288
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#set-torrent-category}
318
289
  */
319
290
  async setTorrentCategory(hashes, category = '') {
320
- await this.request('/torrents/setCategory', 'POST', undefined, undefined, {
321
- hashes: this._normalizeHashes(hashes),
291
+ const data = {
292
+ hashes: normalizeHashes(hashes),
322
293
  category,
323
- });
294
+ };
295
+ await this.request('/torrents/setCategory', 'POST', undefined, objToUrlSearchParams(data));
324
296
  return true;
325
297
  }
326
298
  /**
327
299
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#pause-torrents}
328
300
  */
329
301
  async pauseTorrent(hashes) {
330
- const params = {
331
- hashes: this._normalizeHashes(hashes),
332
- };
333
- await this.request('/torrents/pause', 'POST', undefined, undefined, params);
302
+ const data = { hashes: normalizeHashes(hashes) };
303
+ await this.request('/torrents/pause', 'POST', undefined, objToUrlSearchParams(data));
334
304
  return true;
335
305
  }
336
306
  /**
337
307
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#resume-torrents}
338
308
  */
339
309
  async resumeTorrent(hashes) {
340
- const params = {
341
- hashes: this._normalizeHashes(hashes),
342
- };
343
- await this.request('/torrents/resume', 'POST', undefined, undefined, params);
310
+ const data = { hashes: normalizeHashes(hashes) };
311
+ await this.request('/torrents/resume', 'POST', undefined, objToUrlSearchParams(data));
344
312
  return true;
345
313
  }
346
314
  /**
347
315
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#delete-torrents}
348
316
  */
349
317
  async removeTorrent(hashes, deleteFiles = true) {
350
- const params = {
351
- hashes: this._normalizeHashes(hashes),
318
+ const data = {
319
+ hashes: normalizeHashes(hashes),
352
320
  deleteFiles,
353
321
  };
354
- await this.request('/torrents/delete', 'POST', undefined, undefined, params);
322
+ await this.request('/torrents/delete', 'POST', undefined, objToUrlSearchParams(data));
355
323
  return true;
356
324
  }
357
325
  /**
358
326
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#recheck-torrents}
359
327
  */
360
328
  async recheckTorrent(hashes) {
361
- const params = {
362
- hashes: this._normalizeHashes(hashes),
363
- };
364
- await this.request('/torrents/recheck', 'POST', undefined, undefined, params);
329
+ const data = { hashes: normalizeHashes(hashes) };
330
+ await this.request('/torrents/recheck', 'POST', undefined, objToUrlSearchParams(data));
365
331
  return true;
366
332
  }
367
333
  /**
368
334
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#reannounce-torrents}
369
335
  */
370
336
  async reannounceTorrent(hashes) {
371
- const params = {
372
- hashes: this._normalizeHashes(hashes),
373
- };
374
- await this.request('/torrents/reannounce', 'POST', undefined, undefined, params);
337
+ const data = { hashes: normalizeHashes(hashes) };
338
+ await this.request('/torrents/reannounce', 'POST', undefined, objToUrlSearchParams(data));
375
339
  return true;
376
340
  }
377
341
  async addTorrent(torrent, options = {}) {
@@ -382,13 +346,7 @@ export class QBittorrent {
382
346
  }
383
347
  const type = { type: 'application/x-bittorrent' };
384
348
  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
- }
349
+ form.set('file', new File([Buffer.from(torrent, 'base64')], 'file.torrent', type));
392
350
  }
393
351
  else {
394
352
  const file = new File([torrent], options.filename ?? 'torrent', type);
@@ -403,11 +361,11 @@ export class QBittorrent {
403
361
  options.useAutoTMM = 'false';
404
362
  }
405
363
  for (const [key, value] of Object.entries(options)) {
406
- form.append(key, value);
364
+ form.append(key, `${value}`);
407
365
  }
408
366
  }
409
- const res = await this.request('/torrents/add', 'POST', undefined, form, undefined, undefined, false);
410
- if (res.body === 'Fails.') {
367
+ const res = await this.request('/torrents/add', 'POST', undefined, form, undefined, false);
368
+ if (res === 'Fails.') {
411
369
  throw new Error('Failed to add torrent');
412
370
  }
413
371
  return true;
@@ -445,9 +403,9 @@ export class QBittorrent {
445
403
  async renameFile(hash, id, name) {
446
404
  const form = new FormData();
447
405
  form.append('hash', hash);
448
- form.append('id', id);
406
+ form.append('id', id.toString());
449
407
  form.append('name', name);
450
- await this.request('/torrents/renameFile', 'POST', undefined, form, undefined, false);
408
+ await this.request('/torrents/renameFile', 'POST', undefined, undefined, form, false);
451
409
  return true;
452
410
  }
453
411
  /**
@@ -458,7 +416,7 @@ export class QBittorrent {
458
416
  form.append('hash', hash);
459
417
  form.append('oldPath', oldPath);
460
418
  form.append('newPath', newPath);
461
- await this.request('/torrents/renameFolder', 'POST', undefined, form, undefined, false);
419
+ await this.request('/torrents/renameFolder', 'POST', undefined, undefined, form, false);
462
420
  return true;
463
421
  }
464
422
  /**
@@ -477,11 +435,11 @@ export class QBittorrent {
477
435
  options.useAutoTMM = 'false';
478
436
  }
479
437
  for (const [key, value] of Object.entries(options)) {
480
- form.append(key, value);
438
+ form.append(key, `${value}`);
481
439
  }
482
440
  }
483
- const res = await this.request('/torrents/add', 'POST', undefined, form, undefined, undefined, false);
484
- if (res.body === 'Fails.') {
441
+ const res = await this.request('/torrents/add', 'POST', undefined, form, undefined, false);
442
+ if (res === 'Fails.') {
485
443
  throw new Error('Failed to add torrent');
486
444
  }
487
445
  return true;
@@ -490,64 +448,56 @@ export class QBittorrent {
490
448
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#add-trackers-to-torrent}
491
449
  */
492
450
  async addTrackers(hash, urls) {
493
- const params = { hash, urls };
494
- await this.request('/torrents/addTrackers', 'POST', undefined, undefined, params);
451
+ const data = { hash, urls };
452
+ await this.request('/torrents/addTrackers', 'POST', undefined, objToUrlSearchParams(data));
495
453
  return true;
496
454
  }
497
455
  /**
498
456
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#edit-trackers}
499
457
  */
500
458
  async editTrackers(hash, origUrl, newUrl) {
501
- const params = { hash, origUrl, newUrl };
502
- await this.request('/torrents/editTrackers', 'POST', undefined, undefined, params);
459
+ const data = { hash, origUrl, newUrl };
460
+ await this.request('/torrents/editTrackers', 'POST', undefined, objToUrlSearchParams(data));
503
461
  return true;
504
462
  }
505
463
  /**
506
464
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#remove-trackers}
507
465
  */
508
466
  async removeTrackers(hash, urls) {
509
- const params = { hash, urls };
510
- await this.request('/torrents/removeTrackers', 'POST', undefined, undefined, params);
467
+ const data = { hash, urls };
468
+ await this.request('/torrents/removeTrackers', 'POST', undefined, objToUrlSearchParams(data));
511
469
  return true;
512
470
  }
513
471
  /**
514
472
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#increase-torrent-priority}
515
473
  */
516
474
  async queueUp(hashes) {
517
- const params = {
518
- hashes: this._normalizeHashes(hashes),
519
- };
520
- await this.request('/torrents/increasePrio', 'POST', undefined, undefined, params);
475
+ const data = { hashes: normalizeHashes(hashes) };
476
+ await this.request('/torrents/increasePrio', 'POST', undefined, objToUrlSearchParams(data));
521
477
  return true;
522
478
  }
523
479
  /**
524
480
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#decrease-torrent-priority}
525
481
  */
526
482
  async queueDown(hashes) {
527
- const params = {
528
- hashes: this._normalizeHashes(hashes),
529
- };
530
- await this.request('/torrents/decreasePrio', 'POST', undefined, undefined, params);
483
+ const data = { hashes: normalizeHashes(hashes) };
484
+ await this.request('/torrents/decreasePrio', 'POST', undefined, objToUrlSearchParams(data));
531
485
  return true;
532
486
  }
533
487
  /**
534
488
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#maximal-torrent-priority}
535
489
  */
536
490
  async topPriority(hashes) {
537
- const params = {
538
- hashes: this._normalizeHashes(hashes),
539
- };
540
- await this.request('/torrents/topPrio', 'POST', undefined, undefined, params);
491
+ const data = { hashes: normalizeHashes(hashes) };
492
+ await this.request('/torrents/topPrio', 'POST', undefined, objToUrlSearchParams(data));
541
493
  return true;
542
494
  }
543
495
  /**
544
496
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#minimal-torrent-priority}
545
497
  */
546
498
  async bottomPriority(hashes) {
547
- const params = {
548
- hashes: this._normalizeHashes(hashes),
549
- };
550
- await this.request('/torrents/bottomPrio', 'POST', undefined, undefined, params);
499
+ const data = { hashes: normalizeHashes(hashes) };
500
+ await this.request('/torrents/bottomPrio', 'POST', undefined, objToUrlSearchParams(data));
551
501
  return true;
552
502
  }
553
503
  /**
@@ -561,31 +511,31 @@ export class QBittorrent {
561
511
  params.rid = rid;
562
512
  }
563
513
  const res = await this.request('/sync/torrentPeers', 'GET', params);
564
- return res.body;
514
+ return res;
565
515
  }
566
516
  /**
567
517
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#login}
568
518
  */
569
519
  async login() {
570
- const url = urlJoin(this.config.baseUrl, this.config.path, '/auth/login');
571
- const res = await got({
572
- url,
520
+ const url = joinURL(this.config.baseUrl, this.config.path, '/auth/login');
521
+ const res = await ofetch.raw(url, {
573
522
  method: 'POST',
574
- form: {
523
+ headers: {
524
+ 'Content-Type': 'application/x-www-form-urlencoded',
525
+ },
526
+ body: new URLSearchParams({
575
527
  username: this.config.username ?? '',
576
528
  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 } : {}),
529
+ }),
530
+ redirect: 'manual',
531
+ retry: false,
532
+ timeout: this.config.timeout,
533
+ // ...(this.config.agent ? { agent: this.config.agent } : {}),
583
534
  });
584
- // eslint-disable-next-line @typescript-eslint/prefer-optional-chain
585
- if (!res.headers['set-cookie'] || !res.headers['set-cookie'].length) {
535
+ if (!res.headers.get('set-cookie')?.length) {
586
536
  throw new Error('Cookie not found. Auth Failed.');
587
537
  }
588
- const cookie = Cookie.parse(res.headers['set-cookie'][0]);
538
+ const cookie = Cookie.parse(res.headers.get('set-cookie'));
589
539
  if (!cookie || cookie.key !== 'SID') {
590
540
  throw new Error('Invalid cookie');
591
541
  }
@@ -599,130 +549,46 @@ export class QBittorrent {
599
549
  return true;
600
550
  }
601
551
  // eslint-disable-next-line max-params
602
- async request(path, method, params = {}, body, form, headers = {}, json = true) {
552
+ async request(path, method, params, body, headers = {}, json = true) {
603
553
  if (!this._sid || !this._exp || this._exp.getTime() < new Date().getTime()) {
604
554
  const authed = await this.login();
605
555
  if (!authed) {
606
556
  throw new Error('Auth Failed');
607
557
  }
608
558
  }
609
- const url = urlJoin(this.config.baseUrl, this.config.path, path);
610
- const res = await got(url, {
611
- isStream: false,
612
- resolveBodyOnly: false,
559
+ const url = joinURL(this.config.baseUrl, this.config.path, path);
560
+ const res = await ofetch(url, {
613
561
  method,
614
562
  headers: {
615
563
  Cookie: `SID=${this._sid ?? ''}`,
616
564
  ...headers,
617
565
  },
618
- retry: { limit: 0 },
619
566
  body,
620
- form,
621
- searchParams: new URLSearchParams(params),
567
+ params,
622
568
  // allow proxy agent
623
- timeout: { request: this.config.timeout },
569
+ retry: 0,
570
+ timeout: this.config.timeout,
624
571
  responseType: json ? 'json' : 'text',
625
- ...(this.config.agent ? { agent: this.config.agent } : {}),
572
+ // @ts-expect-error for some reason agent is not in the type
573
+ agent: this.config.agent,
626
574
  });
627
575
  return res;
628
576
  }
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;
577
+ }
578
+ /**
579
+ * Normalizes hashes
580
+ * @returns hashes as string seperated by `|`
581
+ */
582
+ function normalizeHashes(hashes) {
583
+ if (Array.isArray(hashes)) {
584
+ return hashes.join('|');
585
+ }
586
+ return hashes;
587
+ }
588
+ function objToUrlSearchParams(obj) {
589
+ const params = new URLSearchParams();
590
+ for (const [key, value] of Object.entries(obj)) {
591
+ params.append(key, value.toString());
727
592
  }
593
+ return params;
728
594
  }
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.1",
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.1",
39
+ "ofetch": "^1.3.3",
40
+ "tough-cookie": "^4.1.3",
41
+ "ufo": "^1.3.1"
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.9",
45
+ "@sindresorhus/tsconfig": "5.0.0",
46
+ "@types/node": "20.8.10",
47
+ "@types/tough-cookie": "4.0.4",
48
+ "@vitest/coverage-v8": "0.34.6",
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.3",
51
+ "typescript": "5.2.2",
52
+ "vitest": "0.34.6"
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
  }