@ctrl/qbittorrent 9.0.1 → 9.2.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 +10 -1
- package/dist/src/normalizeTorrentData.d.ts +2 -2
- package/dist/src/normalizeTorrentData.js +1 -1
- package/dist/src/qbittorrent.d.ts +27 -7
- package/dist/src/qbittorrent.js +67 -28
- package/dist/src/types.d.ts +4 -2
- package/package.json +16 -15
package/README.md
CHANGED
@@ -20,7 +20,7 @@ const client = new QBittorrent({
|
|
20
20
|
});
|
21
21
|
|
22
22
|
async function main() {
|
23
|
-
const res = await
|
23
|
+
const res = await client.getAllData();
|
24
24
|
console.log(res);
|
25
25
|
}
|
26
26
|
```
|
@@ -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,3 +1,3 @@
|
|
1
|
-
import { NormalizedTorrent } from '@ctrl/shared-torrent';
|
2
|
-
import { Torrent } from './types.js';
|
1
|
+
import { type NormalizedTorrent } from '@ctrl/shared-torrent';
|
2
|
+
import { type Torrent } from './types.js';
|
3
3
|
export declare function normalizeTorrentData(torrent: Torrent): NormalizedTorrent;
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { TorrentState as NormalizedTorrentState } from '@ctrl/shared-torrent';
|
1
|
+
import { TorrentState as NormalizedTorrentState, } from '@ctrl/shared-torrent';
|
2
2
|
import { TorrentState } from './types.js';
|
3
3
|
export function normalizeTorrentData(torrent) {
|
4
4
|
let state = NormalizedTorrentState.unknown;
|
@@ -1,16 +1,34 @@
|
|
1
|
-
import type {
|
1
|
+
import type { Jsonify } from 'type-fest';
|
2
|
+
import type { AddTorrentOptions as NormalizedAddTorrentOptions, AllClientData, NormalizedTorrent, TorrentClient, TorrentClientConfig, TorrentClientState } from '@ctrl/shared-torrent';
|
2
3
|
import type { AddMagnetOptions, AddTorrentOptions, BuildInfo, Preferences, Torrent, TorrentCategories, TorrentFile, TorrentFilePriority, TorrentFilters, TorrentPeersResponse, TorrentPieceState, TorrentProperties, TorrentTrackers, 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
|
*/
|
@@ -212,4 +230,6 @@ export declare class QBittorrent implements TorrentClient {
|
|
212
230
|
login(): Promise<boolean>;
|
213
231
|
logout(): boolean;
|
214
232
|
request<T>(path: string, method: 'GET' | 'POST', params?: Record<string, string | number>, body?: URLSearchParams | FormData, headers?: Record<string, string>, isJson?: boolean): Promise<T>;
|
233
|
+
private checkVersion;
|
215
234
|
}
|
235
|
+
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
|
*/
|
@@ -307,16 +317,20 @@ export class QBittorrent {
|
|
307
317
|
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#pause-torrents}
|
308
318
|
*/
|
309
319
|
async pauseTorrent(hashes) {
|
320
|
+
const endpoint = this.state.version?.isVersion5OrHigher ? '/torrents/stop' : '/torrents/pause';
|
310
321
|
const data = { hashes: normalizeHashes(hashes) };
|
311
|
-
await this.request(
|
322
|
+
await this.request(endpoint, 'POST', undefined, objToUrlSearchParams(data));
|
312
323
|
return true;
|
313
324
|
}
|
314
325
|
/**
|
315
326
|
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#resume-torrents}
|
316
327
|
*/
|
317
328
|
async resumeTorrent(hashes) {
|
329
|
+
const endpoint = this.state.version?.isVersion5OrHigher
|
330
|
+
? '/torrents/start'
|
331
|
+
: '/torrents/resume';
|
318
332
|
const data = { hashes: normalizeHashes(hashes) };
|
319
|
-
await this.request(
|
333
|
+
await this.request(endpoint, 'POST', undefined, objToUrlSearchParams(data));
|
320
334
|
return true;
|
321
335
|
}
|
322
336
|
/**
|
@@ -362,6 +376,11 @@ export class QBittorrent {
|
|
362
376
|
form.set('file', file);
|
363
377
|
}
|
364
378
|
if (options) {
|
379
|
+
// Handle version-specific paused/stopped parameter
|
380
|
+
if (this.state.version?.isVersion5OrHigher && 'paused' in options) {
|
381
|
+
form.append('stopped', options.paused);
|
382
|
+
delete options.paused;
|
383
|
+
}
|
365
384
|
// disable savepath when autoTMM is defined
|
366
385
|
if (options.useAutoTMM === 'true') {
|
367
386
|
options.savepath = '';
|
@@ -436,6 +455,11 @@ export class QBittorrent {
|
|
436
455
|
const form = new FormData();
|
437
456
|
form.append('urls', urls);
|
438
457
|
if (options) {
|
458
|
+
// Handle version-specific paused/stopped parameter
|
459
|
+
if (this.state.version?.isVersion5OrHigher && 'paused' in options) {
|
460
|
+
form.append('stopped', options.paused);
|
461
|
+
delete options.paused;
|
462
|
+
}
|
439
463
|
// disable savepath when autoTMM is defined
|
440
464
|
if (options.useAutoTMM === 'true') {
|
441
465
|
options.savepath = '';
|
@@ -526,7 +550,7 @@ export class QBittorrent {
|
|
526
550
|
* {@link https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#login}
|
527
551
|
*/
|
528
552
|
async login() {
|
529
|
-
const url = joinURL(this.config.baseUrl, this.config.path, '/auth/login');
|
553
|
+
const url = joinURL(this.config.baseUrl, this.config.path ?? '', '/auth/login');
|
530
554
|
const res = await ofetch.raw(url, {
|
531
555
|
method: 'POST',
|
532
556
|
headers: {
|
@@ -539,46 +563,48 @@ export class QBittorrent {
|
|
539
563
|
redirect: 'manual',
|
540
564
|
retry: false,
|
541
565
|
timeout: this.config.timeout,
|
542
|
-
|
566
|
+
dispatcher: this.config.dispatcher,
|
543
567
|
});
|
544
568
|
if (!res.headers.get('set-cookie')?.length) {
|
545
569
|
throw new Error('Cookie not found. Auth Failed.');
|
546
570
|
}
|
547
|
-
const cookie = cookieParse(res.headers.get('set-cookie'));
|
571
|
+
const cookie = cookieParse(res.headers.get('set-cookie') ?? '');
|
548
572
|
if (!cookie.SID) {
|
549
573
|
throw new Error('Invalid cookie');
|
550
574
|
}
|
551
|
-
this._sid = cookie.SID;
|
552
|
-
// Not sure if it might be lowercase
|
553
575
|
const expires = cookie.Expires ?? cookie.expires;
|
554
|
-
// Assumed to be in seconds
|
555
576
|
const maxAge = cookie['Max-Age'] ?? cookie['max-age'];
|
556
|
-
this.
|
557
|
-
|
558
|
-
:
|
559
|
-
? new Date(
|
560
|
-
:
|
561
|
-
new Date(
|
577
|
+
this.state.auth = {
|
578
|
+
sid: cookie.SID,
|
579
|
+
expires: expires
|
580
|
+
? new Date(expires)
|
581
|
+
: maxAge
|
582
|
+
? new Date(Number(maxAge) * 1000)
|
583
|
+
: new Date(Date.now() + 3600000),
|
584
|
+
};
|
585
|
+
// Check version after successful login
|
586
|
+
await this.checkVersion();
|
562
587
|
return true;
|
563
588
|
}
|
564
589
|
logout() {
|
565
|
-
this.
|
566
|
-
this._exp = undefined;
|
590
|
+
delete this.state.auth;
|
567
591
|
return true;
|
568
592
|
}
|
569
593
|
// eslint-disable-next-line max-params
|
570
594
|
async request(path, method, params, body, headers = {}, isJson = true) {
|
571
|
-
if (!this.
|
595
|
+
if (!this.state.auth?.sid ||
|
596
|
+
!this.state.auth.expires ||
|
597
|
+
this.state.auth.expires.getTime() < new Date().getTime()) {
|
572
598
|
const authed = await this.login();
|
573
599
|
if (!authed) {
|
574
600
|
throw new Error('Auth Failed');
|
575
601
|
}
|
576
602
|
}
|
577
|
-
const url = joinURL(this.config.baseUrl, this.config.path, path);
|
603
|
+
const url = joinURL(this.config.baseUrl, this.config.path ?? '', path);
|
578
604
|
const res = await ofetch(url, {
|
579
605
|
method,
|
580
606
|
headers: {
|
581
|
-
Cookie: `SID=${this.
|
607
|
+
Cookie: `SID=${this.state.auth.sid ?? ''}`,
|
582
608
|
...headers,
|
583
609
|
},
|
584
610
|
body,
|
@@ -588,11 +614,21 @@ export class QBittorrent {
|
|
588
614
|
// casting to json to avoid type error
|
589
615
|
responseType: isJson ? 'json' : 'text',
|
590
616
|
// allow proxy agent
|
591
|
-
|
592
|
-
agent: this.config.agent,
|
617
|
+
dispatcher: this.config.dispatcher,
|
593
618
|
});
|
594
619
|
return res;
|
595
620
|
}
|
621
|
+
async checkVersion() {
|
622
|
+
if (!this.state.version?.version) {
|
623
|
+
const newVersion = await this.getAppVersion();
|
624
|
+
// Remove potential 'v' prefix and any extra info after version number
|
625
|
+
const cleanVersion = newVersion.replace(/^v/, '').split('-')[0];
|
626
|
+
this.state.version = {
|
627
|
+
version: newVersion,
|
628
|
+
isVersion5OrHigher: cleanVersion === '5.0.0' || isGreater(cleanVersion, '5.0.0'),
|
629
|
+
};
|
630
|
+
}
|
631
|
+
}
|
596
632
|
}
|
597
633
|
/**
|
598
634
|
* Normalizes hashes
|
@@ -611,3 +647,6 @@ function objToUrlSearchParams(obj) {
|
|
611
647
|
}
|
612
648
|
return params;
|
613
649
|
}
|
650
|
+
function isGreater(a, b) {
|
651
|
+
return a.localeCompare(b, undefined, { numeric: true }) === 1;
|
652
|
+
}
|
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
|
*/
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@ctrl/qbittorrent",
|
3
|
-
"version": "9.0
|
3
|
+
"version": "9.2.0",
|
4
4
|
"description": "TypeScript api wrapper for qbittorrent using got",
|
5
5
|
"author": "Scott Cooper <scttcper@gmail.com>",
|
6
6
|
"license": "MIT",
|
@@ -27,7 +27,7 @@
|
|
27
27
|
"lint:eslint": "eslint .",
|
28
28
|
"lint:fix": "pnpm run '/^(lint:biome|lint:eslint):fix$/'",
|
29
29
|
"lint:eslint:fix": "eslint . --fix",
|
30
|
-
"lint:biome:fix": "biome check . --
|
30
|
+
"lint:biome:fix": "biome check . --write",
|
31
31
|
"prepare": "npm run build",
|
32
32
|
"build": "tsc",
|
33
33
|
"build:docs": "typedoc",
|
@@ -37,26 +37,27 @@
|
|
37
37
|
},
|
38
38
|
"dependencies": {
|
39
39
|
"@ctrl/magnet-link": "^4.0.2",
|
40
|
-
"@ctrl/shared-torrent": "^6.
|
40
|
+
"@ctrl/shared-torrent": "^6.2.1",
|
41
41
|
"@ctrl/torrent-file": "^4.1.0",
|
42
|
-
"cookie": "^0.
|
42
|
+
"cookie": "^1.0.2",
|
43
43
|
"node-fetch-native": "^1.6.4",
|
44
|
-
"ofetch": "^1.
|
44
|
+
"ofetch": "^1.4.1",
|
45
|
+
"type-fest": "^4.30.2",
|
45
46
|
"ufo": "^1.5.4",
|
46
47
|
"uint8array-extras": "^1.4.0"
|
47
48
|
},
|
48
49
|
"devDependencies": {
|
49
|
-
"@biomejs/biome": "1.
|
50
|
-
"@ctrl/eslint-config-biome": "4.
|
51
|
-
"@eslint/compat": "^1.
|
52
|
-
"@sindresorhus/tsconfig": "
|
53
|
-
"@types/cookie": "0.
|
54
|
-
"@types/node": "22.
|
55
|
-
"@vitest/coverage-v8": "2.
|
50
|
+
"@biomejs/biome": "1.9.4",
|
51
|
+
"@ctrl/eslint-config-biome": "4.3.1",
|
52
|
+
"@eslint/compat": "^1.2.4",
|
53
|
+
"@sindresorhus/tsconfig": "7.0.0",
|
54
|
+
"@types/cookie": "1.0.0",
|
55
|
+
"@types/node": "22.10.2",
|
56
|
+
"@vitest/coverage-v8": "2.1.8",
|
56
57
|
"p-wait-for": "5.0.2",
|
57
|
-
"typedoc": "0.
|
58
|
-
"typescript": "5.
|
59
|
-
"vitest": "2.
|
58
|
+
"typedoc": "0.27.5",
|
59
|
+
"typescript": "5.7.2",
|
60
|
+
"vitest": "2.1.8"
|
60
61
|
},
|
61
62
|
"release": {
|
62
63
|
"branches": [
|