@ctrl/rqbit 1.0.0 → 2.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
@@ -8,6 +8,8 @@
8
8
  npm install @ctrl/rqbit
9
9
  ```
10
10
 
11
+ Requires Node.js 24 or newer.
12
+
11
13
  ### Use
12
14
 
13
15
  ```ts
@@ -66,27 +68,23 @@ console.log(data);
66
68
 
67
69
  ##### pauseTorrent and resumeTorrent
68
70
 
69
- Pause or resume a torrent
71
+ Pause or resume one or more torrents
70
72
 
71
73
  ```ts
72
- const paused = await client.pauseTorrent('torrent-hash');
73
- console.log(paused);
74
- const resumed = await client.resumeTorrent('torrent-hash');
75
- console.log(resumed);
74
+ await client.pauseTorrent('torrent-hash');
75
+ await client.resumeTorrent(['torrent-hash', 'other-torrent-hash']);
76
76
  ```
77
77
 
78
78
  ##### removeTorrent
79
79
 
80
- Remove a torrent. Does not remove data on disk by default.
80
+ Remove one or more torrents, throws if a torrent doesn't exist. Does not remove data on disk by default.
81
81
 
82
82
  ```ts
83
83
  // does not remove data on disk
84
- const result = await client.removeTorrent('torrent-hash', false);
85
- console.log(result);
84
+ await client.removeTorrent('torrent-hash', false);
86
85
 
87
86
  // remove data on disk
88
- const res = await client.removeTorrent('torrent-hash', true);
89
- console.log(res);
87
+ await client.removeTorrent(['torrent-hash', 'other-torrent-hash'], true);
90
88
  ```
91
89
 
92
90
  ##### addTorrent
@@ -113,6 +111,22 @@ const result = await client.normalizedAddTorrent('magnet:?xt=urn:btih:...', {
113
111
  console.log(result);
114
112
  ```
115
113
 
114
+ ##### Errors
115
+
116
+ Failed requests throw a `TorrentClientError` from [@ctrl/shared-torrent](https://github.com/scttcper/shared-torrent) with a `code` of `torrent_not_found`, `unauthorized`, `request_failed` or `client_error`, the http `status` when there is one and the original error as the `cause`. rqbit errors are a `RqbitApiError`, a `TorrentClientError` that also has rqbit's error `kind`.
117
+
118
+ ```ts
119
+ import { TorrentClientError } from '@ctrl/rqbit';
120
+
121
+ try {
122
+ await client.getTorrent('torrent-hash');
123
+ } catch (error) {
124
+ if (error instanceof TorrentClientError && error.code === 'torrent_not_found') {
125
+ // not in the client
126
+ }
127
+ }
128
+ ```
129
+
116
130
  ##### export and create from state
117
131
 
118
132
  rqbit uses basic auth on every request so there is no session to save, this exists to match the other clients.
@@ -1,3 +1,4 @@
1
1
  export * from './rqbit.js';
2
2
  export * from './normalizeTorrentData.js';
3
3
  export type * from './types.js';
4
+ export { TorrentClientError, type TorrentClientErrorCode } from '@ctrl/shared-torrent';
package/dist/src/index.js CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './rqbit.js';
2
2
  export * from './normalizeTorrentData.js';
3
+ export { TorrentClientError } from '@ctrl/shared-torrent';
@@ -48,7 +48,7 @@ export function normalizeTorrentData(torrent, files) {
48
48
  savePath: torrent.output_folder,
49
49
  uploadSpeed: live ? Math.round(live.upload_speed.mbps * MIB) : 0,
50
50
  downloadSpeed: live ? Math.round(live.download_speed.mbps * MIB) : 0,
51
- eta: live?.time_remaining?.duration.secs ?? 0,
51
+ eta: stats.finished ? 0 : (live?.time_remaining?.duration.secs ?? -1),
52
52
  // rqbit has no queue
53
53
  queuePosition: 0,
54
54
  // rqbit does not report seeds separately from peers
@@ -1,4 +1,4 @@
1
- import type { AddTorrentOptions as NormalizedAddTorrentOptions, AllClientData, NormalizedTorrent, TorrentClient, TorrentClientConfig, TorrentClientState } from '@ctrl/shared-torrent';
1
+ import { type AddTorrentOptions as NormalizedAddTorrentOptions, type AllClientData, type NormalizedTorrent, type TorrentClient, type TorrentClientConfig, TorrentClientError, type TorrentClientState } from '@ctrl/shared-torrent';
2
2
  import { type FetchOptions, type MappedResponseType, type ResponseType } from 'ofetch';
3
3
  import type { Jsonify } from 'type-fest';
4
4
  import type { AddTorrentOptions, AddTorrentResponse, ApiRootResponse, CreateTorrentOptions, CreateTorrentResponse, DhtStats, EmptyResponse, ListTorrentsResponse, PeerStatsResponse, RateLimits, RqbitErrorResponse, SessionStats, TorrentDetails, TorrentIdOrHash, TorrentListItem, TorrentStats, TorrentWithStats } from './types.js';
@@ -21,12 +21,11 @@ export type ResolvedConfig = RqbitConfig & {
21
21
  /**
22
22
  * Error thrown when a request to rqbit fails, either an error response or a timeout/network error
23
23
  */
24
- export declare class RqbitApiError extends Error {
24
+ /**
25
+ * A {@link TorrentClientError} with rqbit's error details
26
+ */
27
+ export declare class RqbitApiError extends TorrentClientError {
25
28
  name: string;
26
- /**
27
- * HTTP status code, undefined when no response was received (timeout or network error)
28
- */
29
- status?: number;
30
29
  /**
31
30
  * rqbit error kind, ex - `torrent_not_found`. Undefined when rqbit returned a plain text error.
32
31
  */
@@ -89,13 +88,14 @@ export declare class Rqbit implements TorrentClient {
89
88
  * rqbit requires basic auth on this url when it is enabled.
90
89
  */
91
90
  getPlaylistUrl(id?: TorrentIdOrHash): string;
92
- pauseTorrent(id: TorrentIdOrHash): Promise<EmptyResponse>;
93
- resumeTorrent(id: TorrentIdOrHash): Promise<EmptyResponse>;
91
+ pauseTorrent(id: TorrentIdOrHash | TorrentIdOrHash[]): Promise<void>;
92
+ resumeTorrent(id: TorrentIdOrHash | TorrentIdOrHash[]): Promise<void>;
94
93
  /**
95
94
  * Remove a torrent
96
95
  * @param removeData (default: false) If true, remove the downloaded files.
96
+ * @throws {RqbitApiError} when a torrent doesn't exist
97
97
  */
98
- removeTorrent(id: TorrentIdOrHash, removeData?: boolean): Promise<EmptyResponse>;
98
+ removeTorrent(id: TorrentIdOrHash | TorrentIdOrHash[], removeData?: boolean): Promise<void>;
99
99
  /**
100
100
  * Change which files are downloaded
101
101
  * @param fileIds indexes into the torrent's file list
@@ -111,11 +111,11 @@ export declare class Rqbit implements TorrentClient {
111
111
  /**
112
112
  * rqbit does not support queueing
113
113
  */
114
- queueUp(_id: TorrentIdOrHash): Promise<never>;
114
+ queueUp(_id: TorrentIdOrHash | TorrentIdOrHash[]): Promise<never>;
115
115
  /**
116
116
  * rqbit does not support queueing
117
117
  */
118
- queueDown(_id: TorrentIdOrHash): Promise<never>;
118
+ queueDown(_id: TorrentIdOrHash | TorrentIdOrHash[]): Promise<never>;
119
119
  /**
120
120
  * Add a magnet link or a url to a .torrent file.
121
121
  * rqbit resolves magnet metadata from peers before it responds, so this can take a while, see {@link RqbitConfig.addTimeout}
package/dist/src/rqbit.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { TorrentClientError, } from '@ctrl/shared-torrent';
1
2
  import { FetchError, ofetch, } from 'ofetch';
2
3
  import { joinURL } from 'ufo';
3
4
  import { base64ToUint8Array, stringToBase64 } from 'uint8array-extras';
@@ -13,24 +14,28 @@ const defaults = {
13
14
  /**
14
15
  * Error thrown when a request to rqbit fails, either an error response or a timeout/network error
15
16
  */
16
- export class RqbitApiError extends Error {
17
+ /**
18
+ * A {@link TorrentClientError} with rqbit's error details
19
+ */
20
+ export class RqbitApiError extends TorrentClientError {
17
21
  name = 'RqbitApiError';
18
- /**
19
- * HTTP status code, undefined when no response was received (timeout or network error)
20
- */
21
- status;
22
22
  /**
23
23
  * rqbit error kind, ex - `torrent_not_found`. Undefined when rqbit returned a plain text error.
24
24
  */
25
25
  kind;
26
26
  response;
27
27
  constructor(status, message, response, cause) {
28
- super(message, { cause });
29
- this.status = status;
28
+ super(message, rqbitErrorCode(status, response?.error_kind), { status, cause });
30
29
  this.kind = response?.error_kind;
31
30
  this.response = response;
32
31
  }
33
32
  }
33
+ function rqbitErrorCode(status, kind) {
34
+ if (status === 401 || status === 403) {
35
+ return 'unauthorized';
36
+ }
37
+ return kind === 'torrent_not_found' ? 'torrent_not_found' : 'request_failed';
38
+ }
34
39
  /**
35
40
  * rqbit has no published api reference. `GET /` on the server lists every endpoint, see {@link Rqbit.getApiInfo}.
36
41
  * Routes {@link https://github.com/ikatson/rqbit/blob/v9.0.1/crates/librqbit/src/http_api/handlers/mod.rs}
@@ -126,18 +131,25 @@ export class Rqbit {
126
131
  return this.url(id === undefined ? '/torrents/playlist' : `/torrents/${id}/playlist`);
127
132
  }
128
133
  async pauseTorrent(id) {
129
- return this.request(`/torrents/${id}/pause`, { method: 'POST' });
134
+ for (const i of Array.isArray(id) ? id : [id]) {
135
+ await this.request(`/torrents/${i}/pause`, { method: 'POST' });
136
+ }
130
137
  }
131
138
  async resumeTorrent(id) {
132
- return this.request(`/torrents/${id}/start`, { method: 'POST' });
139
+ for (const i of Array.isArray(id) ? id : [id]) {
140
+ await this.request(`/torrents/${i}/start`, { method: 'POST' });
141
+ }
133
142
  }
134
143
  /**
135
144
  * Remove a torrent
136
145
  * @param removeData (default: false) If true, remove the downloaded files.
146
+ * @throws {RqbitApiError} when a torrent doesn't exist
137
147
  */
138
148
  async removeTorrent(id, removeData = false) {
139
149
  const action = removeData ? 'delete' : 'forget';
140
- return this.request(`/torrents/${id}/${action}`, { method: 'POST' });
150
+ for (const i of Array.isArray(id) ? id : [id]) {
151
+ await this.request(`/torrents/${i}/${action}`, { method: 'POST' });
152
+ }
141
153
  }
142
154
  /**
143
155
  * Change which files are downloaded
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctrl/rqbit",
3
- "version": "1.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "TypeScript api wrapper for rqbit using ofetch",
5
5
  "keywords": [
6
6
  "rqbit",
@@ -21,6 +21,13 @@
21
21
  "sideEffects": false,
22
22
  "main": "./dist/src/index.js",
23
23
  "types": "./dist/src/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/src/index.d.ts",
27
+ "default": "./dist/src/index.js"
28
+ },
29
+ "./package.json": "./package.json"
30
+ },
24
31
  "publishConfig": {
25
32
  "access": "public",
26
33
  "provenance": true
@@ -35,7 +42,7 @@
35
42
  "test:watch": "vitest"
36
43
  },
37
44
  "dependencies": {
38
- "@ctrl/shared-torrent": "^6.5.2",
45
+ "@ctrl/shared-torrent": "^6.6.0",
39
46
  "ofetch": "^1.5.1",
40
47
  "type-fest": "^5.10.0",
41
48
  "ufo": "^1.6.4",
@@ -59,7 +66,7 @@
59
66
  ]
60
67
  },
61
68
  "engines": {
62
- "node": ">=22"
69
+ "node": ">=24"
63
70
  },
64
71
  "packageManager": "pnpm@12.9.1"
65
72
  }