@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 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) {
@@ -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.body;
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 = urlJoin(this.config.baseUrl, this.config.path);
288
+ const url = joinURL(this.config.baseUrl, this.config.path);
301
289
  try {
302
- const res = await got.post(url, {
303
- json: {
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: { limit: 0 },
297
+ retry: 0,
309
298
  // allow proxy agent
310
- timeout: { request: this.config.timeout },
299
+ timeout: this.config.timeout,
311
300
  responseType: 'json',
312
- ...(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,
313
311
  });
314
312
  return res;
315
313
  }
316
314
  catch (error) {
317
- if (error?.response?.statusCode === 409) {
318
- 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');
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
  }
@@ -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.3.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.3.1",
32
- "@ctrl/url-join": "^2.0.2",
33
- "got": "^12.5.0"
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.5.0",
37
- "@sindresorhus/tsconfig": "3.0.1",
38
- "@types/node": "18.7.18",
39
- "@vitest/coverage-c8": "0.23.4",
40
- "c8": "7.12.0",
41
- "p-wait-for": "5.0.0",
42
- "typedoc": "0.23.15",
43
- "typescript": "4.8.3",
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": ">=14.16"
55
+ "node": ">=18"
60
56
  }
61
57
  }