@ctrl/qbittorrent 9.1.0 → 9.3.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
@@ -98,6 +98,15 @@ const result = await client.normalizedAddTorrent(fs.readFileSync(torrentFile), {
98
98
  console.log(result);
99
99
  ```
100
100
 
101
+ ##### export and create from state
102
+
103
+ If you're shutting down the server often (serverless?) you can export the state
104
+
105
+ ```ts
106
+ const state = client.exportState()
107
+ const client = QBittorrent.createFromState(config, state);
108
+ ```
109
+
101
110
  ### See Also
102
111
 
103
112
  All of the following npm modules provide the same normalized functions along with supporting the unique apis for each client.
@@ -1,16 +1,34 @@
1
- import type { AddTorrentOptions as NormalizedAddTorrentOptions, AllClientData, NormalizedTorrent, TorrentClient, TorrentSettings } from '@ctrl/shared-torrent';
2
- import type { AddMagnetOptions, AddTorrentOptions, BuildInfo, Preferences, Torrent, TorrentCategories, TorrentFile, TorrentFilePriority, TorrentFilters, TorrentPeersResponse, TorrentPieceState, TorrentProperties, TorrentTrackers, WebSeed } from './types.js';
1
+ import type { Jsonify } from 'type-fest';
2
+ import type { AddTorrentOptions as NormalizedAddTorrentOptions, AllClientData, NormalizedTorrent, TorrentClient, TorrentClientConfig, TorrentClientState } from '@ctrl/shared-torrent';
3
+ import type { AddMagnetOptions, AddTorrentOptions, BuildInfo, DownloadSpeed, Preferences, Torrent, TorrentCategories, TorrentFile, TorrentFilePriority, TorrentFilters, TorrentPeersResponse, TorrentPieceState, TorrentProperties, TorrentTrackers, UploadSpeed, WebSeed } from './types.js';
4
+ interface QBittorrentState extends TorrentClientState {
5
+ auth?: {
6
+ /**
7
+ * auth cookie
8
+ */
9
+ sid: string;
10
+ /**
11
+ * cookie expiration
12
+ */
13
+ expires: Date;
14
+ };
15
+ version?: {
16
+ version: string;
17
+ isVersion5OrHigher: boolean;
18
+ };
19
+ }
3
20
  export declare class QBittorrent implements TorrentClient {
4
- config: TorrentSettings;
5
21
  /**
6
- * auth cookie
22
+ * Create a new QBittorrent client from a state
7
23
  */
8
- private _sid?;
24
+ static createFromState(config: Readonly<TorrentClientConfig>, state: Readonly<Jsonify<QBittorrentState>>): QBittorrent;
25
+ config: TorrentClientConfig;
26
+ state: QBittorrentState;
27
+ constructor(options?: Partial<TorrentClientConfig>);
9
28
  /**
10
- * cookie expiration
29
+ * Export the state of the client as JSON
11
30
  */
12
- private _exp?;
13
- constructor(options?: Partial<TorrentSettings>);
31
+ exportState(): Jsonify<QBittorrentState>;
14
32
  /**
15
33
  * @deprecated
16
34
  */
@@ -30,6 +48,22 @@ export declare class QBittorrent implements TorrentClient {
30
48
  */
31
49
  getBuildInfo(): Promise<BuildInfo>;
32
50
  getTorrent(hash: string): Promise<NormalizedTorrent>;
51
+ /**
52
+ * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-download-limit}
53
+ */
54
+ getTorrentDownloadLimit(hash: string | string[]): Promise<DownloadSpeed>;
55
+ /**
56
+ * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#set-torrent-download-limit}
57
+ */
58
+ setTorrentDownloadLimit(hash: string | string[], limitBytesPerSecond: number): Promise<boolean>;
59
+ /**
60
+ * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-upload-limit}
61
+ */
62
+ getTorrentUploadLimit(hash: string | string[]): Promise<UploadSpeed>;
63
+ /**
64
+ * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#set-torrent-upload-limit}
65
+ */
66
+ setTorrentUploadLimit(hash: string | string[], limitBytesPerSecond: number): Promise<boolean>;
33
67
  /**
34
68
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-application-preferences}
35
69
  */
@@ -212,4 +246,6 @@ export declare class QBittorrent implements TorrentClient {
212
246
  login(): Promise<boolean>;
213
247
  logout(): boolean;
214
248
  request<T>(path: string, method: 'GET' | 'POST', params?: Record<string, string | number>, body?: URLSearchParams | FormData, headers?: Record<string, string>, isJson?: boolean): Promise<T>;
249
+ private checkVersion;
215
250
  }
251
+ export {};
@@ -14,18 +14,28 @@ const defaults = {
14
14
  timeout: 5000,
15
15
  };
16
16
  export class QBittorrent {
17
- config;
18
- /**
19
- * auth cookie
20
- */
21
- _sid;
22
17
  /**
23
- * cookie expiration
18
+ * Create a new QBittorrent client from a state
24
19
  */
25
- _exp;
20
+ static createFromState(config, state) {
21
+ const client = new QBittorrent(config);
22
+ client.state = {
23
+ ...state,
24
+ auth: state.auth ? { ...state.auth, expires: new Date(state.auth.expires) } : undefined,
25
+ };
26
+ return client;
27
+ }
28
+ config;
29
+ state = {};
26
30
  constructor(options = {}) {
27
31
  this.config = { ...defaults, ...options };
28
32
  }
33
+ /**
34
+ * Export the state of the client as JSON
35
+ */
36
+ exportState() {
37
+ return JSON.parse(JSON.stringify(this.state));
38
+ }
29
39
  /**
30
40
  * @deprecated
31
41
  */
@@ -66,6 +76,46 @@ export class QBittorrent {
66
76
  }
67
77
  return normalizeTorrentData(torrentData);
68
78
  }
79
+ /**
80
+ * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-download-limit}
81
+ */
82
+ async getTorrentDownloadLimit(hash) {
83
+ const downloadLimit = await this.request('/torrents/downloadLimit', 'POST', undefined, objToUrlSearchParams({
84
+ hashes: normalizeHashes(hash),
85
+ }), undefined);
86
+ return downloadLimit;
87
+ }
88
+ /**
89
+ * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#set-torrent-download-limit}
90
+ */
91
+ async setTorrentDownloadLimit(hash, limitBytesPerSecond) {
92
+ const data = {
93
+ limit: limitBytesPerSecond.toString(),
94
+ hashes: normalizeHashes(hash),
95
+ };
96
+ await this.request('/torrents/setDownloadLimit', 'POST', undefined, objToUrlSearchParams(data));
97
+ return true;
98
+ }
99
+ /**
100
+ * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-upload-limit}
101
+ */
102
+ async getTorrentUploadLimit(hash) {
103
+ const UploadLimit = await this.request('/torrents/uploadLimit', 'POST', undefined, objToUrlSearchParams({
104
+ hashes: normalizeHashes(hash),
105
+ }), undefined);
106
+ return UploadLimit;
107
+ }
108
+ /**
109
+ * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#set-torrent-upload-limit}
110
+ */
111
+ async setTorrentUploadLimit(hash, limitBytesPerSecond) {
112
+ const data = {
113
+ limit: limitBytesPerSecond.toString(),
114
+ hashes: normalizeHashes(hash),
115
+ };
116
+ await this.request('/torrents/setUploadLimit', 'POST', undefined, objToUrlSearchParams(data));
117
+ return true;
118
+ }
69
119
  /**
70
120
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-application-preferences}
71
121
  */
@@ -307,16 +357,20 @@ export class QBittorrent {
307
357
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#pause-torrents}
308
358
  */
309
359
  async pauseTorrent(hashes) {
360
+ const endpoint = this.state.version?.isVersion5OrHigher ? '/torrents/stop' : '/torrents/pause';
310
361
  const data = { hashes: normalizeHashes(hashes) };
311
- await this.request('/torrents/pause', 'POST', undefined, objToUrlSearchParams(data));
362
+ await this.request(endpoint, 'POST', undefined, objToUrlSearchParams(data));
312
363
  return true;
313
364
  }
314
365
  /**
315
366
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#resume-torrents}
316
367
  */
317
368
  async resumeTorrent(hashes) {
369
+ const endpoint = this.state.version?.isVersion5OrHigher
370
+ ? '/torrents/start'
371
+ : '/torrents/resume';
318
372
  const data = { hashes: normalizeHashes(hashes) };
319
- await this.request('/torrents/resume', 'POST', undefined, objToUrlSearchParams(data));
373
+ await this.request(endpoint, 'POST', undefined, objToUrlSearchParams(data));
320
374
  return true;
321
375
  }
322
376
  /**
@@ -362,6 +416,11 @@ export class QBittorrent {
362
416
  form.set('file', file);
363
417
  }
364
418
  if (options) {
419
+ // Handle version-specific paused/stopped parameter
420
+ if (this.state.version?.isVersion5OrHigher && 'paused' in options) {
421
+ form.append('stopped', options.paused);
422
+ delete options.paused;
423
+ }
365
424
  // disable savepath when autoTMM is defined
366
425
  if (options.useAutoTMM === 'true') {
367
426
  options.savepath = '';
@@ -436,6 +495,11 @@ export class QBittorrent {
436
495
  const form = new FormData();
437
496
  form.append('urls', urls);
438
497
  if (options) {
498
+ // Handle version-specific paused/stopped parameter
499
+ if (this.state.version?.isVersion5OrHigher && 'paused' in options) {
500
+ form.append('stopped', options.paused);
501
+ delete options.paused;
502
+ }
439
503
  // disable savepath when autoTMM is defined
440
504
  if (options.useAutoTMM === 'true') {
441
505
  options.savepath = '';
@@ -526,7 +590,7 @@ export class QBittorrent {
526
590
  * {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#login}
527
591
  */
528
592
  async login() {
529
- const url = joinURL(this.config.baseUrl, this.config.path, '/auth/login');
593
+ const url = joinURL(this.config.baseUrl, this.config.path ?? '', '/auth/login');
530
594
  const res = await ofetch.raw(url, {
531
595
  method: 'POST',
532
596
  headers: {
@@ -544,41 +608,43 @@ export class QBittorrent {
544
608
  if (!res.headers.get('set-cookie')?.length) {
545
609
  throw new Error('Cookie not found. Auth Failed.');
546
610
  }
547
- const cookie = cookieParse(res.headers.get('set-cookie'));
611
+ const cookie = cookieParse(res.headers.get('set-cookie') ?? '');
548
612
  if (!cookie.SID) {
549
613
  throw new Error('Invalid cookie');
550
614
  }
551
- this._sid = cookie.SID;
552
- // Not sure if it might be lowercase
553
615
  const expires = cookie.Expires ?? cookie.expires;
554
- // Assumed to be in seconds
555
616
  const maxAge = cookie['Max-Age'] ?? cookie['max-age'];
556
- this._exp = expires
557
- ? new Date(expires)
558
- : maxAge
559
- ? new Date(Number(maxAge) * 1000)
560
- : // Default expiration 1 hour
561
- new Date(Date.now() + 3600000);
617
+ this.state.auth = {
618
+ sid: cookie.SID,
619
+ expires: expires
620
+ ? new Date(expires)
621
+ : maxAge
622
+ ? new Date(Number(maxAge) * 1000)
623
+ : new Date(Date.now() + 3600000),
624
+ };
625
+ // Check version after successful login
626
+ await this.checkVersion();
562
627
  return true;
563
628
  }
564
629
  logout() {
565
- this._sid = undefined;
566
- this._exp = undefined;
630
+ delete this.state.auth;
567
631
  return true;
568
632
  }
569
633
  // eslint-disable-next-line max-params
570
634
  async request(path, method, params, body, headers = {}, isJson = true) {
571
- if (!this._sid || !this._exp || this._exp.getTime() < new Date().getTime()) {
635
+ if (!this.state.auth?.sid ||
636
+ !this.state.auth.expires ||
637
+ this.state.auth.expires.getTime() < new Date().getTime()) {
572
638
  const authed = await this.login();
573
639
  if (!authed) {
574
640
  throw new Error('Auth Failed');
575
641
  }
576
642
  }
577
- const url = joinURL(this.config.baseUrl, this.config.path, path);
643
+ const url = joinURL(this.config.baseUrl, this.config.path ?? '', path);
578
644
  const res = await ofetch(url, {
579
645
  method,
580
646
  headers: {
581
- Cookie: `SID=${this._sid ?? ''}`,
647
+ Cookie: `SID=${this.state.auth.sid ?? ''}`,
582
648
  ...headers,
583
649
  },
584
650
  body,
@@ -592,6 +658,17 @@ export class QBittorrent {
592
658
  });
593
659
  return res;
594
660
  }
661
+ async checkVersion() {
662
+ if (!this.state.version?.version) {
663
+ const newVersion = await this.getAppVersion();
664
+ // Remove potential 'v' prefix and any extra info after version number
665
+ const cleanVersion = newVersion.replace(/^v/, '').split('-')[0];
666
+ this.state.version = {
667
+ version: newVersion,
668
+ isVersion5OrHigher: cleanVersion === '5.0.0' || isGreater(cleanVersion, '5.0.0'),
669
+ };
670
+ }
671
+ }
595
672
  }
596
673
  /**
597
674
  * Normalizes hashes
@@ -610,3 +687,6 @@ function objToUrlSearchParams(obj) {
610
687
  }
611
688
  return params;
612
689
  }
690
+ function isGreater(a, b) {
691
+ return a.localeCompare(b, undefined, { numeric: true }) === 1;
692
+ }
@@ -549,7 +549,8 @@ export interface AddTorrentOptions {
549
549
  /**
550
550
  * Add torrents in the paused state. Possible values are true, false (default)
551
551
  */
552
- paused: TrueFalseStr;
552
+ paused?: TrueFalseStr;
553
+ stopped?: TrueFalseStr;
553
554
  /**
554
555
  * Control filesystem structure for content (added in Web API v2.7)
555
556
  * Migrating from rootFolder example rootFolder ? 'Original' : 'NoSubfolder'
@@ -606,7 +607,8 @@ export interface AddMagnetOptions {
606
607
  /**
607
608
  * Add torrents in the paused state. Possible values are true, false (default)
608
609
  */
609
- paused: TrueFalseStr;
610
+ paused?: TrueFalseStr;
611
+ stopped?: TrueFalseStr;
610
612
  /**
611
613
  * Create the root folder. Possible values are true, false, unset (default)
612
614
  */
@@ -1266,4 +1268,6 @@ export interface TorrentPeer {
1266
1268
  up_speed?: number;
1267
1269
  uploaded?: number;
1268
1270
  }
1271
+ export type DownloadSpeed = Record<string, number>;
1272
+ export type UploadSpeed = Record<string, number>;
1269
1273
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctrl/qbittorrent",
3
- "version": "9.1.0",
3
+ "version": "9.3.0",
4
4
  "description": "TypeScript api wrapper for qbittorrent using got",
5
5
  "author": "Scott Cooper <scttcper@gmail.com>",
6
6
  "license": "MIT",
@@ -37,26 +37,28 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@ctrl/magnet-link": "^4.0.2",
40
- "@ctrl/shared-torrent": "^6.1.0",
40
+ "@ctrl/shared-torrent": "^6.2.1",
41
41
  "@ctrl/torrent-file": "^4.1.0",
42
- "cookie": "^1.0.1",
43
- "node-fetch-native": "^1.6.4",
42
+ "cookie": "^1.0.2",
43
+ "node-fetch-native": "^1.6.6",
44
44
  "ofetch": "^1.4.1",
45
+ "type-fest": "^4.34.1",
45
46
  "ufo": "^1.5.4",
46
47
  "uint8array-extras": "^1.4.0"
47
48
  },
48
49
  "devDependencies": {
49
50
  "@biomejs/biome": "1.9.4",
50
- "@ctrl/eslint-config-biome": "4.2.11",
51
- "@eslint/compat": "^1.2.2",
52
- "@sindresorhus/tsconfig": "6.0.0",
51
+ "@ctrl/eslint-config-biome": "4.3.2",
52
+ "@eslint/compat": "^1.2.6",
53
+ "@sindresorhus/tsconfig": "7.0.0",
53
54
  "@types/cookie": "1.0.0",
54
- "@types/node": "22.9.0",
55
- "@vitest/coverage-v8": "2.1.4",
55
+ "@types/node": "22.13.1",
56
+ "@vitest/coverage-v8": "3.0.5",
57
+ "eslint": "^9.20.1",
56
58
  "p-wait-for": "5.0.2",
57
- "typedoc": "0.26.11",
58
- "typescript": "5.6.3",
59
- "vitest": "2.1.4"
59
+ "typedoc": "0.27.7",
60
+ "typescript": "5.7.3",
61
+ "vitest": "3.0.5"
60
62
  },
61
63
  "release": {
62
64
  "branches": [