magnet-uri
Version:
Parse a magnet URI and return an object of keys/values
177 lines (145 loc) • 5.76 kB
JavaScript
/*! magnet-uri. MIT License. WebTorrent LLC <https://webtorrent.io/opensource> */
import { decode } from '@thaunknown/thirty-two'
import { parse, compose } from 'bep53-range'
import { hex2arr, arr2hex } from 'uint8-util'
/**
* Parse a magnet URI and return an object of keys/values
*
* @param {string} uri
* @return {Object} parsed uri
*/
function magnetURIDecode (uri) {
const result = {}
// Support 'magnet:' and 'stream-magnet:' uris
const data = uri.split('magnet:?')[1]
const params = (data && data.length >= 0)
? data.split('&')
: []
params.forEach(param => {
const keyval = param.split('=')
// This keyval is invalid, skip it
if (keyval.length !== 2) return
const key = keyval[0]
let val = keyval[1]
// Clean up torrent name
if (key === 'dn') val = decodeURIComponent(val).replace(/\+/g, ' ')
// Address tracker (tr), exact source (xs), and acceptable source (as) are encoded
// URIs, so decode them
if (key === 'tr' || key === 'xs' || key === 'as' || key === 'ws') {
val = decodeURIComponent(val)
}
// Return keywords as an array
if (key === 'kt') val = decodeURIComponent(val).split('+')
// Cast file index (ix) to a number
if (key === 'ix') val = Number(val)
// bep53
if (key === 'so') val = parse(decodeURIComponent(val).split(','))
// If there are repeated parameters, return an array of values
if (result[key]) {
if (!Array.isArray(result[key])) {
result[key] = [result[key]]
}
result[key].push(val)
} else {
result[key] = val
}
})
// Convenience properties for parity with `parse-torrent-file` module
let m
if (result.xt) {
const xts = Array.isArray(result.xt) ? result.xt : [result.xt]
xts.forEach(xt => {
if ((m = xt.match(/^urn:btih:(.{40})/))) {
result.infoHash = m[1].toLowerCase()
} else if ((m = xt.match(/^urn:btih:(.{32})/))) {
result.infoHash = arr2hex(decode(m[1]))
} else if ((m = xt.match(/^urn:btmh:1220(.{64})/))) {
result.infoHashV2 = m[1].toLowerCase()
}
})
}
if (result.xs) {
const xss = Array.isArray(result.xs) ? result.xs : [result.xs]
xss.forEach(xs => {
if ((m = xs.match(/^urn:btpk:(.{64})/))) {
result.publicKey = m[1].toLowerCase()
}
})
}
if (result.infoHash) result.infoHashBuffer = hex2arr(result.infoHash)
if (result.infoHashV2) result.infoHashV2Buffer = hex2arr(result.infoHashV2)
if (result.publicKey) result.publicKeyBuffer = hex2arr(result.publicKey)
if (result.dn) result.name = result.dn
if (result.kt) result.keywords = result.kt
result.announce = []
if (typeof result.tr === 'string' || Array.isArray(result.tr)) {
result.announce = result.announce.concat(result.tr)
}
result.urlList = []
if (typeof result.as === 'string' || Array.isArray(result.as)) {
result.urlList = result.urlList.concat(result.as)
}
if (typeof result.ws === 'string' || Array.isArray(result.ws)) {
result.urlList = result.urlList.concat(result.ws)
}
result.peerAddresses = []
if (typeof result['x.pe'] === 'string' || Array.isArray(result['x.pe'])) {
result.peerAddresses = result.peerAddresses.concat(result['x.pe'])
}
// remove duplicates by converting to Set and back
result.announce = Array.from(new Set(result.announce))
result.urlList = Array.from(new Set(result.urlList))
result.peerAddresses = Array.from(new Set(result.peerAddresses))
return result
}
function magnetURIEncode (obj) {
obj = Object.assign({}, obj) // clone obj, so we can mutate it
// support using convenience names, in addition to spec names
// (example: `infoHash` for `xt`, `name` for `dn`)
// Deduplicate xt by using a set
let xts = new Set()
if (obj.xt && typeof obj.xt === 'string') xts.add(obj.xt)
if (obj.xt && Array.isArray(obj.xt)) xts = new Set(obj.xt)
if (obj.infoHashBuffer) xts.add(`urn:btih:${arr2hex(obj.infoHashBuffer)}`)
if (obj.infoHash) xts.add(`urn:btih:${obj.infoHash}`)
if (obj.infoHashV2Buffer) xts.add(obj.xt = `urn:btmh:1220${arr2hex(obj.infoHashV2Buffer)}`)
if (obj.infoHashV2) xts.add(`urn:btmh:1220${obj.infoHashV2}`)
const xtsDeduped = Array.from(xts)
if (xtsDeduped.length === 1) obj.xt = xtsDeduped[0]
if (xtsDeduped.length > 1) obj.xt = xtsDeduped
if (obj.publicKeyBuffer) obj.xs = `urn:btpk:${arr2hex(obj.publicKeyBuffer)}`
if (obj.publicKey) obj.xs = `urn:btpk:${obj.publicKey}`
if (obj.name) obj.dn = obj.name
if (obj.keywords) obj.kt = obj.keywords
if (obj.announce) obj.tr = obj.announce
if (obj.urlList) {
obj.ws = obj.urlList
delete obj.as
}
if (obj.peerAddresses) obj['x.pe'] = obj.peerAddresses
let result = 'magnet:?'
Object.keys(obj)
.filter(key => key.length === 2 || key === 'x.pe')
.forEach((key, i) => {
const values = Array.isArray(obj[key]) ? obj[key] : [obj[key]]
values.forEach((val, j) => {
if ((i > 0 || j > 0) && ((key !== 'kt' && key !== 'so') || j === 0)) result += '&'
if (key === 'dn') val = encodeURIComponent(val).replace(/%20/g, '+')
if (key === 'tr' || key === 'as' || key === 'ws') {
val = encodeURIComponent(val)
}
// Don't URI encode BEP46 keys
if (key === 'xs' && !val.startsWith('urn:btpk:')) {
val = encodeURIComponent(val)
}
if (key === 'kt') val = encodeURIComponent(val)
if (key === 'so') return
if (key === 'kt' && j > 0) result += `+${val}`
else result += `${key}=${val}`
})
if (key === 'so') result += `${key}=${compose(values)}`
})
return result
}
export default magnetURIDecode
export { magnetURIDecode as decode, magnetURIEncode as encode }