@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 +9 -0
- package/dist/src/qbittorrent.d.ts +44 -8
- package/dist/src/qbittorrent.js +105 -25
- package/dist/src/types.d.ts +6 -2
- package/package.json +14 -12
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 {
|
2
|
-
import type {
|
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
|
-
*
|
22
|
+
* Create a new QBittorrent client from a state
|
7
23
|
*/
|
8
|
-
|
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
|
-
*
|
29
|
+
* Export the state of the client as JSON
|
11
30
|
*/
|
12
|
-
|
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 {};
|
package/dist/src/qbittorrent.js
CHANGED
@@ -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
|
-
*
|
18
|
+
* Create a new QBittorrent client from a state
|
24
19
|
*/
|
25
|
-
|
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(
|
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(
|
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.
|
557
|
-
|
558
|
-
:
|
559
|
-
? new Date(
|
560
|
-
:
|
561
|
-
new Date(
|
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.
|
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.
|
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.
|
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
|
+
}
|
package/dist/src/types.d.ts
CHANGED
@@ -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
|
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
|
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.
|
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
|
40
|
+
"@ctrl/shared-torrent": "^6.2.1",
|
41
41
|
"@ctrl/torrent-file": "^4.1.0",
|
42
|
-
"cookie": "^1.0.
|
43
|
-
"node-fetch-native": "^1.6.
|
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
|
51
|
-
"@eslint/compat": "^1.2.
|
52
|
-
"@sindresorhus/tsconfig": "
|
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.
|
55
|
-
"@vitest/coverage-v8": "
|
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.
|
58
|
-
"typescript": "5.
|
59
|
-
"vitest": "
|
59
|
+
"typedoc": "0.27.7",
|
60
|
+
"typescript": "5.7.3",
|
61
|
+
"vitest": "3.0.5"
|
60
62
|
},
|
61
63
|
"release": {
|
62
64
|
"branches": [
|