@ctrl/rqbit 0.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/LICENSE +21 -0
- package/README.md +154 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +2 -0
- package/dist/src/normalizeTorrentData.d.ts +11 -0
- package/dist/src/normalizeTorrentData.js +65 -0
- package/dist/src/rqbit.d.ts +156 -0
- package/dist/src/rqbit.js +338 -0
- package/dist/src/types.d.ts +336 -0
- package/dist/src/types.js +1 -0
- package/package.json +65 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Scott Cooper <scttcper@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# rqbit [](https://www.npmjs.com/package/@ctrl/rqbit)
|
|
2
|
+
|
|
3
|
+
> TypeScript api wrapper for [rqbit](https://github.com/ikatson/rqbit) using [ofetch](https://github.com/unjs/ofetch)
|
|
4
|
+
|
|
5
|
+
### Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @ctrl/rqbit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
### Use
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { Rqbit } from '@ctrl/rqbit';
|
|
15
|
+
|
|
16
|
+
const client = new Rqbit({
|
|
17
|
+
baseUrl: 'http://localhost:3030/',
|
|
18
|
+
// only needed when rqbit is started with RQBIT_HTTP_BASIC_AUTH_USERPASS
|
|
19
|
+
username: 'admin',
|
|
20
|
+
password: 'adminadmin',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
async function main() {
|
|
24
|
+
const res = await client.getAllData();
|
|
25
|
+
console.log(res);
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### API
|
|
30
|
+
|
|
31
|
+
Docs: https://rqbit.ep.workers.dev
|
|
32
|
+
|
|
33
|
+
rqbit has no published api reference, `GET /` on the server lists every endpoint (`client.getApiInfo()`). The types here are based on responses from rqbit 9.0.1 and its [http api source](https://github.com/ikatson/rqbit/tree/v9.0.1/crates/librqbit/src/http_api).
|
|
34
|
+
|
|
35
|
+
Things that work differently from the other clients:
|
|
36
|
+
|
|
37
|
+
- Adding a magnet does not respond until rqbit resolves the metadata from peers. Adds use `addTimeout` (default 60 seconds) instead of `timeout`.
|
|
38
|
+
- rqbit has no labels, queue, or added/completed dates. `label` is ignored, `queueUp`/`queueDown` throw, and `dateAdded` is an empty string.
|
|
39
|
+
- `createTorrent` needs rqbit started with `RQBIT_HTTP_API_ALLOW_CREATE=true`, and `getStreamUrl`/`getPlaylistUrl` urls need the same basic auth as the api.
|
|
40
|
+
- Speeds are reported in MiB/s and converted to bytes per second when normalized.
|
|
41
|
+
- Torrents are added with `overwrite: true` by default, like Radarr does. rqbit refuses to add a torrent whose files already exist on disk otherwise.
|
|
42
|
+
- Failed requests throw `RqbitApiError` with the http `status` and rqbit's error `kind`, ex - `torrent_not_found`. Timeouts and network errors are also `RqbitApiError` with no `status`.
|
|
43
|
+
- rqbit's `total_bytes` only counts the pieces of selected files. `getTorrent` uses the file list for `totalSize`, `getAllData` skips the extra request per torrent so its `totalSize` is the selected size.
|
|
44
|
+
|
|
45
|
+
### Normalized API
|
|
46
|
+
|
|
47
|
+
These functions are normalized through [@ctrl/shared-torrent](https://github.com/scttcper/shared-torrent), which makes it easier to support multiple torrent clients. See [below](#see-also) for alternative supported torrent clients.
|
|
48
|
+
|
|
49
|
+
##### getAllData
|
|
50
|
+
|
|
51
|
+
Returns all torrent data and an array of label objects. Data has been normalized and does not match the output of native `listTorrents()`. rqbit has no labels so labels is always empty.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const data = await client.getAllData();
|
|
55
|
+
console.log(data.torrents);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
##### getTorrent
|
|
59
|
+
|
|
60
|
+
Returns one torrent data from torrent hash
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
const data = await client.getTorrent('torrent-hash');
|
|
64
|
+
console.log(data);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
##### pauseTorrent and resumeTorrent
|
|
68
|
+
|
|
69
|
+
Pause or resume a torrent
|
|
70
|
+
|
|
71
|
+
```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);
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
##### removeTorrent
|
|
79
|
+
|
|
80
|
+
Remove a torrent. Does not remove data on disk by default.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// does not remove data on disk
|
|
84
|
+
const result = await client.removeTorrent('torrent-hash', false);
|
|
85
|
+
console.log(result);
|
|
86
|
+
|
|
87
|
+
// remove data on disk
|
|
88
|
+
const res = await client.removeTorrent('torrent-hash', true);
|
|
89
|
+
console.log(res);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
##### addTorrent
|
|
93
|
+
|
|
94
|
+
Add a torrent from a magnet link or torrent file, has client specific options. Also see normalizedAddTorrent
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { readFileSync } from 'node:fs';
|
|
98
|
+
|
|
99
|
+
const result = await client.addTorrent(new Uint8Array(readFileSync('./linux.torrent')), {
|
|
100
|
+
output_folder: '/downloads/linux',
|
|
101
|
+
});
|
|
102
|
+
console.log(result.details.info_hash);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
##### normalizedAddTorrent
|
|
106
|
+
|
|
107
|
+
Add a torrent and return normalized torrent data. rqbit cannot add a torrent paused, `startPaused` pauses it right after adding.
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
const result = await client.normalizedAddTorrent('magnet:?xt=urn:btih:...', {
|
|
111
|
+
startPaused: true,
|
|
112
|
+
});
|
|
113
|
+
console.log(result);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
##### export and create from state
|
|
117
|
+
|
|
118
|
+
rqbit uses basic auth on every request so there is no session to save, this exists to match the other clients.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const state = client.exportState();
|
|
122
|
+
const restored = Rqbit.createFromState(config, state);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### See Also
|
|
126
|
+
|
|
127
|
+
All of the following npm modules provide the same normalized functions along with supporting the unique apis for each client.
|
|
128
|
+
|
|
129
|
+
- shared types - [@ctrl/shared-torrent](https://github.com/scttcper/shared-torrent)
|
|
130
|
+
- deluge - [@ctrl/deluge](https://github.com/scttcper/deluge)
|
|
131
|
+
- transmission - [@ctrl/transmission](https://github.com/scttcper/transmission)
|
|
132
|
+
- qbittorrent - [@ctrl/qbittorrent](https://github.com/scttcper/qbittorrent)
|
|
133
|
+
- utorrent - [@ctrl/utorrent](https://github.com/scttcper/utorrent)
|
|
134
|
+
- rtorrent - [@ctrl/rtorrent](https://github.com/scttcper/rtorrent)
|
|
135
|
+
|
|
136
|
+
Usenet clients with the same normalized approach:
|
|
137
|
+
|
|
138
|
+
- usenet shared types - [@ctrl/shared-usenet](https://github.com/scttcper/shared-usenet)
|
|
139
|
+
- nzbget - [@ctrl/nzbget](https://github.com/scttcper/nzbget)
|
|
140
|
+
- sabnzbd - [@ctrl/sabnzbd](https://github.com/scttcper/sabnzbd)
|
|
141
|
+
|
|
142
|
+
### Start a test docker container
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
docker run -d \
|
|
146
|
+
--name=rqbit \
|
|
147
|
+
-e RQBIT_HTTP_BASIC_AUTH_USERPASS=admin:adminadmin \
|
|
148
|
+
-e RQBIT_HTTP_API_ALLOW_CREATE=true \
|
|
149
|
+
-p 3030:3030 \
|
|
150
|
+
-p 4240:4240 \
|
|
151
|
+
-v ~/Documents/rqbit/downloads:/home/rqbit/downloads \
|
|
152
|
+
--restart unless-stopped \
|
|
153
|
+
ikatson/rqbit:latest
|
|
154
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type NormalizedTorrent } from '@ctrl/shared-torrent';
|
|
2
|
+
import type { TorrentFile, TorrentWithStats } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Convert a torrent with stats into the shared normalized format.
|
|
5
|
+
* Speeds are MiB/s and the ETA is a `{ secs, nanos }` duration,
|
|
6
|
+
* see {@link https://github.com/ikatson/rqbit/blob/v9.0.1/crates/librqbit/src/torrent_state/stats.rs}
|
|
7
|
+
*
|
|
8
|
+
* @param files file list from the torrent details, used for `totalSize`.
|
|
9
|
+
* rqbit's `total_bytes` only counts the pieces of selected files, so without the file list `totalSize` is the selected size.
|
|
10
|
+
*/
|
|
11
|
+
export declare function normalizeTorrentData(torrent: TorrentWithStats, files?: TorrentFile[]): NormalizedTorrent;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { TorrentState } from '@ctrl/shared-torrent';
|
|
2
|
+
const MIB = 1024 * 1024;
|
|
3
|
+
/**
|
|
4
|
+
* Convert a torrent with stats into the shared normalized format.
|
|
5
|
+
* Speeds are MiB/s and the ETA is a `{ secs, nanos }` duration,
|
|
6
|
+
* see {@link https://github.com/ikatson/rqbit/blob/v9.0.1/crates/librqbit/src/torrent_state/stats.rs}
|
|
7
|
+
*
|
|
8
|
+
* @param files file list from the torrent details, used for `totalSize`.
|
|
9
|
+
* rqbit's `total_bytes` only counts the pieces of selected files, so without the file list `totalSize` is the selected size.
|
|
10
|
+
*/
|
|
11
|
+
export function normalizeTorrentData(torrent, files) {
|
|
12
|
+
const { stats } = torrent;
|
|
13
|
+
const live = stats.live;
|
|
14
|
+
let state = TorrentState.unknown;
|
|
15
|
+
switch (stats.state) {
|
|
16
|
+
case 'initializing': {
|
|
17
|
+
// pausing during initialization keeps the torrent in initializing until it is started
|
|
18
|
+
state = stats.initializing_paused ? TorrentState.paused : TorrentState.checking;
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
case 'live': {
|
|
22
|
+
state = stats.finished ? TorrentState.seeding : TorrentState.downloading;
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
case 'paused': {
|
|
26
|
+
state = TorrentState.paused;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case 'error': {
|
|
30
|
+
state = TorrentState.error;
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const progress = stats.total_bytes > 0 ? stats.progress_bytes / stats.total_bytes : 0;
|
|
35
|
+
const peerStats = live?.snapshot.peer_stats;
|
|
36
|
+
return {
|
|
37
|
+
id: torrent.info_hash,
|
|
38
|
+
name: torrent.name ?? torrent.info_hash,
|
|
39
|
+
state,
|
|
40
|
+
stateMessage: stats.error ?? '',
|
|
41
|
+
isCompleted: stats.finished,
|
|
42
|
+
progress,
|
|
43
|
+
ratio: stats.progress_bytes > 0 ? stats.uploaded_bytes / stats.progress_bytes : 0,
|
|
44
|
+
// rqbit does not track when a torrent was added or completed
|
|
45
|
+
dateAdded: '',
|
|
46
|
+
dateCompleted: undefined,
|
|
47
|
+
label: undefined,
|
|
48
|
+
savePath: torrent.output_folder,
|
|
49
|
+
uploadSpeed: live ? Math.round(live.upload_speed.mbps * MIB) : 0,
|
|
50
|
+
downloadSpeed: live ? Math.round(live.download_speed.mbps * MIB) : 0,
|
|
51
|
+
eta: live?.time_remaining?.duration.secs ?? 0,
|
|
52
|
+
// rqbit has no queue
|
|
53
|
+
queuePosition: 0,
|
|
54
|
+
// rqbit does not report seeds separately from peers
|
|
55
|
+
connectedPeers: peerStats?.live ?? 0,
|
|
56
|
+
connectedSeeds: 0,
|
|
57
|
+
totalPeers: peerStats?.seen ?? 0,
|
|
58
|
+
totalSeeds: 0,
|
|
59
|
+
totalSelected: stats.total_bytes,
|
|
60
|
+
totalSize: files ? files.reduce((sum, file) => sum + file.length, 0) : stats.total_bytes,
|
|
61
|
+
totalUploaded: stats.uploaded_bytes,
|
|
62
|
+
totalDownloaded: stats.progress_bytes,
|
|
63
|
+
raw: torrent,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { AddTorrentOptions as NormalizedAddTorrentOptions, AllClientData, NormalizedTorrent, TorrentClient, TorrentClientConfig, TorrentClientState } from '@ctrl/shared-torrent';
|
|
2
|
+
import { type FetchOptions, type MappedResponseType, type ResponseType } from 'ofetch';
|
|
3
|
+
import type { Jsonify } from 'type-fest';
|
|
4
|
+
import type { AddTorrentOptions, AddTorrentResponse, ApiRootResponse, CreateTorrentOptions, CreateTorrentResponse, DhtStats, EmptyResponse, ListTorrentsResponse, PeerStatsResponse, RateLimits, RqbitErrorResponse, SessionStats, TorrentDetails, TorrentIdOrHash, TorrentListItem, TorrentStats, TorrentWithStats } from './types.js';
|
|
5
|
+
/**
|
|
6
|
+
* rqbit uses basic auth on every request, there is no session to keep
|
|
7
|
+
*/
|
|
8
|
+
export type RqbitState = TorrentClientState;
|
|
9
|
+
export interface RqbitConfig extends TorrentClientConfig {
|
|
10
|
+
/**
|
|
11
|
+
* How long to wait when adding a torrent, in milliseconds.
|
|
12
|
+
* rqbit does not respond to an add until magnet metadata is resolved from peers, so adds need much longer than `timeout`.
|
|
13
|
+
* default: 60_000
|
|
14
|
+
*/
|
|
15
|
+
addTimeout?: number;
|
|
16
|
+
}
|
|
17
|
+
export type ResolvedConfig = RqbitConfig & {
|
|
18
|
+
path: string;
|
|
19
|
+
addTimeout: number;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Error thrown when a request to rqbit fails, either an error response or a timeout/network error
|
|
23
|
+
*/
|
|
24
|
+
export declare class RqbitApiError extends Error {
|
|
25
|
+
name: string;
|
|
26
|
+
/**
|
|
27
|
+
* HTTP status code, undefined when no response was received (timeout or network error)
|
|
28
|
+
*/
|
|
29
|
+
status?: number;
|
|
30
|
+
/**
|
|
31
|
+
* rqbit error kind, ex - `torrent_not_found`. Undefined when rqbit returned a plain text error.
|
|
32
|
+
*/
|
|
33
|
+
kind?: string;
|
|
34
|
+
response?: RqbitErrorResponse;
|
|
35
|
+
constructor(status: number | undefined, message: string, response?: RqbitErrorResponse, cause?: unknown);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* rqbit has no published api reference. `GET /` on the server lists every endpoint, see {@link Rqbit.getApiInfo}.
|
|
39
|
+
* Routes {@link https://github.com/ikatson/rqbit/blob/v9.0.1/crates/librqbit/src/http_api/handlers/mod.rs}
|
|
40
|
+
* Response types {@link https://github.com/ikatson/rqbit/blob/v9.0.1/crates/librqbit/src/api.rs}
|
|
41
|
+
*/
|
|
42
|
+
export declare class Rqbit implements TorrentClient {
|
|
43
|
+
static createFromState(config: Readonly<Partial<RqbitConfig>>, state: Readonly<Jsonify<RqbitState>>): Rqbit;
|
|
44
|
+
config: ResolvedConfig;
|
|
45
|
+
state: RqbitState;
|
|
46
|
+
constructor(options?: Partial<RqbitConfig>);
|
|
47
|
+
exportState(): Jsonify<RqbitState>;
|
|
48
|
+
/**
|
|
49
|
+
* Lists the available api endpoints and the rqbit version
|
|
50
|
+
*/
|
|
51
|
+
getApiInfo(): Promise<ApiRootResponse>;
|
|
52
|
+
/**
|
|
53
|
+
* rqbit version, ex - `9.0.1`
|
|
54
|
+
*/
|
|
55
|
+
getVersion(): Promise<string>;
|
|
56
|
+
/**
|
|
57
|
+
* Session wide download/upload speed, peer and connection stats
|
|
58
|
+
*/
|
|
59
|
+
getSessionStats(): Promise<SessionStats>;
|
|
60
|
+
getDhtStats(): Promise<DhtStats>;
|
|
61
|
+
getRateLimits(): Promise<RateLimits>;
|
|
62
|
+
/**
|
|
63
|
+
* Set session wide rate limits in bytes per second, null removes the limit
|
|
64
|
+
*/
|
|
65
|
+
setRateLimits(limits: RateLimits): Promise<EmptyResponse>;
|
|
66
|
+
listTorrents(withStats: true): Promise<ListTorrentsResponse<TorrentWithStats>>;
|
|
67
|
+
listTorrents(withStats?: false): Promise<ListTorrentsResponse<TorrentListItem>>;
|
|
68
|
+
/**
|
|
69
|
+
* Torrent details including the file list
|
|
70
|
+
*/
|
|
71
|
+
getTorrentDetails(id: TorrentIdOrHash): Promise<TorrentDetails>;
|
|
72
|
+
getTorrentStats(id: TorrentIdOrHash): Promise<TorrentStats>;
|
|
73
|
+
/**
|
|
74
|
+
* Per peer stats, defaults to only live peers
|
|
75
|
+
*/
|
|
76
|
+
getTorrentPeerStats(id: TorrentIdOrHash, state?: 'live' | 'all'): Promise<PeerStatsResponse>;
|
|
77
|
+
/**
|
|
78
|
+
* Download the .torrent file for a torrent
|
|
79
|
+
*/
|
|
80
|
+
getTorrentMetadata(id: TorrentIdOrHash): Promise<Uint8Array<ArrayBuffer>>;
|
|
81
|
+
/**
|
|
82
|
+
* Url to stream a file from a torrent, supports range requests. rqbit requires basic auth on this url when it is enabled.
|
|
83
|
+
* {@link https://github.com/ikatson/rqbit/blob/v9.0.1/crates/librqbit/src/http_api/handlers/streaming.rs}
|
|
84
|
+
* @param fileIndex index into the torrent's file list
|
|
85
|
+
*/
|
|
86
|
+
getStreamUrl(id: TorrentIdOrHash, fileIndex: number): string;
|
|
87
|
+
/**
|
|
88
|
+
* Url to an m3u8 playlist of a torrent's playable files, or every torrent's when no id is passed.
|
|
89
|
+
* rqbit requires basic auth on this url when it is enabled.
|
|
90
|
+
*/
|
|
91
|
+
getPlaylistUrl(id?: TorrentIdOrHash): string;
|
|
92
|
+
pauseTorrent(id: TorrentIdOrHash): Promise<EmptyResponse>;
|
|
93
|
+
resumeTorrent(id: TorrentIdOrHash): Promise<EmptyResponse>;
|
|
94
|
+
/**
|
|
95
|
+
* Remove a torrent
|
|
96
|
+
* @param removeData (default: false) If true, remove the downloaded files.
|
|
97
|
+
*/
|
|
98
|
+
removeTorrent(id: TorrentIdOrHash, removeData?: boolean): Promise<EmptyResponse>;
|
|
99
|
+
/**
|
|
100
|
+
* Change which files are downloaded
|
|
101
|
+
* @param fileIds indexes into the torrent's file list
|
|
102
|
+
*/
|
|
103
|
+
setTorrentFiles(id: TorrentIdOrHash, fileIds: number[]): Promise<EmptyResponse>;
|
|
104
|
+
/**
|
|
105
|
+
* Connect to additional peers
|
|
106
|
+
* @param peers ex - `['1.2.3.4:6881']`
|
|
107
|
+
*/
|
|
108
|
+
addPeers(id: TorrentIdOrHash, peers: string[]): Promise<{
|
|
109
|
+
added: number;
|
|
110
|
+
}>;
|
|
111
|
+
/**
|
|
112
|
+
* rqbit does not support queueing
|
|
113
|
+
*/
|
|
114
|
+
queueUp(_id: TorrentIdOrHash): Promise<never>;
|
|
115
|
+
/**
|
|
116
|
+
* rqbit does not support queueing
|
|
117
|
+
*/
|
|
118
|
+
queueDown(_id: TorrentIdOrHash): Promise<never>;
|
|
119
|
+
/**
|
|
120
|
+
* Add a magnet link or a url to a .torrent file.
|
|
121
|
+
* rqbit resolves magnet metadata from peers before it responds, so this can take a while, see {@link RqbitConfig.addTimeout}
|
|
122
|
+
* {@link https://github.com/ikatson/rqbit/blob/v9.0.1/crates/librqbit/src/http_api/handlers/torrents.rs}
|
|
123
|
+
*/
|
|
124
|
+
addMagnet(url: string, options?: Partial<AddTorrentOptions>): Promise<AddTorrentResponse>;
|
|
125
|
+
/**
|
|
126
|
+
* Add a torrent
|
|
127
|
+
* @param torrent .torrent file contents, or the file contents as a base64 string. Magnet links and urls are passed to {@link Rqbit.addMagnet}
|
|
128
|
+
*/
|
|
129
|
+
addTorrent(torrent: string | Uint8Array<ArrayBuffer>, options?: Partial<AddTorrentOptions>): Promise<AddTorrentResponse>;
|
|
130
|
+
/**
|
|
131
|
+
* Resolve a magnet link to .torrent file contents without adding it.
|
|
132
|
+
* Like adding, rqbit fetches the metadata from peers before it responds, see {@link RqbitConfig.addTimeout}
|
|
133
|
+
*/
|
|
134
|
+
resolveMagnet(magnet: string, options?: Partial<Pick<AddTorrentOptions, 'timeout_ms'>>): Promise<Uint8Array<ArrayBuffer>>;
|
|
135
|
+
/**
|
|
136
|
+
* Create a torrent from a folder on the rqbit server and start seeding it.
|
|
137
|
+
* rqbit must be started with `--http-api-allow-create` or `RQBIT_HTTP_API_ALLOW_CREATE=true`.
|
|
138
|
+
* @param folder path to the folder on the rqbit server
|
|
139
|
+
*/
|
|
140
|
+
createTorrent(folder: string, options?: Partial<CreateTorrentOptions>): Promise<CreateTorrentResponse>;
|
|
141
|
+
/**
|
|
142
|
+
* Add a torrent and return normalized torrent data.
|
|
143
|
+
* The add endpoint has no paused option, `startPaused` pauses the torrent after adding.
|
|
144
|
+
* rqbit does not support labels, `label` is ignored.
|
|
145
|
+
* {@link https://github.com/ikatson/rqbit/blob/v9.0.1/crates/librqbit/src/http_api_types.rs}
|
|
146
|
+
*/
|
|
147
|
+
normalizedAddTorrent(torrent: string | Uint8Array<ArrayBuffer>, options?: Partial<NormalizedAddTorrentOptions>): Promise<NormalizedTorrent>;
|
|
148
|
+
getTorrent(id: TorrentIdOrHash): Promise<NormalizedTorrent>;
|
|
149
|
+
/**
|
|
150
|
+
* rqbit does not support labels so labels is always empty
|
|
151
|
+
*/
|
|
152
|
+
getAllData(): Promise<AllClientData>;
|
|
153
|
+
request<T, R extends ResponseType = 'json'>(path: string, options?: FetchOptions<R>): Promise<MappedResponseType<R, T>>;
|
|
154
|
+
private url;
|
|
155
|
+
private postTorrent;
|
|
156
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { FetchError, ofetch, } from 'ofetch';
|
|
2
|
+
import { joinURL } from 'ufo';
|
|
3
|
+
import { base64ToUint8Array, stringToBase64 } from 'uint8array-extras';
|
|
4
|
+
import { normalizeTorrentData } from './normalizeTorrentData.js';
|
|
5
|
+
const defaults = {
|
|
6
|
+
baseUrl: 'http://localhost:3030/',
|
|
7
|
+
path: '/',
|
|
8
|
+
username: '',
|
|
9
|
+
password: '',
|
|
10
|
+
timeout: 5000,
|
|
11
|
+
addTimeout: 60_000,
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Error thrown when a request to rqbit fails, either an error response or a timeout/network error
|
|
15
|
+
*/
|
|
16
|
+
export class RqbitApiError extends Error {
|
|
17
|
+
name = 'RqbitApiError';
|
|
18
|
+
/**
|
|
19
|
+
* HTTP status code, undefined when no response was received (timeout or network error)
|
|
20
|
+
*/
|
|
21
|
+
status;
|
|
22
|
+
/**
|
|
23
|
+
* rqbit error kind, ex - `torrent_not_found`. Undefined when rqbit returned a plain text error.
|
|
24
|
+
*/
|
|
25
|
+
kind;
|
|
26
|
+
response;
|
|
27
|
+
constructor(status, message, response, cause) {
|
|
28
|
+
super(message, { cause });
|
|
29
|
+
this.status = status;
|
|
30
|
+
this.kind = response?.error_kind;
|
|
31
|
+
this.response = response;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* rqbit has no published api reference. `GET /` on the server lists every endpoint, see {@link Rqbit.getApiInfo}.
|
|
36
|
+
* Routes {@link https://github.com/ikatson/rqbit/blob/v9.0.1/crates/librqbit/src/http_api/handlers/mod.rs}
|
|
37
|
+
* Response types {@link https://github.com/ikatson/rqbit/blob/v9.0.1/crates/librqbit/src/api.rs}
|
|
38
|
+
*/
|
|
39
|
+
export class Rqbit {
|
|
40
|
+
static createFromState(config, state) {
|
|
41
|
+
const client = new Rqbit(config);
|
|
42
|
+
client.state = { ...state };
|
|
43
|
+
return client;
|
|
44
|
+
}
|
|
45
|
+
config;
|
|
46
|
+
state = {};
|
|
47
|
+
constructor(options = {}) {
|
|
48
|
+
this.config = { ...defaults, ...options };
|
|
49
|
+
}
|
|
50
|
+
exportState() {
|
|
51
|
+
return JSON.parse(JSON.stringify(this.state));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Lists the available api endpoints and the rqbit version
|
|
55
|
+
*/
|
|
56
|
+
async getApiInfo() {
|
|
57
|
+
return this.request('/');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* rqbit version, ex - `9.0.1`
|
|
61
|
+
*/
|
|
62
|
+
async getVersion() {
|
|
63
|
+
const res = await this.getApiInfo();
|
|
64
|
+
return res.version;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Session wide download/upload speed, peer and connection stats
|
|
68
|
+
*/
|
|
69
|
+
async getSessionStats() {
|
|
70
|
+
return this.request('/stats');
|
|
71
|
+
}
|
|
72
|
+
async getDhtStats() {
|
|
73
|
+
return this.request('/dht/stats');
|
|
74
|
+
}
|
|
75
|
+
async getRateLimits() {
|
|
76
|
+
return this.request('/torrents/limits');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Set session wide rate limits in bytes per second, null removes the limit
|
|
80
|
+
*/
|
|
81
|
+
async setRateLimits(limits) {
|
|
82
|
+
return this.request('/torrents/limits', { method: 'POST', body: limits });
|
|
83
|
+
}
|
|
84
|
+
async listTorrents(withStats = false) {
|
|
85
|
+
return this.request('/torrents', {
|
|
86
|
+
query: withStats ? { with_stats: true } : undefined,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Torrent details including the file list
|
|
91
|
+
*/
|
|
92
|
+
async getTorrentDetails(id) {
|
|
93
|
+
return this.request(`/torrents/${id}`);
|
|
94
|
+
}
|
|
95
|
+
async getTorrentStats(id) {
|
|
96
|
+
return this.request(`/torrents/${id}/stats/v1`);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Per peer stats, defaults to only live peers
|
|
100
|
+
*/
|
|
101
|
+
async getTorrentPeerStats(id, state = 'live') {
|
|
102
|
+
return this.request(`/torrents/${id}/peer_stats`, { query: { state } });
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Download the .torrent file for a torrent
|
|
106
|
+
*/
|
|
107
|
+
async getTorrentMetadata(id) {
|
|
108
|
+
const res = await this.request(`/torrents/${id}/metadata`, {
|
|
109
|
+
responseType: 'arrayBuffer',
|
|
110
|
+
});
|
|
111
|
+
return new Uint8Array(res);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Url to stream a file from a torrent, supports range requests. rqbit requires basic auth on this url when it is enabled.
|
|
115
|
+
* {@link https://github.com/ikatson/rqbit/blob/v9.0.1/crates/librqbit/src/http_api/handlers/streaming.rs}
|
|
116
|
+
* @param fileIndex index into the torrent's file list
|
|
117
|
+
*/
|
|
118
|
+
getStreamUrl(id, fileIndex) {
|
|
119
|
+
return this.url(`/torrents/${id}/stream/${fileIndex}`);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Url to an m3u8 playlist of a torrent's playable files, or every torrent's when no id is passed.
|
|
123
|
+
* rqbit requires basic auth on this url when it is enabled.
|
|
124
|
+
*/
|
|
125
|
+
getPlaylistUrl(id) {
|
|
126
|
+
return this.url(id === undefined ? '/torrents/playlist' : `/torrents/${id}/playlist`);
|
|
127
|
+
}
|
|
128
|
+
async pauseTorrent(id) {
|
|
129
|
+
return this.request(`/torrents/${id}/pause`, { method: 'POST' });
|
|
130
|
+
}
|
|
131
|
+
async resumeTorrent(id) {
|
|
132
|
+
return this.request(`/torrents/${id}/start`, { method: 'POST' });
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Remove a torrent
|
|
136
|
+
* @param removeData (default: false) If true, remove the downloaded files.
|
|
137
|
+
*/
|
|
138
|
+
async removeTorrent(id, removeData = false) {
|
|
139
|
+
const action = removeData ? 'delete' : 'forget';
|
|
140
|
+
return this.request(`/torrents/${id}/${action}`, { method: 'POST' });
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Change which files are downloaded
|
|
144
|
+
* @param fileIds indexes into the torrent's file list
|
|
145
|
+
*/
|
|
146
|
+
async setTorrentFiles(id, fileIds) {
|
|
147
|
+
return this.request(`/torrents/${id}/update_only_files`, {
|
|
148
|
+
method: 'POST',
|
|
149
|
+
body: { only_files: fileIds },
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Connect to additional peers
|
|
154
|
+
* @param peers ex - `['1.2.3.4:6881']`
|
|
155
|
+
*/
|
|
156
|
+
async addPeers(id, peers) {
|
|
157
|
+
return this.request(`/torrents/${id}/add_peers`, {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
body: peers.join('\n'),
|
|
160
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* rqbit does not support queueing
|
|
165
|
+
*/
|
|
166
|
+
async queueUp(_id) {
|
|
167
|
+
throw new Error('rqbit does not support queueing');
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* rqbit does not support queueing
|
|
171
|
+
*/
|
|
172
|
+
async queueDown(_id) {
|
|
173
|
+
throw new Error('rqbit does not support queueing');
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Add a magnet link or a url to a .torrent file.
|
|
177
|
+
* rqbit resolves magnet metadata from peers before it responds, so this can take a while, see {@link RqbitConfig.addTimeout}
|
|
178
|
+
* {@link https://github.com/ikatson/rqbit/blob/v9.0.1/crates/librqbit/src/http_api/handlers/torrents.rs}
|
|
179
|
+
*/
|
|
180
|
+
async addMagnet(url, options = {}) {
|
|
181
|
+
return this.postTorrent(url, true, options);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Add a torrent
|
|
185
|
+
* @param torrent .torrent file contents, or the file contents as a base64 string. Magnet links and urls are passed to {@link Rqbit.addMagnet}
|
|
186
|
+
*/
|
|
187
|
+
async addTorrent(torrent, options = {}) {
|
|
188
|
+
if (typeof torrent === 'string') {
|
|
189
|
+
if (isUrl(torrent)) {
|
|
190
|
+
return this.addMagnet(torrent, options);
|
|
191
|
+
}
|
|
192
|
+
return this.postTorrent(base64ToUint8Array(torrent), false, options);
|
|
193
|
+
}
|
|
194
|
+
return this.postTorrent(torrent, false, options);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Resolve a magnet link to .torrent file contents without adding it.
|
|
198
|
+
* Like adding, rqbit fetches the metadata from peers before it responds, see {@link RqbitConfig.addTimeout}
|
|
199
|
+
*/
|
|
200
|
+
async resolveMagnet(magnet, options = {}) {
|
|
201
|
+
const timeoutMs = options.timeout_ms ?? this.config.addTimeout;
|
|
202
|
+
const res = await this.request('/torrents/resolve_magnet', {
|
|
203
|
+
method: 'POST',
|
|
204
|
+
query: { timeout_ms: timeoutMs },
|
|
205
|
+
body: magnet,
|
|
206
|
+
// rqbit returns the torrent decoded as json when asked for json
|
|
207
|
+
headers: { 'Content-Type': 'text/plain', Accept: 'application/x-bittorrent' },
|
|
208
|
+
responseType: 'arrayBuffer',
|
|
209
|
+
timeout: timeoutMs + 5000,
|
|
210
|
+
});
|
|
211
|
+
return new Uint8Array(res);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Create a torrent from a folder on the rqbit server and start seeding it.
|
|
215
|
+
* rqbit must be started with `--http-api-allow-create` or `RQBIT_HTTP_API_ALLOW_CREATE=true`.
|
|
216
|
+
* @param folder path to the folder on the rqbit server
|
|
217
|
+
*/
|
|
218
|
+
async createTorrent(folder, options = {}) {
|
|
219
|
+
const magnet = await this.request('/torrents/create', {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
// repeated trackers=a&trackers=b
|
|
222
|
+
query: { output: 'magnet', ...options },
|
|
223
|
+
body: folder,
|
|
224
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
225
|
+
responseType: 'text',
|
|
226
|
+
});
|
|
227
|
+
// rqbit only sends the info hash in the magnet
|
|
228
|
+
const infoHash = new URL(magnet).searchParams.get('xt').replace('urn:btih:', '');
|
|
229
|
+
return { magnet, info_hash: infoHash };
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Add a torrent and return normalized torrent data.
|
|
233
|
+
* The add endpoint has no paused option, `startPaused` pauses the torrent after adding.
|
|
234
|
+
* rqbit does not support labels, `label` is ignored.
|
|
235
|
+
* {@link https://github.com/ikatson/rqbit/blob/v9.0.1/crates/librqbit/src/http_api_types.rs}
|
|
236
|
+
*/
|
|
237
|
+
async normalizedAddTorrent(torrent, options = {}) {
|
|
238
|
+
const res = await this.addTorrent(torrent);
|
|
239
|
+
const hash = res.details.info_hash;
|
|
240
|
+
if (options.startPaused) {
|
|
241
|
+
await this.pauseTorrent(hash);
|
|
242
|
+
}
|
|
243
|
+
return this.getTorrent(hash);
|
|
244
|
+
}
|
|
245
|
+
async getTorrent(id) {
|
|
246
|
+
const [details, stats] = await Promise.all([
|
|
247
|
+
this.getTorrentDetails(id),
|
|
248
|
+
this.getTorrentStats(id),
|
|
249
|
+
]);
|
|
250
|
+
return normalizeTorrentData({
|
|
251
|
+
id: details.id,
|
|
252
|
+
info_hash: details.info_hash,
|
|
253
|
+
name: details.name,
|
|
254
|
+
output_folder: details.output_folder,
|
|
255
|
+
total_pieces: details.total_pieces,
|
|
256
|
+
stats,
|
|
257
|
+
}, details.files);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* rqbit does not support labels so labels is always empty
|
|
261
|
+
*/
|
|
262
|
+
async getAllData() {
|
|
263
|
+
const res = await this.listTorrents(true);
|
|
264
|
+
return {
|
|
265
|
+
torrents: res.torrents.map(torrent => normalizeTorrentData(torrent)),
|
|
266
|
+
labels: [],
|
|
267
|
+
raw: res,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
async request(path, options = {}) {
|
|
271
|
+
const url = this.url(path);
|
|
272
|
+
const headers = new Headers(options.headers);
|
|
273
|
+
if (this.config.username || this.config.password) {
|
|
274
|
+
const auth = stringToBase64(`${this.config.username}:${this.config.password}`);
|
|
275
|
+
headers.set('Authorization', `Basic ${auth}`);
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
return await ofetch(url, {
|
|
279
|
+
timeout: this.config.timeout,
|
|
280
|
+
dispatcher: this.config.dispatcher,
|
|
281
|
+
retry: false,
|
|
282
|
+
...options,
|
|
283
|
+
headers,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
if (!(error instanceof FetchError)) {
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
if (!error.response) {
|
|
291
|
+
// ofetch aborted (timeout) or the request never reached rqbit
|
|
292
|
+
throw new RqbitApiError(undefined, error.message, undefined, error);
|
|
293
|
+
}
|
|
294
|
+
const data = error.data;
|
|
295
|
+
if (isRqbitError(data)) {
|
|
296
|
+
throw new RqbitApiError(error.response.status, data.human_readable, data, error);
|
|
297
|
+
}
|
|
298
|
+
// rqbit returns plain text for routing and query string errors
|
|
299
|
+
const message = typeof data === 'string' && data ? data : error.message;
|
|
300
|
+
throw new RqbitApiError(error.response.status, message, undefined, error);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
url(path) {
|
|
304
|
+
return joinURL(this.config.baseUrl, this.config.path, path);
|
|
305
|
+
}
|
|
306
|
+
async postTorrent(body, isUrlBody, options) {
|
|
307
|
+
const { only_files, initial_peers, ...rest } = options;
|
|
308
|
+
const timeoutMs = options.timeout_ms ?? this.config.addTimeout;
|
|
309
|
+
const query = {
|
|
310
|
+
overwrite: true,
|
|
311
|
+
is_url: isUrlBody,
|
|
312
|
+
...rest,
|
|
313
|
+
timeout_ms: timeoutMs,
|
|
314
|
+
};
|
|
315
|
+
if (only_files) {
|
|
316
|
+
query.only_files = only_files.join(',');
|
|
317
|
+
}
|
|
318
|
+
if (initial_peers) {
|
|
319
|
+
query.initial_peers = initial_peers.join(',');
|
|
320
|
+
}
|
|
321
|
+
return this.request('/torrents', {
|
|
322
|
+
method: 'POST',
|
|
323
|
+
query,
|
|
324
|
+
body,
|
|
325
|
+
headers: {
|
|
326
|
+
'Content-Type': isUrlBody ? 'text/plain' : 'application/x-bittorrent',
|
|
327
|
+
},
|
|
328
|
+
// leave room for rqbit to respond with its own timeout error
|
|
329
|
+
timeout: timeoutMs + 5000,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
function isUrl(str) {
|
|
334
|
+
return /^(magnet:|https?:\/\/)/i.test(str);
|
|
335
|
+
}
|
|
336
|
+
function isRqbitError(data) {
|
|
337
|
+
return typeof data === 'object' && data !== null && 'error_kind' in data;
|
|
338
|
+
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types are based on responses from rqbit 9.0.1.
|
|
3
|
+
* {@link https://github.com/ikatson/rqbit/blob/v9.0.1/crates/librqbit/src/api.rs}
|
|
4
|
+
* {@link https://github.com/ikatson/rqbit/blob/v9.0.1/crates/librqbit/webui/src/api-types.ts}
|
|
5
|
+
*
|
|
6
|
+
* Response from `GET /`
|
|
7
|
+
*/
|
|
8
|
+
export interface ApiRootResponse {
|
|
9
|
+
/**
|
|
10
|
+
* Map of `"METHOD /path"` to a description of the endpoint
|
|
11
|
+
*/
|
|
12
|
+
apis: Record<string, string>;
|
|
13
|
+
server: 'rqbit';
|
|
14
|
+
/**
|
|
15
|
+
* rqbit version, ex - `9.0.1`
|
|
16
|
+
*/
|
|
17
|
+
version: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Torrent id from rqbit, or the 40 character info hash
|
|
21
|
+
*/
|
|
22
|
+
export type TorrentIdOrHash = number | string;
|
|
23
|
+
/**
|
|
24
|
+
* {@link https://github.com/ikatson/rqbit/blob/v9.0.1/crates/librqbit/src/torrent_state/stats.rs}
|
|
25
|
+
*/
|
|
26
|
+
export interface Speed {
|
|
27
|
+
/**
|
|
28
|
+
* Speed in MiB/s (despite the name), multiply by 1024 * 1024 for bytes per second
|
|
29
|
+
*/
|
|
30
|
+
mbps: number;
|
|
31
|
+
/**
|
|
32
|
+
* ex - `7.56 MiB/s`
|
|
33
|
+
*/
|
|
34
|
+
human_readable: string;
|
|
35
|
+
}
|
|
36
|
+
export interface Duration {
|
|
37
|
+
secs: number;
|
|
38
|
+
nanos: number;
|
|
39
|
+
}
|
|
40
|
+
export interface DurationWithHumanReadable {
|
|
41
|
+
duration: Duration;
|
|
42
|
+
/**
|
|
43
|
+
* ex - `4m 1s`
|
|
44
|
+
*/
|
|
45
|
+
human_readable: string;
|
|
46
|
+
}
|
|
47
|
+
export interface AggregatePeerStats {
|
|
48
|
+
queued: number;
|
|
49
|
+
connecting: number;
|
|
50
|
+
live: number;
|
|
51
|
+
live_tcp: number;
|
|
52
|
+
live_utp: number;
|
|
53
|
+
live_socks: number;
|
|
54
|
+
seen: number;
|
|
55
|
+
dead: number;
|
|
56
|
+
not_needed: number;
|
|
57
|
+
steals: number;
|
|
58
|
+
}
|
|
59
|
+
export interface StatsSnapshot {
|
|
60
|
+
downloaded_and_checked_bytes: number;
|
|
61
|
+
fetched_bytes: number;
|
|
62
|
+
uploaded_bytes: number;
|
|
63
|
+
downloaded_and_checked_pieces: number;
|
|
64
|
+
total_piece_download_ms: number;
|
|
65
|
+
peer_stats: AggregatePeerStats;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Only present while the torrent is live
|
|
69
|
+
*/
|
|
70
|
+
export interface LiveStats {
|
|
71
|
+
snapshot: StatsSnapshot;
|
|
72
|
+
average_piece_download_time: Duration | null;
|
|
73
|
+
download_speed: Speed;
|
|
74
|
+
upload_speed: Speed;
|
|
75
|
+
time_remaining: DurationWithHumanReadable | null;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A torrent paused while initializing stays `initializing` with `initializing_paused: true` until it is started.
|
|
79
|
+
* {@link https://github.com/ikatson/rqbit/blob/v9.0.1/crates/librqbit/src/torrent_state/stats.rs}
|
|
80
|
+
*/
|
|
81
|
+
export type TorrentStatsState = 'initializing' | 'live' | 'paused' | 'error';
|
|
82
|
+
/**
|
|
83
|
+
* Response from `GET /torrents/{id}/stats/v1`
|
|
84
|
+
*/
|
|
85
|
+
export interface TorrentStats {
|
|
86
|
+
state: TorrentStatsState;
|
|
87
|
+
/**
|
|
88
|
+
* Only present when state is `initializing`
|
|
89
|
+
*/
|
|
90
|
+
initializing_paused?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Bytes downloaded per file, in file order
|
|
93
|
+
*/
|
|
94
|
+
file_progress: number[];
|
|
95
|
+
error: string | null;
|
|
96
|
+
progress_bytes: number;
|
|
97
|
+
uploaded_bytes: number;
|
|
98
|
+
/**
|
|
99
|
+
* Total bytes of the selected files
|
|
100
|
+
*/
|
|
101
|
+
total_bytes: number;
|
|
102
|
+
finished: boolean;
|
|
103
|
+
live: LiveStats | null;
|
|
104
|
+
}
|
|
105
|
+
export interface TorrentFileAttributes {
|
|
106
|
+
symlink: boolean;
|
|
107
|
+
hidden: boolean;
|
|
108
|
+
padding: boolean;
|
|
109
|
+
executable: boolean;
|
|
110
|
+
}
|
|
111
|
+
export interface TorrentFile {
|
|
112
|
+
name: string;
|
|
113
|
+
components: string[];
|
|
114
|
+
length: number;
|
|
115
|
+
/**
|
|
116
|
+
* Whether the file is selected for download
|
|
117
|
+
*/
|
|
118
|
+
included: boolean;
|
|
119
|
+
attributes: TorrentFileAttributes;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Response from `GET /torrents/{id}`
|
|
123
|
+
*/
|
|
124
|
+
export interface TorrentDetails {
|
|
125
|
+
id: number;
|
|
126
|
+
info_hash: string;
|
|
127
|
+
/**
|
|
128
|
+
* null until metadata is resolved
|
|
129
|
+
*/
|
|
130
|
+
name: string | null;
|
|
131
|
+
output_folder: string;
|
|
132
|
+
total_pieces: number;
|
|
133
|
+
files: TorrentFile[];
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Item from `GET /torrents`
|
|
137
|
+
*/
|
|
138
|
+
export interface TorrentListItem {
|
|
139
|
+
id: number;
|
|
140
|
+
info_hash: string;
|
|
141
|
+
name: string | null;
|
|
142
|
+
output_folder: string;
|
|
143
|
+
total_pieces: number;
|
|
144
|
+
/**
|
|
145
|
+
* Only present when listing with stats
|
|
146
|
+
*/
|
|
147
|
+
stats?: TorrentStats;
|
|
148
|
+
}
|
|
149
|
+
export interface TorrentWithStats extends TorrentListItem {
|
|
150
|
+
stats: TorrentStats;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Response from `GET /torrents`
|
|
154
|
+
*/
|
|
155
|
+
export interface ListTorrentsResponse<T extends TorrentListItem = TorrentListItem> {
|
|
156
|
+
torrents: T[];
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Response from `POST /torrents`
|
|
160
|
+
*/
|
|
161
|
+
export interface AddTorrentResponse {
|
|
162
|
+
/**
|
|
163
|
+
* null when added with `list_only`
|
|
164
|
+
*/
|
|
165
|
+
id: number | null;
|
|
166
|
+
details: TorrentDetails;
|
|
167
|
+
output_folder: string;
|
|
168
|
+
seen_peers: string[] | null;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Query options for `POST /torrents`
|
|
172
|
+
* {@link https://github.com/ikatson/rqbit/blob/v9.0.1/crates/librqbit/src/http_api_types.rs}
|
|
173
|
+
*/
|
|
174
|
+
export interface AddTorrentOptions {
|
|
175
|
+
/**
|
|
176
|
+
* Allow writing over existing files on disk, rqbit refuses to add a torrent whose files already exist without this.
|
|
177
|
+
* default: true
|
|
178
|
+
*/
|
|
179
|
+
overwrite: boolean;
|
|
180
|
+
/**
|
|
181
|
+
* Absolute folder to download into, defaults to the folder rqbit was started with
|
|
182
|
+
*/
|
|
183
|
+
output_folder: string;
|
|
184
|
+
/**
|
|
185
|
+
* Folder inside the output folder to download into
|
|
186
|
+
*/
|
|
187
|
+
sub_folder: string;
|
|
188
|
+
/**
|
|
189
|
+
* Only download files matching this regex
|
|
190
|
+
*/
|
|
191
|
+
only_files_regex: string;
|
|
192
|
+
/**
|
|
193
|
+
* Only download these file indexes
|
|
194
|
+
*/
|
|
195
|
+
only_files: number[];
|
|
196
|
+
/**
|
|
197
|
+
* Resolve the torrent and return its details without adding it
|
|
198
|
+
*/
|
|
199
|
+
list_only: boolean;
|
|
200
|
+
/**
|
|
201
|
+
* Extra peers to connect to, ex - `['1.2.3.4:6881']`
|
|
202
|
+
*/
|
|
203
|
+
initial_peers: string[];
|
|
204
|
+
/**
|
|
205
|
+
* Seconds
|
|
206
|
+
*/
|
|
207
|
+
peer_connect_timeout: number;
|
|
208
|
+
/**
|
|
209
|
+
* Seconds
|
|
210
|
+
*/
|
|
211
|
+
peer_read_write_timeout: number;
|
|
212
|
+
/**
|
|
213
|
+
* How long rqbit will wait to resolve magnet metadata before giving up, also used as the request timeout.
|
|
214
|
+
* default: {@link RqbitConfig.addTimeout}
|
|
215
|
+
*/
|
|
216
|
+
timeout_ms: number;
|
|
217
|
+
}
|
|
218
|
+
export interface PeerCounters {
|
|
219
|
+
incoming_connections: number;
|
|
220
|
+
fetched_bytes: number;
|
|
221
|
+
uploaded_bytes: number;
|
|
222
|
+
total_time_connecting_ms: number;
|
|
223
|
+
connection_attempts: number;
|
|
224
|
+
connections: number;
|
|
225
|
+
errors: number;
|
|
226
|
+
fetched_chunks: number;
|
|
227
|
+
downloaded_and_checked_pieces: number;
|
|
228
|
+
total_piece_download_ms: number;
|
|
229
|
+
times_stolen_from_me: number;
|
|
230
|
+
times_i_stole: number;
|
|
231
|
+
}
|
|
232
|
+
export type PeerState = 'queued' | 'connecting' | 'live' | 'dead' | 'not_needed';
|
|
233
|
+
export interface PeerStats {
|
|
234
|
+
counters: PeerCounters;
|
|
235
|
+
state: PeerState;
|
|
236
|
+
conn_kind: 'tcp' | 'utp' | 'socks' | null;
|
|
237
|
+
client_name: string | null;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Response from `GET /torrents/{id}/peer_stats`
|
|
241
|
+
*/
|
|
242
|
+
export interface PeerStatsResponse {
|
|
243
|
+
/**
|
|
244
|
+
* Keyed by `ip:port`
|
|
245
|
+
*/
|
|
246
|
+
peers: Record<string, PeerStats>;
|
|
247
|
+
}
|
|
248
|
+
export interface ConnectionStatSingle {
|
|
249
|
+
attempts: number;
|
|
250
|
+
successes: number;
|
|
251
|
+
errors: number;
|
|
252
|
+
}
|
|
253
|
+
export interface ConnectionStatsPerFamily {
|
|
254
|
+
v4: ConnectionStatSingle;
|
|
255
|
+
v6: ConnectionStatSingle;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Response from `GET /stats`
|
|
259
|
+
*/
|
|
260
|
+
export interface SessionStats {
|
|
261
|
+
counters: {
|
|
262
|
+
fetched_bytes: number;
|
|
263
|
+
uploaded_bytes: number;
|
|
264
|
+
blocked_incoming: number;
|
|
265
|
+
blocked_outgoing: number;
|
|
266
|
+
};
|
|
267
|
+
download_speed: Speed;
|
|
268
|
+
upload_speed: Speed;
|
|
269
|
+
peers: AggregatePeerStats;
|
|
270
|
+
uptime_seconds: number;
|
|
271
|
+
connections: {
|
|
272
|
+
tcp: ConnectionStatsPerFamily;
|
|
273
|
+
utp: ConnectionStatsPerFamily;
|
|
274
|
+
socks: ConnectionStatsPerFamily;
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Session wide rate limits, null is unlimited
|
|
279
|
+
*/
|
|
280
|
+
export interface RateLimits {
|
|
281
|
+
/**
|
|
282
|
+
* bytes per second
|
|
283
|
+
*/
|
|
284
|
+
upload_bps: number | null;
|
|
285
|
+
/**
|
|
286
|
+
* bytes per second
|
|
287
|
+
*/
|
|
288
|
+
download_bps: number | null;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* JSON error body returned by rqbit
|
|
292
|
+
*/
|
|
293
|
+
export interface RqbitErrorResponse {
|
|
294
|
+
/**
|
|
295
|
+
* ex - `torrent_not_found`, `internal_error`
|
|
296
|
+
*/
|
|
297
|
+
error_kind: string;
|
|
298
|
+
human_readable: string;
|
|
299
|
+
status: number;
|
|
300
|
+
status_text: string;
|
|
301
|
+
id?: string;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Empty response from actions like pause, start, forget and delete
|
|
305
|
+
*/
|
|
306
|
+
export type EmptyResponse = Record<string, never>;
|
|
307
|
+
/**
|
|
308
|
+
* Response from `GET /dht/stats`
|
|
309
|
+
*/
|
|
310
|
+
export interface DhtStats {
|
|
311
|
+
/**
|
|
312
|
+
* This node's DHT id
|
|
313
|
+
*/
|
|
314
|
+
id: string;
|
|
315
|
+
outstanding_requests: number;
|
|
316
|
+
routing_table_size: number;
|
|
317
|
+
routing_table_size_v6: number;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Query options for `POST /torrents/create`
|
|
321
|
+
* {@link https://github.com/ikatson/rqbit/blob/v9.0.1/crates/librqbit/src/http_api/handlers/torrents.rs}
|
|
322
|
+
*/
|
|
323
|
+
export interface CreateTorrentOptions {
|
|
324
|
+
/**
|
|
325
|
+
* Torrent name, defaults to the folder name
|
|
326
|
+
*/
|
|
327
|
+
name: string;
|
|
328
|
+
/**
|
|
329
|
+
* Announce urls
|
|
330
|
+
*/
|
|
331
|
+
trackers: string[];
|
|
332
|
+
}
|
|
333
|
+
export interface CreateTorrentResponse {
|
|
334
|
+
magnet: string;
|
|
335
|
+
info_hash: string;
|
|
336
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ctrl/rqbit",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "TypeScript api wrapper for rqbit using ofetch",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"rqbit",
|
|
7
|
+
"torrent",
|
|
8
|
+
"typescript"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://rqbit.ep.workers.dev",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "Scott Cooper <scttcper@gmail.com>",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/scttcper/rqbit.git"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist/src"
|
|
19
|
+
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"main": "./dist/src/index.js",
|
|
23
|
+
"types": "./dist/src/index.d.ts",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public",
|
|
26
|
+
"provenance": true
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"lint": "oxlint . && oxfmt --check",
|
|
30
|
+
"lint:fix": "oxlint . --fix && oxfmt",
|
|
31
|
+
"prepare": "pnpm run build",
|
|
32
|
+
"build": "tsc",
|
|
33
|
+
"build:docs": "node --import ./typedoc.mjs ./node_modules/typedoc/bin/typedoc",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"test:watch": "vitest"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@ctrl/shared-torrent": "^6.5.2",
|
|
39
|
+
"ofetch": "^1.5.1",
|
|
40
|
+
"type-fest": "^5.10.0",
|
|
41
|
+
"ufo": "^1.6.4",
|
|
42
|
+
"uint8array-extras": "^1.6.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@ctrl/oxlint-config": "1.5.0",
|
|
46
|
+
"@sindresorhus/tsconfig": "8.1.0",
|
|
47
|
+
"@types/node": "26.6.4",
|
|
48
|
+
"oxfmt": "0.71.0",
|
|
49
|
+
"oxlint": "1.86.0",
|
|
50
|
+
"p-wait-for": "6.0.0",
|
|
51
|
+
"typedoc": "0.28.20",
|
|
52
|
+
"typescript": "7.0.2",
|
|
53
|
+
"typescript6": "npm:typescript@6.0.3",
|
|
54
|
+
"vitest": "5.0.3"
|
|
55
|
+
},
|
|
56
|
+
"release": {
|
|
57
|
+
"branches": [
|
|
58
|
+
"master"
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
"engines": {
|
|
62
|
+
"node": ">=22"
|
|
63
|
+
},
|
|
64
|
+
"packageManager": "pnpm@12.9.1"
|
|
65
|
+
}
|