@ctrl/magnet-link 3.0.0 → 3.1.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.
@@ -0,0 +1,2 @@
1
+ export declare function composeRange(range: number[]): string[];
2
+ export declare function parseRange(range: string[]): number[];
@@ -0,0 +1,23 @@
1
+ /* eslint-disable @typescript-eslint/restrict-plus-operands */
2
+ export function composeRange(range) {
3
+ return range
4
+ .reduce((acc, cur, idx, arr) => {
5
+ // @ts-expect-error
6
+ if (idx === 0 || cur !== arr[idx - 1] + 1) {
7
+ acc.push([]);
8
+ }
9
+ // @ts-expect-error
10
+ acc[acc.length - 1].push(cur);
11
+ return acc;
12
+ }, [])
13
+ .map(cur => {
14
+ return cur.length > 1 ? `${cur[0]}-${cur[cur.length - 1]}` : `${cur[0]}`;
15
+ });
16
+ }
17
+ export function parseRange(range) {
18
+ const generateRange = (start, end = start) => Array.from({ length: end - start + 1 }, (_, idx) => idx + start);
19
+ return range.reduce((acc, cur) => {
20
+ const r = cur.split('-').map(cur => parseInt(cur, 10));
21
+ return acc.concat(generateRange(r[0], r[1]));
22
+ }, []);
23
+ }
@@ -1,3 +1,4 @@
1
+ /// <reference types="node" />
1
2
  export interface MagnetData {
2
3
  /**
3
4
  * Is the info-hash hex encoded, for a total of 40 characters. For compatability with existing links in the wild, clients should also support the 32 character base32 encoded info-hash.
@@ -12,6 +13,9 @@ export interface MagnetData {
12
13
  * Parsed xt= parameter see xt
13
14
  */
14
15
  infoHash?: string;
16
+ infoHashBuffer?: Buffer;
17
+ infoHashV2?: string;
18
+ infoHashV2Buffer?: Buffer;
15
19
  /**
16
20
  * The display name that may be used by the client to display while waiting for metadata
17
21
  */
@@ -44,6 +48,8 @@ export interface MagnetData {
44
48
  * "keyword topic": a more general search, specifying search terms rather than a particular file
45
49
  */
46
50
  kt?: string[];
51
+ so?: string[] | number[];
52
+ 'x.pe'?: string | string[];
47
53
  /**
48
54
  * "keyword topic": a more general search, specifying search terms rather than a particular file
49
55
  */
@@ -60,6 +66,9 @@ export interface MagnetData {
60
66
  * Combined as= and ws= parameters if they exist
61
67
  */
62
68
  urlList?: string[];
69
+ peerAddresses?: string[];
70
+ publicKey?: string;
71
+ publicKeyBuffer?: Buffer;
63
72
  }
64
73
  export declare function magnetDecode(uri: string): MagnetData;
65
74
  export declare function magnetEncode(data: MagnetData): string;
package/dist/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { base32Decode } from '@ctrl/ts-base32';
2
+ import * as bep53Range from './bep53.js';
2
3
  const start = 'magnet:?';
3
4
  export function magnetDecode(uri) {
4
5
  // Support 'stream-magnet:' as well
@@ -41,8 +42,30 @@ export function magnetDecode(uri) {
41
42
  const decodedStr = base32Decode(m[1]);
42
43
  result.infoHash = Buffer.from(decodedStr).toString('hex');
43
44
  }
45
+ else if ((m = xt.match(/^urn:btmh:1220(.{64})/))) {
46
+ result.infoHashV2 = m[1].toLowerCase();
47
+ }
44
48
  });
45
49
  }
50
+ if (result.xs) {
51
+ let m;
52
+ const xss = Array.isArray(result.xs) ? result.xs : [result.xs];
53
+ xss.forEach(xs => {
54
+ var _a;
55
+ if ((m = /^urn:btpk:(.{64})/.exec(xs))) {
56
+ result.publicKey = (_a = m[1]) === null || _a === void 0 ? void 0 : _a.toLowerCase();
57
+ }
58
+ });
59
+ }
60
+ if (result.infoHash) {
61
+ result.infoHashBuffer = Buffer.from(result.infoHash, 'hex');
62
+ }
63
+ if (result.infoHashV2) {
64
+ result.infoHashV2Buffer = Buffer.from(result.infoHashV2, 'hex');
65
+ }
66
+ if (result.publicKey) {
67
+ result.publicKeyBuffer = Buffer.from(result.publicKey, 'hex');
68
+ }
46
69
  if (result.dn) {
47
70
  result.name = result.dn;
48
71
  }
@@ -65,8 +88,13 @@ export function magnetDecode(uri) {
65
88
  if (typeof result.ws === 'string' || Array.isArray(result.ws)) {
66
89
  result.urlList = result.urlList.concat(result.ws);
67
90
  }
91
+ result.peerAddresses = [];
92
+ if (typeof result['x.pe'] === 'string' || Array.isArray(result['x.pe'])) {
93
+ result.peerAddresses = result.peerAddresses.concat(result['x.pe']);
94
+ }
68
95
  result.announce = [...new Set(result.announce)].sort((a, b) => a.localeCompare(b));
69
96
  result.urlList = [...new Set(result.urlList)].sort((a, b) => a.localeCompare(b));
97
+ result.peerAddresses = [...new Set(result.peerAddresses)];
70
98
  return result;
71
99
  }
72
100
  /**
@@ -86,6 +114,10 @@ function parseQueryParamValue(key, val) {
86
114
  if (key === 'kt') {
87
115
  return decodeURIComponent(val).split('+');
88
116
  }
117
+ // bep53
118
+ if (key === 'so') {
119
+ return bep53Range.parseRange(decodeURIComponent(val).split(','));
120
+ }
89
121
  // Cast file index (ix) to a number
90
122
  if (key === 'ix') {
91
123
  return Number(val);
@@ -94,11 +126,44 @@ function parseQueryParamValue(key, val) {
94
126
  }
95
127
  export function magnetEncode(data) {
96
128
  const obj = { ...data }; // Shallow clone object
129
+ // Deduplicate xt by using a set
130
+ let xts = new Set();
131
+ if (obj.xt && typeof obj.xt === 'string') {
132
+ xts.add(obj.xt);
133
+ }
134
+ if (obj.xt && Array.isArray(obj.xt)) {
135
+ xts = new Set(obj.xt);
136
+ }
137
+ if (obj.infoHashBuffer) {
138
+ xts.add(`urn:btih:${obj.infoHashBuffer.toString('hex')}`);
139
+ }
140
+ if (obj.infoHash) {
141
+ xts.add(`urn:btih:${obj.infoHash}`);
142
+ }
143
+ if (obj.infoHashV2Buffer) {
144
+ xts.add((obj.xt = `urn:btmh:1220${obj.infoHashV2Buffer.toString('hex')}`));
145
+ }
146
+ if (obj.infoHashV2) {
147
+ xts.add(`urn:btmh:1220${obj.infoHashV2}`);
148
+ }
149
+ const xtsDeduped = Array.from(xts);
150
+ if (xtsDeduped.length === 1) {
151
+ obj.xt = xtsDeduped[0];
152
+ }
153
+ if (xtsDeduped.length > 1) {
154
+ obj.xt = xtsDeduped;
155
+ }
97
156
  // Support using convenience names, in addition to spec names
98
157
  // (example: `infoHash` for `xt`, `name` for `dn`)
99
158
  if (obj.infoHash) {
100
159
  obj.xt = `urn:btih:${obj.infoHash}`;
101
160
  }
161
+ if (obj.publicKeyBuffer) {
162
+ obj.xs = `urn:btpk:${obj.publicKeyBuffer.toString('hex')}`;
163
+ }
164
+ if (obj.publicKey) {
165
+ obj.xs = `urn:btpk:${obj.publicKey}`;
166
+ }
102
167
  if (obj.name) {
103
168
  obj.dn = obj.name;
104
169
  }
@@ -112,32 +177,44 @@ export function magnetEncode(data) {
112
177
  obj.ws = obj.urlList;
113
178
  delete obj.as;
114
179
  }
180
+ if (obj.peerAddresses) {
181
+ obj['x.pe'] = obj.peerAddresses;
182
+ }
115
183
  return Object.keys(obj)
116
- .filter(key => key.length === 2)
184
+ .filter(key => key.length === 2 || key === 'x.pe')
117
185
  .reduce((prev, key, i) => {
118
186
  let acc = prev;
119
187
  const values = Array.isArray(obj[key]) ? obj[key] : [obj[key]];
120
188
  values.forEach((val, j) => {
121
- if ((i > 0 || j > 0) && (key !== 'kt' || j === 0)) {
122
- acc = `${acc}&`;
189
+ if ((i > 0 || j > 0) && ((key !== 'kt' && key !== 'so') || j === 0)) {
190
+ acc += '&';
123
191
  }
124
- let res = val;
125
192
  if (key === 'dn') {
126
- res = encodeURIComponent(val).replace(/%20/g, '+');
193
+ val = encodeURIComponent(val).replace(/%20/g, '+');
127
194
  }
128
- if (key === 'tr' || key === 'xs' || key === 'as' || key === 'ws') {
129
- res = encodeURIComponent(val);
195
+ if (key === 'tr' || key === 'as' || key === 'ws') {
196
+ val = encodeURIComponent(val);
197
+ }
198
+ // Don't URI encode BEP46 keys
199
+ if (key === 'xs' && !val.startsWith('urn:btpk:')) {
200
+ val = encodeURIComponent(val);
130
201
  }
131
202
  if (key === 'kt') {
132
- res = encodeURIComponent(val);
203
+ val = encodeURIComponent(val);
204
+ }
205
+ if (key === 'so') {
206
+ return;
133
207
  }
134
208
  if (key === 'kt' && j > 0) {
135
- acc = `${acc}+${res}`;
209
+ acc += `+${val}`;
136
210
  }
137
211
  else {
138
- acc = `${acc}${key}=${res}`;
212
+ acc += `${key}=${val}`;
139
213
  }
140
214
  });
215
+ if (key === 'so') {
216
+ acc += `${key}=${bep53Range.composeRange(values)}`;
217
+ }
141
218
  return acc;
142
219
  }, `${start}`);
143
220
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ctrl/magnet-link",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Parse a magnet URI into an object",
5
5
  "author": "Scott Cooper <scttcper@gmail.com>",
6
6
  "homepage": "https://magnet-link.vercel.app",
@@ -34,13 +34,13 @@
34
34
  "@ctrl/ts-base32": "^2.1.1"
35
35
  },
36
36
  "devDependencies": {
37
- "@ctrl/eslint-config": "3.2.0",
37
+ "@ctrl/eslint-config": "3.3.1",
38
38
  "@sindresorhus/tsconfig": "2.0.0",
39
- "@types/node": "17.0.1",
40
- "ava": "4.0.0-rc.1",
39
+ "@types/node": "17.0.9",
40
+ "ava": "4.0.1",
41
41
  "buffer": "6.0.3",
42
- "c8": "7.10.0",
43
- "tsm": "2.2.1",
42
+ "c8": "7.11.0",
43
+ "ts-node": "10.4.0",
44
44
  "typescript": "4.5.4"
45
45
  },
46
46
  "ava": {
@@ -51,7 +51,7 @@
51
51
  "ts": "module"
52
52
  },
53
53
  "nodeArguments": [
54
- "--loader=tsm"
54
+ "--loader=ts-node/esm"
55
55
  ]
56
56
  },
57
57
  "workspaces": [