@ctrl/transmission 4.3.0 → 5.0.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 +19 -1
- package/dist/src/normalizeTorrentData.d.ts +3 -0
- package/dist/src/normalizeTorrentData.js +49 -0
- package/dist/src/transmission.d.ts +2 -3
- package/dist/src/transmission.js +46 -96
- package/dist/src/types.d.ts +2 -2
- package/package.json +17 -21
package/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# transmission [](https://www.npmjs.com/package/@ctrl/transmission) [](https://www.npmjs.com/package/@ctrl/transmission) [](https://codecov.io/gh/scttcper/transmission)
|
2
2
|
|
3
3
|
> TypeScript api wrapper for [transmission](https://transmissionbt.com/) using [got](https://github.com/sindresorhus/got)
|
4
4
|
|
@@ -77,3 +77,21 @@ All of the following npm modules provide the same normalized functions along wit
|
|
77
77
|
deluge - https://github.com/scttcper/deluge
|
78
78
|
qbittorrent - https://github.com/scttcper/qbittorrent
|
79
79
|
utorrent - https://github.com/scttcper/utorrent
|
80
|
+
|
81
|
+
### Start a test docker container
|
82
|
+
|
83
|
+
```
|
84
|
+
docker run -d \
|
85
|
+
--name=transmission \
|
86
|
+
-e PUID=1000 \
|
87
|
+
-e PGID=1000 \
|
88
|
+
-e TZ=Etc/UTC \
|
89
|
+
-p 9091:9091 \
|
90
|
+
-p 51413:51413 \
|
91
|
+
-p 51413:51413/udp \
|
92
|
+
-v ~/Documents/transmission/config:/config \
|
93
|
+
-v ~/Documents/transmission/downloads:/downloads \
|
94
|
+
-v ~/Documents/transmission/watch:/watch \
|
95
|
+
--restart unless-stopped \
|
96
|
+
lscr.io/linuxserver/transmission:latest
|
97
|
+
```
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import { TorrentState } from '@ctrl/shared-torrent';
|
2
|
+
export function normalizeTorrentData(torrent) {
|
3
|
+
const dateAdded = new Date(torrent.addedDate * 1000).toISOString();
|
4
|
+
const dateCompleted = new Date(torrent.doneDate * 1000).toISOString();
|
5
|
+
// normalize state to enum
|
6
|
+
// https://github.com/transmission/transmission/blob/c11f2870fd18ff781ca06ce84b6d43541f3293dd/web/javascript/torrent.js#L18
|
7
|
+
let state = TorrentState.unknown;
|
8
|
+
if (torrent.status === 6) {
|
9
|
+
state = TorrentState.seeding;
|
10
|
+
}
|
11
|
+
else if (torrent.status === 4) {
|
12
|
+
state = TorrentState.downloading;
|
13
|
+
}
|
14
|
+
else if (torrent.status === 0) {
|
15
|
+
state = TorrentState.paused;
|
16
|
+
}
|
17
|
+
else if (torrent.status === 2) {
|
18
|
+
state = TorrentState.checking;
|
19
|
+
}
|
20
|
+
else if (torrent.status === 3 || torrent.status === 5) {
|
21
|
+
state = TorrentState.queued;
|
22
|
+
}
|
23
|
+
return {
|
24
|
+
id: torrent.hashString,
|
25
|
+
name: torrent.name,
|
26
|
+
state,
|
27
|
+
isCompleted: torrent.leftUntilDone < 1,
|
28
|
+
stateMessage: '',
|
29
|
+
progress: torrent.percentDone,
|
30
|
+
ratio: torrent.uploadRatio,
|
31
|
+
dateAdded,
|
32
|
+
dateCompleted,
|
33
|
+
label: torrent.labels?.length ? torrent.labels[0] : undefined,
|
34
|
+
savePath: torrent.downloadDir,
|
35
|
+
uploadSpeed: torrent.rateUpload,
|
36
|
+
downloadSpeed: torrent.rateDownload,
|
37
|
+
eta: torrent.eta,
|
38
|
+
queuePosition: torrent.queuePosition,
|
39
|
+
connectedPeers: torrent.peersSendingToUs,
|
40
|
+
connectedSeeds: torrent.peersGettingFromUs,
|
41
|
+
totalPeers: torrent.peersConnected,
|
42
|
+
totalSeeds: torrent.peersConnected,
|
43
|
+
totalSelected: torrent.sizeWhenDone,
|
44
|
+
totalSize: torrent.totalSize,
|
45
|
+
totalUploaded: torrent.uploadedEver,
|
46
|
+
totalDownloaded: torrent.downloadedEver,
|
47
|
+
raw: torrent,
|
48
|
+
};
|
49
|
+
}
|
@@ -1,5 +1,5 @@
|
|
1
1
|
/// <reference types="node" resolution-mode="require"/>
|
2
|
-
import
|
2
|
+
import { FetchResponse } from 'ofetch';
|
3
3
|
import type { AddTorrentOptions as NormalizedAddTorrentOptions, AllClientData, NormalizedTorrent, TorrentClient, TorrentSettings } from '@ctrl/shared-torrent';
|
4
4
|
import type { AddTorrentOptions, AddTorrentResponse, DefaultResponse, FreeSpaceResponse, GetTorrentRepsonse, NormalizedTorrentIds, RenamePathOptions, SessionArguments, SessionResponse, SetTorrentOptions } from './types.js';
|
5
5
|
export declare class Transmission implements TorrentClient {
|
@@ -53,7 +53,6 @@ export declare class Transmission implements TorrentClient {
|
|
53
53
|
getTorrent(id: NormalizedTorrentIds): Promise<NormalizedTorrent>;
|
54
54
|
getAllData(): Promise<AllClientData>;
|
55
55
|
listTorrents(id?: NormalizedTorrentIds, additionalFields?: string[]): Promise<GetTorrentRepsonse>;
|
56
|
-
request<T>(method: string, args?: any): Promise<
|
56
|
+
request<T>(method: string, args?: any): Promise<FetchResponse<T>>;
|
57
57
|
private _handleNormalizedIds;
|
58
|
-
private _normalizeTorrentData;
|
59
58
|
}
|
package/dist/src/transmission.js
CHANGED
@@ -1,8 +1,7 @@
|
|
1
|
-
import {
|
2
|
-
import
|
1
|
+
import { FetchError, ofetch } from 'ofetch';
|
2
|
+
import { joinURL } from 'ufo';
|
3
3
|
import { magnetDecode } from '@ctrl/magnet-link';
|
4
|
-
import {
|
5
|
-
import { urlJoin } from '@ctrl/url-join';
|
4
|
+
import { normalizeTorrentData } from './normalizeTorrentData.js';
|
6
5
|
const defaults = {
|
7
6
|
baseUrl: 'http://localhost:9091/',
|
8
7
|
path: '/transmission/rpc',
|
@@ -11,67 +10,57 @@ const defaults = {
|
|
11
10
|
timeout: 5000,
|
12
11
|
};
|
13
12
|
export class Transmission {
|
13
|
+
config;
|
14
|
+
sessionId;
|
14
15
|
constructor(options = {}) {
|
15
|
-
Object.defineProperty(this, "config", {
|
16
|
-
enumerable: true,
|
17
|
-
configurable: true,
|
18
|
-
writable: true,
|
19
|
-
value: void 0
|
20
|
-
});
|
21
|
-
Object.defineProperty(this, "sessionId", {
|
22
|
-
enumerable: true,
|
23
|
-
configurable: true,
|
24
|
-
writable: true,
|
25
|
-
value: void 0
|
26
|
-
});
|
27
16
|
this.config = { ...defaults, ...options };
|
28
17
|
}
|
29
18
|
async getSession() {
|
30
19
|
const res = await this.request('session-get');
|
31
|
-
return res.
|
20
|
+
return res._data;
|
32
21
|
}
|
33
22
|
async setSession(args) {
|
34
23
|
const res = await this.request('session-set', args);
|
35
|
-
return res.
|
24
|
+
return res._data;
|
36
25
|
}
|
37
26
|
async queueTop(id) {
|
38
27
|
const ids = this._handleNormalizedIds(id);
|
39
28
|
const res = await this.request('queue-move-top', { ids });
|
40
|
-
return res.
|
29
|
+
return res._data;
|
41
30
|
}
|
42
31
|
async queueBottom(id) {
|
43
32
|
const ids = this._handleNormalizedIds(id);
|
44
33
|
const res = await this.request('queue-move-bottom', { ids });
|
45
|
-
return res.
|
34
|
+
return res._data;
|
46
35
|
}
|
47
36
|
async queueUp(id) {
|
48
37
|
const ids = this._handleNormalizedIds(id);
|
49
38
|
const res = await this.request('queue-move-up', { ids });
|
50
|
-
return res.
|
39
|
+
return res._data;
|
51
40
|
}
|
52
41
|
async queueDown(id) {
|
53
42
|
const ids = this._handleNormalizedIds(id);
|
54
43
|
const res = await this.request('queue-move-down', { ids });
|
55
|
-
return res.
|
44
|
+
return res._data;
|
56
45
|
}
|
57
46
|
async freeSpace(path = '/downloads/complete') {
|
58
47
|
const res = await this.request('free-space', { path });
|
59
|
-
return res.
|
48
|
+
return res._data;
|
60
49
|
}
|
61
50
|
async pauseTorrent(id) {
|
62
51
|
const ids = this._handleNormalizedIds(id);
|
63
52
|
const res = await this.request('torrent-stop', { ids });
|
64
|
-
return res.
|
53
|
+
return res._data;
|
65
54
|
}
|
66
55
|
async resumeTorrent(id) {
|
67
56
|
const ids = this._handleNormalizedIds(id);
|
68
57
|
const res = await this.request('torrent-start', { ids });
|
69
|
-
return res.
|
58
|
+
return res._data;
|
70
59
|
}
|
71
60
|
async verifyTorrent(id) {
|
72
61
|
const ids = this._handleNormalizedIds(id);
|
73
62
|
const res = await this.request('torrent-verify', { ids });
|
74
|
-
return res.
|
63
|
+
return res._data;
|
75
64
|
}
|
76
65
|
/**
|
77
66
|
* ask tracker for more peers
|
@@ -79,7 +68,7 @@ export class Transmission {
|
|
79
68
|
async reannounceTorrent(id) {
|
80
69
|
const ids = this._handleNormalizedIds(id);
|
81
70
|
const res = await this.request('torrent-reannounce', { ids });
|
82
|
-
return res.
|
71
|
+
return res._data;
|
83
72
|
}
|
84
73
|
async moveTorrent(id, location) {
|
85
74
|
const ids = this._handleNormalizedIds(id);
|
@@ -88,7 +77,7 @@ export class Transmission {
|
|
88
77
|
move: true,
|
89
78
|
location,
|
90
79
|
});
|
91
|
-
return res.
|
80
|
+
return res._data;
|
92
81
|
}
|
93
82
|
/**
|
94
83
|
* Torrent Mutators
|
@@ -97,7 +86,7 @@ export class Transmission {
|
|
97
86
|
const ids = this._handleNormalizedIds(id);
|
98
87
|
options.ids = ids;
|
99
88
|
const res = await this.request('torrent-set', options);
|
100
|
-
return res.
|
89
|
+
return res._data;
|
101
90
|
}
|
102
91
|
/**
|
103
92
|
* Renaming a Torrent's Path
|
@@ -106,7 +95,7 @@ export class Transmission {
|
|
106
95
|
const ids = this._handleNormalizedIds(id);
|
107
96
|
options.ids = ids;
|
108
97
|
const res = await this.request('torrent-rename-path', options);
|
109
|
-
return res.
|
98
|
+
return res._data;
|
110
99
|
}
|
111
100
|
/**
|
112
101
|
* Removing a Torrent
|
@@ -117,7 +106,7 @@ export class Transmission {
|
|
117
106
|
ids,
|
118
107
|
'delete-local-data': removeData,
|
119
108
|
});
|
120
|
-
return res.
|
109
|
+
return res._data;
|
121
110
|
}
|
122
111
|
/**
|
123
112
|
* An alias for {@link Transmission.addMagnet}
|
@@ -139,7 +128,7 @@ export class Transmission {
|
|
139
128
|
};
|
140
129
|
args.filename = url;
|
141
130
|
const res = await this.request('torrent-add', args);
|
142
|
-
return res.
|
131
|
+
return res._data;
|
143
132
|
}
|
144
133
|
/**
|
145
134
|
* Adding a torrent
|
@@ -152,15 +141,13 @@ export class Transmission {
|
|
152
141
|
...options,
|
153
142
|
};
|
154
143
|
if (typeof torrent === 'string') {
|
155
|
-
args.metainfo =
|
156
|
-
? Buffer.from(readFileSync(torrent)).toString('base64')
|
157
|
-
: Buffer.from(torrent, 'base64').toString('base64');
|
144
|
+
args.metainfo = Buffer.from(torrent, 'base64').toString('base64');
|
158
145
|
}
|
159
146
|
else {
|
160
147
|
args.metainfo = torrent.toString('base64');
|
161
148
|
}
|
162
149
|
const res = await this.request('torrent-add', args);
|
163
|
-
return res.
|
150
|
+
return res._data;
|
164
151
|
}
|
165
152
|
async normalizedAddTorrent(torrent, options = {}) {
|
166
153
|
const torrentOptions = {};
|
@@ -192,11 +179,11 @@ export class Transmission {
|
|
192
179
|
if (!result.arguments.torrents || result.arguments.torrents.length === 0) {
|
193
180
|
throw new Error('Torrent not found');
|
194
181
|
}
|
195
|
-
return
|
182
|
+
return normalizeTorrentData(result.arguments.torrents[0]);
|
196
183
|
}
|
197
184
|
async getAllData() {
|
198
185
|
const listTorrents = await this.listTorrents();
|
199
|
-
const torrents = listTorrents.arguments.torrents.map((n) =>
|
186
|
+
const torrents = listTorrents.arguments.torrents.map((n) => normalizeTorrentData(n));
|
200
187
|
const labels = [];
|
201
188
|
for (const torrent of torrents) {
|
202
189
|
if (!torrent.label) {
|
@@ -284,7 +271,7 @@ export class Transmission {
|
|
284
271
|
args.ids = ids;
|
285
272
|
}
|
286
273
|
const res = await this.request('torrent-get', args);
|
287
|
-
return res.
|
274
|
+
return res._data;
|
288
275
|
}
|
289
276
|
async request(method, args = {}) {
|
290
277
|
if (!this.sessionId && method !== 'session-get') {
|
@@ -292,30 +279,41 @@ export class Transmission {
|
|
292
279
|
}
|
293
280
|
const headers = {
|
294
281
|
'X-Transmission-Session-Id': this.sessionId,
|
282
|
+
'Content-Type': 'application/json',
|
295
283
|
};
|
296
284
|
if (this.config.username || this.config.password) {
|
297
285
|
const str = `${this.config.username ?? ''}:${this.config.password ?? ''}`;
|
298
286
|
headers.Authorization = 'Basic ' + Buffer.from(str).toString('base64');
|
299
287
|
}
|
300
|
-
const url =
|
288
|
+
const url = joinURL(this.config.baseUrl, this.config.path);
|
301
289
|
try {
|
302
|
-
const res = await
|
303
|
-
|
290
|
+
const res = await ofetch.raw(url, {
|
291
|
+
method: 'POST',
|
292
|
+
body: JSON.stringify({
|
304
293
|
method,
|
305
294
|
arguments: args,
|
306
|
-
},
|
295
|
+
}),
|
307
296
|
headers,
|
308
|
-
retry:
|
297
|
+
retry: 0,
|
309
298
|
// allow proxy agent
|
310
|
-
timeout:
|
299
|
+
timeout: this.config.timeout,
|
311
300
|
responseType: 'json',
|
312
|
-
|
301
|
+
parseResponse(body) {
|
302
|
+
try {
|
303
|
+
return JSON.parse(body);
|
304
|
+
}
|
305
|
+
catch (error) {
|
306
|
+
return body;
|
307
|
+
}
|
308
|
+
},
|
309
|
+
// @ts-expect-error agent is not in the type
|
310
|
+
agent: this.config.agent,
|
313
311
|
});
|
314
312
|
return res;
|
315
313
|
}
|
316
314
|
catch (error) {
|
317
|
-
if (error
|
318
|
-
this.sessionId = error.response.headers
|
315
|
+
if (error instanceof FetchError && error.response.status === 409) {
|
316
|
+
this.sessionId = error.response.headers.get('x-transmission-session-id');
|
319
317
|
// eslint-disable-next-line no-return-await
|
320
318
|
return await this.request(method, args);
|
321
319
|
}
|
@@ -329,52 +327,4 @@ export class Transmission {
|
|
329
327
|
}
|
330
328
|
return ids;
|
331
329
|
}
|
332
|
-
_normalizeTorrentData(torrent) {
|
333
|
-
const dateAdded = new Date(torrent.addedDate * 1000).toISOString();
|
334
|
-
const dateCompleted = new Date(torrent.doneDate * 1000).toISOString();
|
335
|
-
// normalize state to enum
|
336
|
-
// https://github.com/transmission/transmission/blob/c11f2870fd18ff781ca06ce84b6d43541f3293dd/web/javascript/torrent.js#L18
|
337
|
-
let state = TorrentState.unknown;
|
338
|
-
if (torrent.status === 6) {
|
339
|
-
state = TorrentState.seeding;
|
340
|
-
}
|
341
|
-
else if (torrent.status === 4) {
|
342
|
-
state = TorrentState.downloading;
|
343
|
-
}
|
344
|
-
else if (torrent.status === 0) {
|
345
|
-
state = TorrentState.paused;
|
346
|
-
}
|
347
|
-
else if (torrent.status === 2) {
|
348
|
-
state = TorrentState.checking;
|
349
|
-
}
|
350
|
-
else if (torrent.status === 3 || torrent.status === 5) {
|
351
|
-
state = TorrentState.queued;
|
352
|
-
}
|
353
|
-
return {
|
354
|
-
id: torrent.hashString,
|
355
|
-
name: torrent.name,
|
356
|
-
state,
|
357
|
-
isCompleted: torrent.leftUntilDone < 1,
|
358
|
-
stateMessage: '',
|
359
|
-
progress: torrent.percentDone,
|
360
|
-
ratio: torrent.uploadRatio,
|
361
|
-
dateAdded,
|
362
|
-
dateCompleted,
|
363
|
-
label: torrent.labels?.length ? torrent.labels[0] : undefined,
|
364
|
-
savePath: torrent.downloadDir,
|
365
|
-
uploadSpeed: torrent.rateUpload,
|
366
|
-
downloadSpeed: torrent.rateDownload,
|
367
|
-
eta: torrent.eta,
|
368
|
-
queuePosition: torrent.queuePosition,
|
369
|
-
connectedPeers: torrent.peersSendingToUs,
|
370
|
-
connectedSeeds: torrent.peersGettingFromUs,
|
371
|
-
totalPeers: torrent.peersConnected,
|
372
|
-
totalSeeds: torrent.peersConnected,
|
373
|
-
totalSelected: torrent.sizeWhenDone,
|
374
|
-
totalSize: torrent.totalSize,
|
375
|
-
totalUploaded: torrent.uploadedEver,
|
376
|
-
totalDownloaded: torrent.downloadedEver,
|
377
|
-
raw: torrent,
|
378
|
-
};
|
379
|
-
}
|
380
330
|
}
|
package/dist/src/types.d.ts
CHANGED
@@ -45,11 +45,11 @@ export interface FreeSpaceResponse extends DefaultResponse {
|
|
45
45
|
* 2. a list of torrent id numbers, sha1 hash strings, or both
|
46
46
|
* 3. a string, "recently-active", for recently-active torrents
|
47
47
|
*/
|
48
|
-
export
|
48
|
+
export type TorrentIds = number | 'recently-active' | Array<number | string>;
|
49
49
|
/**
|
50
50
|
* Allows the user to pass a single hash, this will be converted to an array
|
51
51
|
*/
|
52
|
-
export
|
52
|
+
export type NormalizedTorrentIds = TorrentIds | string;
|
53
53
|
export interface GetTorrentRepsonse extends DefaultResponse {
|
54
54
|
arguments: {
|
55
55
|
removed: Torrent[];
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@ctrl/transmission",
|
3
|
-
"version": "
|
3
|
+
"version": "5.0.0",
|
4
4
|
"description": "TypeScript api wrapper for transmission using got",
|
5
5
|
"author": "Scott Cooper <scttcper@gmail.com>",
|
6
6
|
"license": "MIT",
|
@@ -24,31 +24,27 @@
|
|
24
24
|
"build:docs": "typedoc",
|
25
25
|
"test": "vitest run",
|
26
26
|
"test:watch": "vitest",
|
27
|
-
"test:ci": "vitest run --coverage"
|
27
|
+
"test:ci": "vitest run --coverage --reporter=default --reporter=junit --outputFile=./junit.xml"
|
28
28
|
},
|
29
29
|
"dependencies": {
|
30
|
-
"@ctrl/magnet-link": "^3.1.
|
31
|
-
"@ctrl/shared-torrent": "^
|
32
|
-
"
|
33
|
-
"
|
30
|
+
"@ctrl/magnet-link": "^3.1.2",
|
31
|
+
"@ctrl/shared-torrent": "^5.0.0",
|
32
|
+
"ofetch": "^1.3.3",
|
33
|
+
"ufo": "^1.3.0"
|
34
34
|
},
|
35
35
|
"devDependencies": {
|
36
|
-
"@ctrl/eslint-config": "
|
37
|
-
"@sindresorhus/tsconfig": "
|
38
|
-
"@types/node": "
|
39
|
-
"@vitest/coverage-
|
40
|
-
"
|
41
|
-
"
|
42
|
-
"
|
43
|
-
"
|
44
|
-
"vitest": "0.23.4"
|
45
|
-
},
|
46
|
-
"jest": {
|
47
|
-
"testEnvironment": "node",
|
48
|
-
"coverageProvider": "v8"
|
36
|
+
"@ctrl/eslint-config": "4.0.7",
|
37
|
+
"@sindresorhus/tsconfig": "4.0.0",
|
38
|
+
"@types/node": "20.6.5",
|
39
|
+
"@vitest/coverage-v8": "0.34.5",
|
40
|
+
"p-wait-for": "5.0.2",
|
41
|
+
"typedoc": "0.25.1",
|
42
|
+
"typescript": "5.2.2",
|
43
|
+
"vitest": "0.34.5"
|
49
44
|
},
|
50
45
|
"publishConfig": {
|
51
|
-
"access": "public"
|
46
|
+
"access": "public",
|
47
|
+
"provenance": true
|
52
48
|
},
|
53
49
|
"release": {
|
54
50
|
"branches": [
|
@@ -56,6 +52,6 @@
|
|
56
52
|
]
|
57
53
|
},
|
58
54
|
"engines": {
|
59
|
-
"node": ">=
|
55
|
+
"node": ">=18"
|
60
56
|
}
|
61
57
|
}
|