@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.
- package/dist/src/bep53.d.ts +2 -0
- package/dist/src/bep53.js +23 -0
- package/dist/src/index.d.ts +9 -0
- package/dist/src/index.js +87 -10
- package/package.json +7 -7
@@ -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
|
+
}
|
package/dist/src/index.d.ts
CHANGED
@@ -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
|
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
|
-
|
193
|
+
val = encodeURIComponent(val).replace(/%20/g, '+');
|
127
194
|
}
|
128
|
-
if (key === 'tr' || key === '
|
129
|
-
|
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
|
-
|
203
|
+
val = encodeURIComponent(val);
|
204
|
+
}
|
205
|
+
if (key === 'so') {
|
206
|
+
return;
|
133
207
|
}
|
134
208
|
if (key === 'kt' && j > 0) {
|
135
|
-
acc
|
209
|
+
acc += `+${val}`;
|
136
210
|
}
|
137
211
|
else {
|
138
|
-
acc
|
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.
|
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.
|
37
|
+
"@ctrl/eslint-config": "3.3.1",
|
38
38
|
"@sindresorhus/tsconfig": "2.0.0",
|
39
|
-
"@types/node": "17.0.
|
40
|
-
"ava": "4.0.
|
39
|
+
"@types/node": "17.0.9",
|
40
|
+
"ava": "4.0.1",
|
41
41
|
"buffer": "6.0.3",
|
42
|
-
"c8": "7.
|
43
|
-
"
|
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=
|
54
|
+
"--loader=ts-node/esm"
|
55
55
|
]
|
56
56
|
},
|
57
57
|
"workspaces": [
|