@ctrl/transmission 4.2.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 CHANGED
@@ -1,4 +1,4 @@
1
- # transmission [![npm](https://img.shields.io/npm/v/@ctrl/transmission.svg?maxAge=3600)](https://www.npmjs.com/package/@ctrl/transmission) [![CircleCI](https://circleci.com/gh/scttcper/transmission.svg?style=svg)](https://circleci.com/gh/scttcper/transmission) [![coverage status](https://codecov.io/gh/scttcper/transmission/branch/master/graph/badge.svg)](https://codecov.io/gh/scttcper/transmission)
1
+ # transmission [![npm](https://img.shields.io/npm/v/@ctrl/transmission.svg?maxAge=3600)](https://www.npmjs.com/package/@ctrl/transmission) [![coverage status](https://codecov.io/gh/scttcper/transmission/branch/master/graph/badge.svg)](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,3 @@
1
+ import type { NormalizedTorrent } from '@ctrl/shared-torrent';
2
+ import type { Torrent } from './types.js';
3
+ export declare function normalizeTorrentData(torrent: Torrent): NormalizedTorrent;
@@ -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 type { Response } from 'got';
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<Response<T>>;
56
+ request<T>(method: string, args?: any): Promise<FetchResponse<T>>;
57
57
  private _handleNormalizedIds;
58
- private _normalizeTorrentData;
59
58
  }
@@ -1,8 +1,7 @@
1
- import { existsSync, readFileSync } from 'fs';
2
- import got from 'got';
1
+ import { FetchError, ofetch } from 'ofetch';
2
+ import { joinURL } from 'ufo';
3
3
  import { magnetDecode } from '@ctrl/magnet-link';
4
- import { TorrentState } from '@ctrl/shared-torrent';
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.body;
20
+ return res._data;
32
21
  }
33
22
  async setSession(args) {
34
23
  const res = await this.request('session-set', args);
35
- return res.body;
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.body;
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.body;
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.body;
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.body;
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.body;
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.body;
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.body;
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.body;
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.body;
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.body;
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.body;
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.body;
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.body;
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.body;
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 = existsSync(torrent)
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.body;
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 this._normalizeTorrentData(result.arguments.torrents[0]);
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) => this._normalizeTorrentData(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) {
@@ -212,6 +199,7 @@ export class Transmission {
212
199
  const results = {
213
200
  torrents,
214
201
  labels,
202
+ raw: listTorrents,
215
203
  };
216
204
  return results;
217
205
  }
@@ -283,7 +271,7 @@ export class Transmission {
283
271
  args.ids = ids;
284
272
  }
285
273
  const res = await this.request('torrent-get', args);
286
- return res.body;
274
+ return res._data;
287
275
  }
288
276
  async request(method, args = {}) {
289
277
  if (!this.sessionId && method !== 'session-get') {
@@ -291,30 +279,41 @@ export class Transmission {
291
279
  }
292
280
  const headers = {
293
281
  'X-Transmission-Session-Id': this.sessionId,
282
+ 'Content-Type': 'application/json',
294
283
  };
295
284
  if (this.config.username || this.config.password) {
296
285
  const str = `${this.config.username ?? ''}:${this.config.password ?? ''}`;
297
286
  headers.Authorization = 'Basic ' + Buffer.from(str).toString('base64');
298
287
  }
299
- const url = urlJoin(this.config.baseUrl, this.config.path);
288
+ const url = joinURL(this.config.baseUrl, this.config.path);
300
289
  try {
301
- const res = await got.post(url, {
302
- json: {
290
+ const res = await ofetch.raw(url, {
291
+ method: 'POST',
292
+ body: JSON.stringify({
303
293
  method,
304
294
  arguments: args,
305
- },
295
+ }),
306
296
  headers,
307
- retry: { limit: 0 },
297
+ retry: 0,
308
298
  // allow proxy agent
309
- timeout: { request: this.config.timeout },
299
+ timeout: this.config.timeout,
310
300
  responseType: 'json',
311
- ...(this.config.agent ? { agent: this.config.agent } : {}),
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,
312
311
  });
313
312
  return res;
314
313
  }
315
314
  catch (error) {
316
- if (error?.response?.statusCode === 409) {
317
- this.sessionId = error.response.headers['x-transmission-session-id'];
315
+ if (error instanceof FetchError && error.response.status === 409) {
316
+ this.sessionId = error.response.headers.get('x-transmission-session-id');
318
317
  // eslint-disable-next-line no-return-await
319
318
  return await this.request(method, args);
320
319
  }
@@ -328,51 +327,4 @@ export class Transmission {
328
327
  }
329
328
  return ids;
330
329
  }
331
- _normalizeTorrentData(torrent) {
332
- const dateAdded = new Date(torrent.addedDate * 1000).toISOString();
333
- const dateCompleted = new Date(torrent.doneDate * 1000).toISOString();
334
- // normalize state to enum
335
- // https://github.com/transmission/transmission/blob/c11f2870fd18ff781ca06ce84b6d43541f3293dd/web/javascript/torrent.js#L18
336
- let state = TorrentState.unknown;
337
- if (torrent.status === 6) {
338
- state = TorrentState.seeding;
339
- }
340
- else if (torrent.status === 4) {
341
- state = TorrentState.downloading;
342
- }
343
- else if (torrent.status === 0) {
344
- state = TorrentState.paused;
345
- }
346
- else if (torrent.status === 2) {
347
- state = TorrentState.checking;
348
- }
349
- else if (torrent.status === 3 || torrent.status === 5) {
350
- state = TorrentState.queued;
351
- }
352
- return {
353
- id: torrent.hashString,
354
- name: torrent.name,
355
- state,
356
- isCompleted: torrent.leftUntilDone < 1,
357
- stateMessage: '',
358
- progress: torrent.percentDone,
359
- ratio: torrent.uploadRatio,
360
- dateAdded,
361
- dateCompleted,
362
- label: torrent.labels?.length ? torrent.labels[0] : undefined,
363
- savePath: torrent.downloadDir,
364
- uploadSpeed: torrent.rateUpload,
365
- downloadSpeed: torrent.rateDownload,
366
- eta: torrent.eta,
367
- queuePosition: torrent.queuePosition,
368
- connectedPeers: torrent.peersSendingToUs,
369
- connectedSeeds: torrent.peersGettingFromUs,
370
- totalPeers: torrent.peersConnected,
371
- totalSeeds: torrent.peersConnected,
372
- totalSelected: torrent.sizeWhenDone,
373
- totalSize: torrent.totalSize,
374
- totalUploaded: torrent.uploadedEver,
375
- totalDownloaded: torrent.downloadedEver,
376
- };
377
- }
378
330
  }
@@ -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 declare type TorrentIds = number | 'recently-active' | Array<number | string>;
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 declare type NormalizedTorrentIds = TorrentIds | string;
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": "4.2.0",
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.1",
31
- "@ctrl/shared-torrent": "^4.2.0",
32
- "@ctrl/url-join": "^2.0.2",
33
- "got": "^12.3.1"
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": "3.4.10",
37
- "@sindresorhus/tsconfig": "3.0.1",
38
- "@types/node": "18.7.14",
39
- "@vitest/coverage-c8": "0.22.1",
40
- "c8": "7.12.0",
41
- "p-wait-for": "5.0.0",
42
- "typedoc": "0.23.12",
43
- "typescript": "4.8.2",
44
- "vitest": "0.22.1"
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": ">=14.16"
55
+ "node": ">=18"
60
56
  }
61
57
  }