├── .npmignore ├── .gitignore ├── test ├── torrents │ ├── bitlove-intro.torrent │ ├── leaves-url-list.torrent │ ├── leaves-empty-url-list.torrent │ ├── leaves-duplicate-tracker.torrent │ └── leaves-empty-announce-list.torrent ├── package.json ├── corrupt.js ├── empty-url-list.js ├── empty-announce-list.js ├── dedupe-announce.js ├── encode.js ├── webseed.js ├── non-torrent-magnet.js ├── node │ └── basic.js ├── no-announce-list.js ├── magnet-metadata.js └── basic.js ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── LICENSE ├── bin └── cmd.js ├── package.json ├── README.md ├── index.js └── CHANGELOG.md /.npmignore: -------------------------------------------------------------------------------- 1 | test/ 2 | .github/ 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /test/torrents/bitlove-intro.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webtorrent/parse-torrent/HEAD/test/torrents/bitlove-intro.torrent -------------------------------------------------------------------------------- /test/torrents/leaves-url-list.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webtorrent/parse-torrent/HEAD/test/torrents/leaves-url-list.torrent -------------------------------------------------------------------------------- /test/torrents/leaves-empty-url-list.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webtorrent/parse-torrent/HEAD/test/torrents/leaves-empty-url-list.torrent -------------------------------------------------------------------------------- /test/torrents/leaves-duplicate-tracker.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webtorrent/parse-torrent/HEAD/test/torrents/leaves-duplicate-tracker.torrent -------------------------------------------------------------------------------- /test/torrents/leaves-empty-announce-list.torrent: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/webtorrent/parse-torrent/HEAD/test/torrents/leaves-empty-announce-list.torrent -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "browserify": { 6 | "transform": ["brfs"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/corrupt.js: -------------------------------------------------------------------------------- 1 | import fixtures from 'webtorrent-fixtures' 2 | import parseTorrent from '../index.js' 3 | import test from 'tape' 4 | 5 | test('exception thrown when torrent file is missing `name` field', async t => { 6 | try { 7 | await parseTorrent(fixtures.corrupt.torrent) 8 | t.error({ message: 'Expected throw' }) 9 | } catch (e) { 10 | t.ok(e instanceof Error) 11 | } 12 | t.end() 13 | }) 14 | -------------------------------------------------------------------------------- /test/empty-url-list.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import parseTorrent from '../index.js' 3 | import path, { dirname } from 'path' 4 | import test from 'tape' 5 | import { fileURLToPath } from 'url' 6 | 7 | const __filename = fileURLToPath(import.meta.url) 8 | const __dirname = dirname(__filename) 9 | 10 | const leavesUrlList = fs.readFileSync(path.join(__dirname, 'torrents/leaves-empty-url-list.torrent')) 11 | 12 | test('parse empty url-list', async t => { 13 | const torrent = await parseTorrent(leavesUrlList) 14 | t.deepEqual(torrent.urlList, []) 15 | t.end() 16 | }) 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 'on': 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node ${{ matrix.node }} / ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | os: 13 | - ubuntu-latest 14 | node: 15 | - '18' 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ runner.node }} 21 | - run: npm install 22 | - run: npm run build --if-present 23 | - run: npm test 24 | -------------------------------------------------------------------------------- /test/empty-announce-list.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import parseTorrent from '../index.js' 3 | import path, { dirname } from 'path' 4 | import test from 'tape' 5 | import { fileURLToPath } from 'url' 6 | 7 | const __filename = fileURLToPath(import.meta.url) 8 | const __dirname = dirname(__filename) 9 | 10 | const leavesAnnounceList = fs.readFileSync(path.join(__dirname, 'torrents/leaves-empty-announce-list.torrent')) 11 | 12 | test('parse torrent with empty announce-list', async t => { 13 | t.deepEquals((await parseTorrent(leavesAnnounceList)).announce, ['udp://tracker.publicbt.com:80/announce']) 14 | t.end() 15 | }) 16 | -------------------------------------------------------------------------------- /test/dedupe-announce.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import parseTorrent from '../index.js' 3 | import path, { dirname } from 'path' 4 | import test from 'tape' 5 | import { fileURLToPath } from 'url' 6 | 7 | const __filename = fileURLToPath(import.meta.url) 8 | const __dirname = dirname(__filename) 9 | 10 | const leavesDuplicateTracker = fs.readFileSync(path.join(__dirname, 'torrents/leaves-duplicate-tracker.torrent')) 11 | 12 | const expectedAnnounce = [ 13 | 'http://tracker.example.com/announce' 14 | ] 15 | 16 | test('dedupe announce list', async t => { 17 | t.deepEqual((await parseTorrent(leavesDuplicateTracker)).announce, expectedAnnounce) 18 | t.end() 19 | }) 20 | -------------------------------------------------------------------------------- /test/encode.js: -------------------------------------------------------------------------------- 1 | import fixtures from 'webtorrent-fixtures' 2 | import parseTorrent, { toTorrentFile } from '../index.js' 3 | import test from 'tape' 4 | 5 | test('parseTorrent.toTorrentFile', async t => { 6 | const parsedTorrent = await parseTorrent(fixtures.leaves.torrent) 7 | const buf = toTorrentFile(parsedTorrent) 8 | const doubleParsedTorrent = await parseTorrent(buf) 9 | 10 | t.deepEqual(doubleParsedTorrent, parsedTorrent) 11 | t.end() 12 | }) 13 | 14 | test('parseTorrent.toTorrentFile w/ comment field', async t => { 15 | const parsedTorrent = await parseTorrent(fixtures.leaves.torrent) 16 | parsedTorrent.comment = 'hi there!' 17 | const buf = toTorrentFile(parsedTorrent) 18 | const doubleParsedTorrent = await parseTorrent(buf) 19 | 20 | t.deepEqual(doubleParsedTorrent, parsedTorrent) 21 | t.end() 22 | }) 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | with: 16 | persist-credentials: false 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 18 21 | - name: Cache 22 | uses: actions/cache@v3 23 | with: 24 | path: ~/.npm 25 | key: ${{ runner.os }}-npm-${{ hashFiles('**/package.json') }} 26 | restore-keys: | 27 | ${{ runner.os }}-npm- 28 | - name: Install dependencies 29 | run: npm i 30 | env: 31 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | - name: Release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | run: npx semantic-release 37 | -------------------------------------------------------------------------------- /test/webseed.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import parseTorrent, { toTorrentFile } from '../index.js' 3 | import path, { dirname } from 'path' 4 | import test from 'tape' 5 | import { fileURLToPath } from 'url' 6 | 7 | const __filename = fileURLToPath(import.meta.url) 8 | const __dirname = dirname(__filename) 9 | 10 | const leavesUrlList = fs.readFileSync(path.join(__dirname, 'torrents/leaves-url-list.torrent')) 11 | 12 | test('parse url-list for webseed support', async t => { 13 | const torrent = await parseTorrent(leavesUrlList) 14 | t.deepEqual(torrent.urlList, ['http://www2.hn.psu.edu/faculty/jmanis/whitman/leaves-of-grass6x9.pdf']) 15 | t.end() 16 | }) 17 | 18 | test('parseTorrent.toTorrentFile url-list for webseed support', async t => { 19 | const parsedTorrent = await parseTorrent(leavesUrlList) 20 | const buf = toTorrentFile(parsedTorrent) 21 | const doubleParsedTorrent = await parseTorrent(buf) 22 | t.deepEqual(doubleParsedTorrent.urlList, ['http://www2.hn.psu.edu/faculty/jmanis/whitman/leaves-of-grass6x9.pdf']) 23 | t.end() 24 | }) 25 | -------------------------------------------------------------------------------- /test/non-torrent-magnet.js: -------------------------------------------------------------------------------- 1 | import parseTorrent from '../index.js' 2 | import test from 'tape' 3 | 4 | test('exception thrown with non-bittorrent URNs', async function (t) { 5 | // Non-bittorrent URNs (examples from Wikipedia) 6 | const magnets = [ 7 | 'magnet:?xt=urn:sha1:PDAQRAOQQRYS76MRZJ33LK4MMVZBDSCL', 8 | 'magnet:?xt=urn:tree:tiger:IZZG2KNL4BKA7LYEKK5JAX6BQ27UV4QZKPL2JZQ', 9 | 'magnet:?xt=urn:bitprint:QBMYI5FTYSFFSP7HJ37XALYNNVYLJE27.E6ITPBX6LSBBW34T3UGPIVJDNNJZIQOMP5WNEUI', 10 | 'magnet:?xt=urn:ed2k:31D6CFE0D16AE931B73C59D7E0C089C0', 11 | 'magnet:?xt=urn:aich:D6EUDGK2DBTBEZ2XVN3G6H4CINSTZD7M', 12 | 'magnet:?xt=urn:kzhash:35759fdf77748ba01240b0d8901127bfaff929ed1849b9283f7694b37c192d038f535434', 13 | 'magnet:?xt=urn:md5:4e7bef74677be349ccffc6a178e38299' 14 | ] 15 | 16 | await Promise.all(magnets.map(async function (magnet) { 17 | try { 18 | await parseTorrent(magnet) 19 | t.error({ message: 'Expected throw' }) 20 | } catch (e) { 21 | t.ok(e instanceof Error) 22 | } 23 | })) 24 | 25 | t.end() 26 | }) 27 | -------------------------------------------------------------------------------- /test/node/basic.js: -------------------------------------------------------------------------------- 1 | import fixtures from 'webtorrent-fixtures' 2 | import http from 'http' 3 | import { remote } from '../../index.js' 4 | import test from 'tape' 5 | 6 | fixtures.leaves.parsedTorrent.infoHashBuffer = new Uint8Array(fixtures.leaves.parsedTorrent.infoHashBuffer) 7 | 8 | test('http url to a torrent file, string', t => { 9 | t.plan(3) 10 | 11 | const server = http.createServer((req, res) => { 12 | t.pass('server got request') 13 | res.end(fixtures.leaves.torrent) 14 | }) 15 | 16 | server.listen(0, () => { 17 | const port = server.address().port 18 | const url = `http://127.0.0.1:${port}` 19 | remote(url, (err, parsedTorrent) => { 20 | t.error(err) 21 | t.deepEqual(parsedTorrent, fixtures.leaves.parsedTorrent) 22 | server.close() 23 | }) 24 | }) 25 | }) 26 | 27 | test('filesystem path to a torrent file, string', t => { 28 | t.plan(2) 29 | 30 | remote(fixtures.leaves.torrentPath, (err, parsedTorrent) => { 31 | t.error(err) 32 | t.deepEqual(parsedTorrent, fixtures.leaves.parsedTorrent) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Feross Aboukhadijeh and WebTorrent, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /bin/cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import stdin from 'get-stdin' 4 | import { remote } from '../index.js' 5 | 6 | function usage () { 7 | console.error('Usage: parse-torrent /path/to/torrent') 8 | console.error(' parse-torrent magnet_uri') 9 | console.error(' parse-torrent --stdin') 10 | console.error(' parse-torrent --raw /path/to/torrent') 11 | console.error(' parse-torrent --raw magnet_uri') 12 | } 13 | 14 | function error (err) { 15 | console.error(err.message) 16 | process.exit(1) 17 | } 18 | 19 | const args = process.argv.slice(2) 20 | 21 | if (!args[0] || args.includes('--help')) { 22 | usage() 23 | process.exit(1) 24 | } 25 | 26 | if (args.includes('--stdin') || args.includes('-')) stdin.buffer().then(onTorrentId).catch(error) 27 | else if (args.includes() === '--version' || args.includes('-v')) console.log(require('../package.json').version) 28 | else onTorrentId(args[args.length - 1]) 29 | 30 | function onTorrentId (torrentId) { 31 | remote(torrentId, function (err, parsedTorrent) { 32 | if (err) return error(err) 33 | 34 | if (args.includes('--raw')) { 35 | recursiveStringify(parsedTorrent.info) 36 | } else { 37 | delete parsedTorrent.info 38 | } 39 | 40 | delete parsedTorrent.infoBuffer 41 | delete parsedTorrent.infoHashBuffer 42 | 43 | console.log(JSON.stringify(parsedTorrent, undefined, 2)) 44 | }) 45 | } 46 | 47 | function recursiveStringify (obj) { 48 | for (const key of Object.keys(obj)) { 49 | if (!Buffer.isBuffer(obj[key]) && 50 | typeof obj[key] === 'object' && obj[key] !== null) { 51 | recursiveStringify(obj[key]) 52 | } else { 53 | obj[key] = obj[key].toString() 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parse-torrent", 3 | "description": "Parse a torrent identifier (magnet uri, .torrent file, info hash)", 4 | "version": "11.0.19", 5 | "author": { 6 | "name": "WebTorrent LLC", 7 | "email": "feross@webtorrent.io", 8 | "url": "https://webtorrent.io" 9 | }, 10 | "browser": { 11 | "fs": false 12 | }, 13 | "bin": "./bin/cmd.js", 14 | "bugs": { 15 | "url": "https://github.com/webtorrent/parse-torrent/issues" 16 | }, 17 | "standard": { 18 | "globals": [ 19 | "Blob" 20 | ] 21 | }, 22 | "type": "module", 23 | "dependencies": { 24 | "bencode": "^4.0.0", 25 | "cross-fetch-ponyfill": "^1.0.3", 26 | "get-stdin": "^9.0.0", 27 | "magnet-uri": "^7.0.7", 28 | "queue-microtask": "^1.2.3", 29 | "uint8-util": "^2.2.5" 30 | }, 31 | "devDependencies": { 32 | "@webtorrent/semantic-release-config": "1.0.10", 33 | "brfs": "2.0.2", 34 | "semantic-release": "21.1.2", 35 | "standard": "^17.1.0", 36 | "tape": "5.9.0", 37 | "webtorrent-fixtures": "2.0.2", 38 | "xtend": "4.0.2" 39 | }, 40 | "keywords": [ 41 | ".torrent", 42 | "bittorrent", 43 | "parse torrent", 44 | "peer-to-peer", 45 | "read torrent", 46 | "torrent", 47 | "webtorrent" 48 | ], 49 | "license": "MIT", 50 | "engines": { 51 | "node": ">=12.20.0" 52 | }, 53 | "exports": { 54 | "import": "./index.js" 55 | }, 56 | "repository": { 57 | "type": "git", 58 | "url": "git://github.com/webtorrent/parse-torrent.git" 59 | }, 60 | "scripts": { 61 | "test": "standard && tape test/*.js test/node/*.js" 62 | }, 63 | "funding": [ 64 | { 65 | "type": "github", 66 | "url": "https://github.com/sponsors/feross" 67 | }, 68 | { 69 | "type": "patreon", 70 | "url": "https://www.patreon.com/feross" 71 | }, 72 | { 73 | "type": "consulting", 74 | "url": "https://feross.org/support" 75 | } 76 | ], 77 | "renovate": { 78 | "extends": [ 79 | "github>webtorrent/renovate-config" 80 | ], 81 | "rangeStrategy": "bump" 82 | }, 83 | "release": { 84 | "extends": "@webtorrent/semantic-release-config" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /test/no-announce-list.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import parseTorrent from '../index.js' 3 | import path, { dirname } from 'path' 4 | import test from 'tape' 5 | import { fileURLToPath } from 'url' 6 | 7 | const __filename = fileURLToPath(import.meta.url) 8 | const __dirname = dirname(__filename) 9 | 10 | const bitloveIntro = fs.readFileSync(path.join(__dirname, 'torrents/bitlove-intro.torrent')) 11 | 12 | const bitloveParsed = { 13 | infoHash: '4cb67059ed6bd08362da625b3ae77f6f4a075705', 14 | infoHashBuffer: new Uint8Array(Buffer.from('4cb67059ed6bd08362da625b3ae77f6f4a075705', 'hex')), 15 | name: 'bl001-introduction.webm', 16 | announce: [ 17 | 'http://t.bitlove.org/announce' 18 | ], 19 | urlList: [ 20 | 'http://spaceboyz.net/~astro/bitlove-show/bl001-introduction.webm' 21 | ], 22 | files: [ 23 | { 24 | path: 'bl001-introduction.webm', 25 | name: 'bl001-introduction.webm', 26 | length: 19211729, 27 | offset: 0 28 | } 29 | ], 30 | length: 19211729, 31 | pieceLength: 1048576, 32 | lastPieceLength: 337361, 33 | pieces: [ 34 | '90a75dcd4e88d287c7ac5599c108f6036c13c4ce', 35 | '1ef5468bdff9a4466ad4e446477981cb67d07933', 36 | '1fa911a663451280953edb723e67611957dc0fe1', 37 | '2abad6066e29c723f01b0908ec30e0e737514a88', 38 | '55afda8e14a45e7f797eb47b82b2d0a3b2ca5f36', 39 | '7e1f49593515ca1b93ad01c3ee050e35f04f5c2e', 40 | '15b9abb123228002cca6a7d88fc9fc99d24583e1', 41 | '32704a020d2f121bfc612b7627cd92e2b39ad43c', 42 | '35bebb2888f7143c2966bb4d5f74e0b875825856', 43 | '6875f4bb1a9fa631ee35bcd7469b1e8ff37d65a2', 44 | 'cbbeeeadc148ed681b699e88a940f796f51c0915', 45 | 'c69121c81d85055678bf198bb29fc9e504ed8c7f', 46 | '7e3863c6e1c6a8c824569f1cc0950498dceb03c4', 47 | 'ab4e77dade5f54246559c40915b700a4f734cee0', 48 | '92c47be2d397afbf06a9e9a573a63a3c683d2aa5', 49 | '01ad212a1495208b7ffbb173ce5782291695652b', 50 | '3f6233bf4ea3649c7799a1848f06cade97987525', 51 | 'db37c799e45bd02fc25eacc12e18c6c11b4da3fb', 52 | '4c73df9307b3939fec3cd5f0df179c50a49c6ca3' 53 | ], 54 | info: { 55 | length: 19211729, 56 | name: new Uint8Array(Buffer.from('YmwwMDEtaW50cm9kdWN0aW9uLndlYm0=', 'base64')), 57 | 'piece length': 1048576, 58 | pieces: new Uint8Array(Buffer.from('kKddzU6I0ofHrFWZwQj2A2wTxM4e9UaL3/mkRmrU5EZHeYHLZ9B5Mx+pEaZjRRKAlT7bcj5nYRlX3A/hKrrWBm4pxyPwGwkI7DDg5zdRSohVr9qOFKRef3l+tHuCstCjsspfNn4fSVk1Fcobk60Bw+4FDjXwT1wuFbmrsSMigALMpqfYj8n8mdJFg+EycEoCDS8SG/xhK3YnzZLis5rUPDW+uyiI9xQ8KWa7TV904Lh1glhWaHX0uxqfpjHuNbzXRpsej/N9ZaLLvu6twUjtaBtpnoipQPeW9RwJFcaRIcgdhQVWeL8Zi7KfyeUE7Yx/fjhjxuHGqMgkVp8cwJUEmNzrA8SrTnfa3l9UJGVZxAkVtwCk9zTO4JLEe+LTl6+/BqnppXOmOjxoPSqlAa0hKhSVIIt/+7FzzleCKRaVZSs/YjO/TqNknHeZoYSPBsrel5h1Jds3x5nkW9Avwl6swS4YxsEbTaP7THPfkwezk5/sPNXw3xecUKScbKM=', 'base64')) 59 | }, 60 | infoBuffer: new Uint8Array(Buffer.from('ZDY6bGVuZ3RoaTE5MjExNzI5ZTQ6bmFtZTIzOmJsMDAxLWludHJvZHVjdGlvbi53ZWJtMTI6cGllY2UgbGVuZ3RoaTEwNDg1NzZlNjpwaWVjZXMzODA6kKddzU6I0ofHrFWZwQj2A2wTxM4e9UaL3/mkRmrU5EZHeYHLZ9B5Mx+pEaZjRRKAlT7bcj5nYRlX3A/hKrrWBm4pxyPwGwkI7DDg5zdRSohVr9qOFKRef3l+tHuCstCjsspfNn4fSVk1Fcobk60Bw+4FDjXwT1wuFbmrsSMigALMpqfYj8n8mdJFg+EycEoCDS8SG/xhK3YnzZLis5rUPDW+uyiI9xQ8KWa7TV904Lh1glhWaHX0uxqfpjHuNbzXRpsej/N9ZaLLvu6twUjtaBtpnoipQPeW9RwJFcaRIcgdhQVWeL8Zi7KfyeUE7Yx/fjhjxuHGqMgkVp8cwJUEmNzrA8SrTnfa3l9UJGVZxAkVtwCk9zTO4JLEe+LTl6+/BqnppXOmOjxoPSqlAa0hKhSVIIt/+7FzzleCKRaVZSs/YjO/TqNknHeZoYSPBsrel5h1Jds3x5nkW9Avwl6swS4YxsEbTaP7THPfkwezk5/sPNXw3xecUKScbKNl', 'base64')) 61 | } 62 | 63 | test('parse torrent with no announce-list', async t => { 64 | t.deepEquals(await parseTorrent(bitloveIntro), bitloveParsed) 65 | t.end() 66 | }) 67 | -------------------------------------------------------------------------------- /test/magnet-metadata.js: -------------------------------------------------------------------------------- 1 | import fixtures from 'webtorrent-fixtures' 2 | import parseTorrent from '../index.js' 3 | import test from 'tape' 4 | 5 | const leavesMagnetParsed = { 6 | infoHash: 'd2474e86c95b19b8bcfdb92bc12c9d44667cfa36', 7 | infoHashBuffer: new Uint8Array(Buffer.from('d2474e86c95b19b8bcfdb92bc12c9d44667cfa36', 'hex')), 8 | name: 'Leaves of Grass by Walt Whitman.epub', 9 | announce: [], 10 | urlList: [], 11 | files: [ 12 | { 13 | path: 'Leaves of Grass by Walt Whitman.epub', 14 | name: 'Leaves of Grass by Walt Whitman.epub', 15 | length: 362017, 16 | offset: 0 17 | } 18 | ], 19 | length: 362017, 20 | pieceLength: 16384, 21 | lastPieceLength: 1569, 22 | pieces: [ 23 | '1f9c3f59beec079715ec53324bde8569e4a0b4eb', 24 | 'ec42307d4ce5557b5d3964c5ef55d354cf4a6ecc', 25 | '7bf1bcaf79d11fa5e0be06593c8faafc0c2ba2cf', 26 | '76d71c5b01526b23007f9e9929beafc5151e6511', 27 | '0931a1b44c21bf1e68b9138f90495e690dbc55f5', 28 | '72e4c2944cbacf26e6b3ae8a7229d88aafa05f61', 29 | 'eaae6abf3f07cb6db9677cc6aded4dd3985e4586', 30 | '27567fa7639f065f71b18954304aca6366729e0b', 31 | '4773d77ae80caa96a524804dfe4b9bd3deaef999', 32 | 'c9dd51027467519d5eb2561ae2cc01467de5f643', 33 | '0a60bcba24797692efa8770d23df0a830d91cb35', 34 | 'b3407a88baa0590dc8c9aa6a120f274367dcd867', 35 | 'e88e8338c572a06e3c801b29f519df532b3e76f6', 36 | '70cf6aee53107f3d39378483f69cf80fa568b1ea', 37 | 'c53b506159e988d8bc16922d125d77d803d652c3', 38 | 'ca3070c16eed9172ab506d20e522ea3f1ab674b3', 39 | 'f923d76fe8f44ff32e372c3b376564c6fb5f0dbe', 40 | '52164f03629fd1322636babb2c014b7dae582da4', 41 | '1363965261e6ce12b43701f0a8c9ed1520a70eba', 42 | '004400a267765f6d3dd5c7beb5bd3c75f3df2a54', 43 | '560a61801147fa4ec7cf568e703acb04e5610a4d', 44 | '56dcc242d03293e9446cf5e457d8eb3d9588fd90', 45 | 'c698de9b0dad92980906c026d8c1408fa08fe4ec' 46 | ], 47 | info: { 48 | length: 362017, 49 | name: new Uint8Array(Buffer.from('TGVhdmVzIG9mIEdyYXNzIGJ5IFdhbHQgV2hpdG1hbi5lcHVi', 'base64')), 50 | 'piece length': 16384, 51 | pieces: new Uint8Array(Buffer.from('H5w/Wb7sB5cV7FMyS96FaeSgtOvsQjB9TOVVe105ZMXvVdNUz0puzHvxvK950R+l4L4GWTyPqvwMK6LPdtccWwFSayMAf56ZKb6vxRUeZREJMaG0TCG/Hmi5E4+QSV5pDbxV9XLkwpRMus8m5rOuinIp2IqvoF9h6q5qvz8Hy225Z3zGre1N05heRYYnVn+nY58GX3GxiVQwSspjZnKeC0dz13roDKqWpSSATf5Lm9PervmZyd1RAnRnUZ1eslYa4swBRn3l9kMKYLy6JHl2ku+odw0j3wqDDZHLNbNAeoi6oFkNyMmqahIPJ0Nn3Nhn6I6DOMVyoG48gBsp9RnfUys+dvZwz2ruUxB/PTk3hIP2nPgPpWix6sU7UGFZ6YjYvBaSLRJdd9gD1lLDyjBwwW7tkXKrUG0g5SLqPxq2dLP5I9dv6PRP8y43LDs3ZWTG+18NvlIWTwNin9EyJja6uywBS32uWC2kE2OWUmHmzhK0NwHwqMntFSCnDroARACiZ3ZfbT3Vx761vTx1898qVFYKYYARR/pOx89WjnA6ywTlYQpNVtzCQtAyk+lEbPXkV9jrPZWI/ZDGmN6bDa2SmAkGwCbYwUCPoI/k7A==', 'base64')) 52 | }, 53 | infoBuffer: new Uint8Array(Buffer.from('ZDY6bGVuZ3RoaTM2MjAxN2U0Om5hbWUzNjpMZWF2ZXMgb2YgR3Jhc3MgYnkgV2FsdCBXaGl0bWFuLmVwdWIxMjpwaWVjZSBsZW5ndGhpMTYzODRlNjpwaWVjZXM0NjA6H5w/Wb7sB5cV7FMyS96FaeSgtOvsQjB9TOVVe105ZMXvVdNUz0puzHvxvK950R+l4L4GWTyPqvwMK6LPdtccWwFSayMAf56ZKb6vxRUeZREJMaG0TCG/Hmi5E4+QSV5pDbxV9XLkwpRMus8m5rOuinIp2IqvoF9h6q5qvz8Hy225Z3zGre1N05heRYYnVn+nY58GX3GxiVQwSspjZnKeC0dz13roDKqWpSSATf5Lm9PervmZyd1RAnRnUZ1eslYa4swBRn3l9kMKYLy6JHl2ku+odw0j3wqDDZHLNbNAeoi6oFkNyMmqahIPJ0Nn3Nhn6I6DOMVyoG48gBsp9RnfUys+dvZwz2ruUxB/PTk3hIP2nPgPpWix6sU7UGFZ6YjYvBaSLRJdd9gD1lLDyjBwwW7tkXKrUG0g5SLqPxq2dLP5I9dv6PRP8y43LDs3ZWTG+18NvlIWTwNin9EyJja6uywBS32uWC2kE2OWUmHmzhK0NwHwqMntFSCnDroARACiZ3ZfbT3Vx761vTx1898qVFYKYYARR/pOx89WjnA6ywTlYQpNVtzCQtAyk+lEbPXkV9jrPZWI/ZDGmN6bDa2SmAkGwCbYwUCPoI/k7GU=', 'base64')) 54 | } 55 | 56 | test('parse "torrent" from magnet metadata protocol', async t => { 57 | t.deepEquals(await parseTorrent(fixtures.leavesMetadata.torrent), leavesMagnetParsed) 58 | t.end() 59 | }) 60 | -------------------------------------------------------------------------------- /test/basic.js: -------------------------------------------------------------------------------- 1 | /* global Blob */ 2 | 3 | import extend from 'xtend' 4 | import fixtures from 'webtorrent-fixtures' 5 | import parseTorrent, { remote } from '../index.js' 6 | import test from 'tape' 7 | 8 | test('Test supported torrentInfo types', async t => { 9 | let parsed 10 | 11 | // info hash (as a hex string) 12 | parsed = await parseTorrent(fixtures.leaves.parsedTorrent.infoHash) 13 | t.equal(parsed.infoHash, fixtures.leaves.parsedTorrent.infoHash) 14 | t.equal(parsed.name, undefined) 15 | t.deepEqual(parsed.announce, []) 16 | 17 | // info hash (as a Buffer) 18 | parsed = await parseTorrent(Buffer.from(fixtures.leaves.parsedTorrent.infoHash, 'hex')) 19 | t.equal(parsed.infoHash, fixtures.leaves.parsedTorrent.infoHash) 20 | t.equal(parsed.name, undefined) 21 | t.deepEqual(parsed.announce, []) 22 | 23 | // magnet uri (as a utf8 string) 24 | const magnet = `magnet:?xt=urn:btih:${fixtures.leaves.parsedTorrent.infoHash}` 25 | parsed = await parseTorrent(magnet) 26 | t.equal(parsed.infoHash, fixtures.leaves.parsedTorrent.infoHash) 27 | t.equal(parsed.name, undefined) 28 | t.deepEqual(parsed.announce, []) 29 | 30 | // stream-magnet uri (as a utf8 string) 31 | const streamMagnet = `stream-magnet:?xt=urn:btih:${fixtures.leaves.parsedTorrent.infoHash}` 32 | parsed = await parseTorrent(streamMagnet) 33 | t.equal(parsed.infoHash, fixtures.leaves.parsedTorrent.infoHash) 34 | t.equal(parsed.name, undefined) 35 | t.deepEqual(parsed.announce, []) 36 | 37 | // magnet uri with name 38 | parsed = await parseTorrent(`${magnet}&dn=${encodeURIComponent(fixtures.leaves.parsedTorrent.name)}`) 39 | t.equal(parsed.infoHash, fixtures.leaves.parsedTorrent.infoHash) 40 | t.equal(parsed.name, fixtures.leaves.parsedTorrent.name) 41 | t.deepEqual(parsed.announce, []) 42 | 43 | // magnet uri with trackers 44 | parsed = await parseTorrent(`${magnet}&tr=${encodeURIComponent('udp://tracker.example.com:80')}`) 45 | t.equal(parsed.infoHash, fixtures.leaves.parsedTorrent.infoHash) 46 | t.equal(parsed.name, undefined) 47 | t.deepEqual(parsed.announce, ['udp://tracker.example.com:80']) 48 | 49 | // .torrent file (as a Buffer) 50 | parsed = await parseTorrent(fixtures.leaves.torrent) 51 | t.equal(parsed.infoHash, fixtures.leaves.parsedTorrent.infoHash) 52 | t.equal(parsed.name, fixtures.leaves.parsedTorrent.name) 53 | t.deepEqual(parsed.announce, fixtures.leaves.parsedTorrent.announce) 54 | 55 | // parsed torrent (as an Object) 56 | parsed = await parseTorrent(fixtures.leaves.parsedTorrent) 57 | t.equal(parsed.infoHash, fixtures.leaves.parsedTorrent.infoHash) 58 | t.equal(parsed.name, fixtures.leaves.parsedTorrent.name) 59 | t.deepEqual(parsed.announce, fixtures.leaves.parsedTorrent.announce) 60 | 61 | // parsed torrent (as an Object), with string 'announce' property 62 | let leavesParsedModified = extend(fixtures.leaves.parsedTorrent, { announce: 'udp://tracker.example.com:80' }) 63 | parsed = await parseTorrent(leavesParsedModified) 64 | t.equal(parsed.infoHash, fixtures.leaves.parsedTorrent.infoHash) 65 | t.equal(parsed.name, fixtures.leaves.parsedTorrent.name) 66 | t.deepEqual(parsed.announce, ['udp://tracker.example.com:80']) 67 | 68 | // parsed torrent (as an Object), with array 'announce' property 69 | leavesParsedModified = extend(fixtures.leaves.parsedTorrent, { 70 | announce: ['udp://tracker.example.com:80', 'udp://tracker.example.com:81'] 71 | }) 72 | parsed = await parseTorrent(leavesParsedModified) 73 | t.equal(parsed.infoHash, fixtures.leaves.parsedTorrent.infoHash) 74 | t.equal(parsed.name, fixtures.leaves.parsedTorrent.name) 75 | t.deepEqual(parsed.announce, [ 76 | 'udp://tracker.example.com:80', 77 | 'udp://tracker.example.com:81' 78 | ]) 79 | 80 | // parsed torrent (as an Object), with empty 'announce' property 81 | leavesParsedModified = extend(fixtures.leaves.parsedTorrent, { announce: undefined }) 82 | parsed = await parseTorrent(leavesParsedModified) 83 | t.equal(parsed.infoHash, fixtures.leaves.parsedTorrent.infoHash) 84 | t.equal(parsed.name, fixtures.leaves.parsedTorrent.name) 85 | t.deepEqual(parsed.announce, []) 86 | 87 | t.end() 88 | }) 89 | 90 | test('parse single file torrent', async t => { 91 | const parsed = await parseTorrent(fixtures.leaves.torrent) 92 | t.equal(parsed.infoHash, fixtures.leaves.parsedTorrent.infoHash) 93 | t.equal(parsed.name, fixtures.leaves.parsedTorrent.name) 94 | t.deepEquals(parsed.announce, fixtures.leaves.parsedTorrent.announce) 95 | t.end() 96 | }) 97 | 98 | test('parse multiple file torrent', async t => { 99 | const parsed = await parseTorrent(fixtures.numbers.torrent) 100 | t.equal(parsed.infoHash, fixtures.numbers.parsedTorrent.infoHash) 101 | t.equal(parsed.name, fixtures.numbers.parsedTorrent.name) 102 | t.deepEquals(parsed.files, fixtures.numbers.parsedTorrent.files) 103 | t.deepEquals(parsed.announce, fixtures.numbers.parsedTorrent.announce) 104 | t.end() 105 | }) 106 | 107 | test('torrent file missing `name` field throws', async t => { 108 | try { 109 | await parseTorrent(fixtures.invalid.torrent) 110 | } catch (e) { 111 | t.ok(e instanceof Error) 112 | } 113 | t.end() 114 | }) 115 | 116 | test('parse url-list for webseed support', async t => { 117 | const torrent = await parseTorrent(fixtures.bunny.torrent) 118 | t.deepEqual(torrent.urlList, ['http://distribution.bbb3d.renderfarming.net/video/mp4/bbb_sunflower_1080p_30fps_stereo_abl.mp4']) 119 | t.end() 120 | }) 121 | 122 | test('parse single file torrent from Blob', t => { 123 | if (typeof Blob === 'undefined') { 124 | t.pass('Skipping Blob test') 125 | t.end() 126 | return 127 | } 128 | 129 | t.plan(4) 130 | const leavesBlob = makeBlobShim(fixtures.leaves.torrent) 131 | remote(leavesBlob, (err, parsed) => { 132 | t.error(err) 133 | t.equal(parsed.infoHash, fixtures.leaves.parsedTorrent.infoHash) 134 | t.equal(parsed.name, fixtures.leaves.parsedTorrent.name) 135 | t.deepEquals(parsed.announce, fixtures.leaves.parsedTorrent.announce) 136 | }) 137 | }) 138 | 139 | function makeBlobShim (buf, name) { 140 | const file = new Blob([buf]) 141 | file.name = name 142 | return file 143 | } 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # parse-torrent [![ci][ci-image]][ci-url] [![npm][npm-image]][npm-url] [![downloads][downloads-image]][downloads-url] [![javascript style guide][standard-image]][standard-url] 2 | 3 | [ci-image]: https://img.shields.io/github/actions/workflow/status/webtorrent/parse-torrent/ci.yml 4 | [ci-url]: https://github.com/webtorrent/parse-torrent/actions 5 | [npm-image]: https://img.shields.io/npm/v/parse-torrent.svg 6 | [npm-url]: https://npmjs.org/package/parse-torrent 7 | [downloads-image]: https://img.shields.io/npm/dm/parse-torrent.svg 8 | [downloads-url]: https://npmjs.org/package/parse-torrent 9 | [standard-image]: https://img.shields.io/badge/code_style-standard-brightgreen.svg 10 | [standard-url]: https://standardjs.com 11 | 12 | ### Parse a torrent identifier (magnet uri, .torrent file, info hash) 13 | 14 | Works in node and the browser (with [browserify](http://browserify.org/)). This module is used by [WebTorrent](http://webtorrent.io)! 15 | 16 | ## install 17 | 18 | ``` 19 | npm install parse-torrent 20 | ``` 21 | 22 | ## usage 23 | 24 | ### parse 25 | 26 | The return value of `parseTorrent` will contain as much info as possible about the 27 | torrent. The only property that is guaranteed to be present is `infoHash`. 28 | 29 | ```js 30 | import parseTorrent from 'parse-torrent' 31 | import fs from 'fs' 32 | 33 | // info hash (as a hex string) 34 | parseTorrent('d2474e86c95b19b8bcfdb92bc12c9d44667cfa36') 35 | // { infoHash: 'd2474e86c95b19b8bcfdb92bc12c9d44667cfa36' } 36 | 37 | // info hash (as a Buffer) 38 | parseTorrent(new Buffer('d2474e86c95b19b8bcfdb92bc12c9d44667cfa36', 'hex')) 39 | // { infoHash: 'd2474e86c95b19b8bcfdb92bc12c9d44667cfa36' } 40 | 41 | // magnet uri (as a utf8 string) 42 | parseTorrent('magnet:?xt=urn:btih:d2474e86c95b19b8bcfdb92bc12c9d44667cfa36') 43 | // { xt: 'urn:btih:d2474e86c95b19b8bcfdb92bc12c9d44667cfa36', 44 | // infoHash: 'd2474e86c95b19b8bcfdb92bc12c9d44667cfa36' } 45 | 46 | // magnet uri with torrent name 47 | parseTorrent('magnet:?xt=urn:btih:d2474e86c95b19b8bcfdb92bc12c9d44667cfa36&dn=Leaves%20of%20Grass%20by%20Walt%20Whitman.epub') 48 | // { xt: 'urn:btih:d2474e86c95b19b8bcfdb92bc12c9d44667cfa36', 49 | // dn: 'Leaves of Grass by Walt Whitman.epub', 50 | // infoHash: 'd2474e86c95b19b8bcfdb92bc12c9d44667cfa36', 51 | // name: 'Leaves of Grass by Walt Whitman.epub' } 52 | 53 | // magnet uri with trackers 54 | parseTorrent('magnet:?xt=urn:btih:d2474e86c95b19b8bcfdb92bc12c9d44667cfa36&tr=http%3A%2F%2Ftracker.example.com%2Fannounce') 55 | // { xt: 'urn:btih:d2474e86c95b19b8bcfdb92bc12c9d44667cfa36', 56 | // tr: 'http://tracker.example.com/announce', 57 | // infoHash: 'd2474e86c95b19b8bcfdb92bc12c9d44667cfa36', 58 | // announce: [ 'http://tracker.example.com/announce' ] } 59 | 60 | // .torrent file (as a Buffer) 61 | parseTorrent(fs.readFileSync(__dirname + '/torrents/leaves.torrent')) 62 | // { info: 63 | // { length: 362017, 64 | // name: , 65 | // 'piece length': 16384, 66 | // pieces: }, 67 | // infoBuffer: , 68 | // infoHash: 'd2474e86c95b19b8bcfdb92bc12c9d44667cfa36', 69 | // name: 'Leaves of Grass by Walt Whitman.epub', 70 | // private: false, 71 | // created: Thu Aug 01 2013 06:27:46 GMT-0700 (PDT), 72 | // comment: 'Downloaded from http://TheTorrent.org', 73 | // announce: 74 | // [ 'http://tracker.example.com/announce' ], 75 | // urlList: [], 76 | // files: 77 | // [ { path: 'Leaves of Grass by Walt Whitman.epub', 78 | // name: 'Leaves of Grass by Walt Whitman.epub', 79 | // length: 362017, 80 | // offset: 0 } ], 81 | // length: 362017, 82 | // pieceLength: 16384, 83 | // lastPieceLength: 1569, 84 | // pieces: 85 | // [ '1f9c3f59beec079715ec53324bde8569e4a0b4eb', 86 | // 'ec42307d4ce5557b5d3964c5ef55d354cf4a6ecc', 87 | // '7bf1bcaf79d11fa5e0be06593c8faafc0c2ba2cf', 88 | // '76d71c5b01526b23007f9e9929beafc5151e6511', 89 | // '0931a1b44c21bf1e68b9138f90495e690dbc55f5', 90 | // '72e4c2944cbacf26e6b3ae8a7229d88aafa05f61', 91 | // 'eaae6abf3f07cb6db9677cc6aded4dd3985e4586', 92 | // '27567fa7639f065f71b18954304aca6366729e0b', 93 | // '4773d77ae80caa96a524804dfe4b9bd3deaef999', 94 | // 'c9dd51027467519d5eb2561ae2cc01467de5f643', 95 | // '0a60bcba24797692efa8770d23df0a830d91cb35', 96 | // 'b3407a88baa0590dc8c9aa6a120f274367dcd867', 97 | // 'e88e8338c572a06e3c801b29f519df532b3e76f6', 98 | // '70cf6aee53107f3d39378483f69cf80fa568b1ea', 99 | // 'c53b506159e988d8bc16922d125d77d803d652c3', 100 | // 'ca3070c16eed9172ab506d20e522ea3f1ab674b3', 101 | // 'f923d76fe8f44ff32e372c3b376564c6fb5f0dbe', 102 | // '52164f03629fd1322636babb2c014b7dae582da4', 103 | // '1363965261e6ce12b43701f0a8c9ed1520a70eba', 104 | // '004400a267765f6d3dd5c7beb5bd3c75f3df2a54', 105 | // '560a61801147fa4ec7cf568e703acb04e5610a4d', 106 | // '56dcc242d03293e9446cf5e457d8eb3d9588fd90', 107 | // 'c698de9b0dad92980906c026d8c1408fa08fe4ec' ] } 108 | ``` 109 | 110 | ### encode 111 | 112 | The reverse works too. To convert an object of keys/value to a magnet uri or .torrent file 113 | buffer, use `toMagnetURI` and `toTorrentFile`. 114 | 115 | ```js 116 | import { toMagnetURI, toTorrentFile } from 'parse-torrent' 117 | 118 | const uri = toMagnetURI({ 119 | infoHash: 'd2474e86c95b19b8bcfdb92bc12c9d44667cfa36' 120 | }) 121 | console.log(uri) // 'magnet:?xt=urn:btih:d2474e86c95b19b8bcfdb92bc12c9d44667cfa36' 122 | 123 | const buf = toTorrentFile({ 124 | info: { 125 | /* ... */ 126 | } 127 | }) 128 | console.log(buf) 129 | ``` 130 | 131 | ### remote torrents 132 | 133 | To support remote torrent identifiers (i.e. http/https links to .torrent files, or 134 | filesystem paths), as well as Blobs use the `parseTorrent.remote` function. It takes 135 | a callback since these torrent types require async operations: 136 | 137 | ```js 138 | import { remote } from 'parse-torrent' 139 | remote(torrentId, (err, parsedTorrent) => { 140 | if (err) throw err 141 | console.log(parsedTorrent) 142 | }) 143 | ``` 144 | 145 | If the `torrentId` is an http/https link to the .torrent file, then the request to the file 146 | can be modified by passing `simple-get` params. For example: 147 | 148 | ```js 149 | import { remote } from 'parse-torrent' 150 | remote(torrentId, { timeout: 60 * 1000 }, (err, parsedTorrent) => { 151 | if (err) throw err 152 | console.log(parsedTorrent) 153 | }) 154 | ``` 155 | 156 | ### command line program 157 | 158 | This package also includes a command line program. 159 | 160 | ``` 161 | Usage: parse-torrent /path/to/torrent 162 | parse-torrent magnet_uri 163 | parse-torrent --stdin 164 | ``` 165 | 166 | To install it, run: 167 | 168 | ``` 169 | npm install parse-torrent -g 170 | ``` 171 | 172 | ## license 173 | 174 | MIT. Copyright (c) [Feross Aboukhadijeh](https://feross.org) and [WebTorrent, LLC](https://webtorrent.io). 175 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! parse-torrent. MIT License. WebTorrent LLC */ 2 | 3 | import bencode from 'bencode' 4 | import fs from 'fs' // browser exclude 5 | import fetch from 'cross-fetch-ponyfill' 6 | import magnet, { encode } from 'magnet-uri' 7 | import path from 'path' 8 | import { hash, arr2hex, text2arr, arr2text } from 'uint8-util' 9 | import queueMicrotask from 'queue-microtask' 10 | 11 | /** 12 | * Parse a torrent identifier (magnet uri, .torrent file, info hash) 13 | * @param {string|ArrayBufferView|Object} torrentId 14 | * @return {Object} 15 | */ 16 | async function parseTorrent (torrentId) { 17 | if (typeof torrentId === 'string' && /^(stream-)?magnet:/.test(torrentId)) { 18 | // if magnet uri (string) 19 | const torrentObj = magnet(torrentId) 20 | 21 | // infoHash won't be defined if a non-bittorrent magnet is passed 22 | if (!torrentObj.infoHash) { 23 | throw new Error('Invalid torrent identifier') 24 | } 25 | 26 | return torrentObj 27 | } else if (typeof torrentId === 'string' && (/^[a-f0-9]{40}$/i.test(torrentId) || /^[a-z2-7]{32}$/i.test(torrentId))) { 28 | // if info hash (hex/base-32 string) 29 | return magnet(`magnet:?xt=urn:btih:${torrentId}`) 30 | } else if (ArrayBuffer.isView(torrentId) && torrentId.length === 20) { 31 | // if info hash (buffer) 32 | return magnet(`magnet:?xt=urn:btih:${arr2hex(torrentId)}`) 33 | } else if (ArrayBuffer.isView(torrentId)) { 34 | // if .torrent file (buffer) 35 | return await decodeTorrentFile(torrentId) // might throw 36 | } else if (torrentId && torrentId.infoHash) { 37 | // if parsed torrent (from `parse-torrent` or `magnet-uri`) 38 | torrentId.infoHash = torrentId.infoHash.toLowerCase() 39 | 40 | if (!torrentId.announce) torrentId.announce = [] 41 | 42 | if (typeof torrentId.announce === 'string') { 43 | torrentId.announce = [torrentId.announce] 44 | } 45 | 46 | if (!torrentId.urlList) torrentId.urlList = [] 47 | 48 | return torrentId 49 | } else { 50 | throw new Error('Invalid torrent identifier') 51 | } 52 | } 53 | 54 | async function parseTorrentRemote (torrentId, opts, cb) { 55 | if (typeof opts === 'function') return parseTorrentRemote(torrentId, {}, opts) 56 | if (typeof cb !== 'function') throw new Error('second argument must be a Function') 57 | 58 | let parsedTorrent 59 | try { 60 | parsedTorrent = await parseTorrent(torrentId) 61 | } catch (err) { 62 | // If torrent fails to parse, it could be a Blob, http/https URL or 63 | // filesystem path, so don't consider it an error yet. 64 | } 65 | 66 | if (parsedTorrent && parsedTorrent.infoHash) { 67 | queueMicrotask(() => { 68 | cb(null, parsedTorrent) 69 | }) 70 | } else if (isBlob(torrentId)) { 71 | try { 72 | const torrentBuf = new Uint8Array(await torrentId.arrayBuffer()) 73 | parseOrThrow(torrentBuf) 74 | } catch (err) { 75 | return cb(new Error(`Error converting Blob: ${err.message}`)) 76 | } 77 | } else if (/^https?:/.test(torrentId)) { 78 | try { 79 | const res = await fetch(torrentId, { 80 | headers: { 'user-agent': 'WebTorrent (https://webtorrent.io)' }, 81 | signal: AbortSignal.timeout(30 * 1000), 82 | ...opts 83 | }) 84 | const torrentBuf = new Uint8Array(await res.arrayBuffer()) 85 | parseOrThrow(torrentBuf) 86 | } catch (err) { 87 | return cb(new Error(`Error downloading torrent: ${err.message}`)) 88 | } 89 | } else if (typeof fs.readFile === 'function' && typeof torrentId === 'string') { 90 | // assume it's a filesystem path 91 | fs.readFile(torrentId, (err, torrentBuf) => { 92 | if (err) return cb(new Error('Invalid torrent identifier')) 93 | parseOrThrow(torrentBuf) 94 | }) 95 | } else { 96 | queueMicrotask(() => { 97 | cb(new Error('Invalid torrent identifier')) 98 | }) 99 | } 100 | 101 | async function parseOrThrow (torrentBuf) { 102 | try { 103 | parsedTorrent = await parseTorrent(torrentBuf) 104 | } catch (err) { 105 | return cb(err) 106 | } 107 | if (parsedTorrent && parsedTorrent.infoHash) cb(null, parsedTorrent) 108 | else cb(new Error('Invalid torrent identifier')) 109 | } 110 | } 111 | 112 | /** 113 | * Parse a torrent. Throws an exception if the torrent is missing required fields. 114 | * @param {ArrayBufferView|Object} torrent 115 | * @return {Object} parsed torrent 116 | */ 117 | async function decodeTorrentFile (torrent) { 118 | if (ArrayBuffer.isView(torrent)) { 119 | torrent = bencode.decode(torrent) 120 | } 121 | 122 | // sanity check 123 | ensure(torrent.info, 'info') 124 | ensure(torrent.info['name.utf-8'] || torrent.info.name, 'info.name') 125 | ensure(torrent.info['piece length'], 'info[\'piece length\']') 126 | ensure(torrent.info.pieces, 'info.pieces') 127 | 128 | if (torrent.info.files) { 129 | torrent.info.files.forEach(file => { 130 | ensure(typeof file.length === 'number', 'info.files[0].length') 131 | ensure(file['path.utf-8'] || file.path, 'info.files[0].path') 132 | }) 133 | } else { 134 | ensure(typeof torrent.info.length === 'number', 'info.length') 135 | } 136 | 137 | const result = { 138 | info: torrent.info, 139 | infoBuffer: bencode.encode(torrent.info), 140 | name: arr2text(torrent.info['name.utf-8'] || torrent.info.name), 141 | announce: [] 142 | } 143 | 144 | result.infoHashBuffer = await hash(result.infoBuffer) 145 | result.infoHash = arr2hex(result.infoHashBuffer) 146 | 147 | if (torrent.info.private !== undefined) result.private = !!torrent.info.private 148 | 149 | if (torrent['creation date']) result.created = new Date(torrent['creation date'] * 1000) 150 | if (torrent['created by']) result.createdBy = arr2text(torrent['created by']) 151 | 152 | if (ArrayBuffer.isView(torrent.comment)) result.comment = arr2text(torrent.comment) 153 | 154 | // announce and announce-list will be missing if metadata fetched via ut_metadata 155 | if (Array.isArray(torrent['announce-list']) && torrent['announce-list'].length > 0) { 156 | torrent['announce-list'].forEach(urls => { 157 | urls.forEach(url => { 158 | result.announce.push(arr2text(url)) 159 | }) 160 | }) 161 | } else if (torrent.announce) { 162 | result.announce.push(arr2text(torrent.announce)) 163 | } 164 | 165 | // handle url-list (BEP19 / web seeding) 166 | if (ArrayBuffer.isView(torrent['url-list'])) { 167 | // some clients set url-list to empty string 168 | torrent['url-list'] = torrent['url-list'].length > 0 169 | ? [torrent['url-list']] 170 | : [] 171 | } 172 | result.urlList = (torrent['url-list'] || []).map(url => arr2text(url)) 173 | 174 | // remove duplicates by converting to Set and back 175 | result.announce = Array.from(new Set(result.announce)) 176 | result.urlList = Array.from(new Set(result.urlList)) 177 | 178 | let sum = 0 179 | const files = torrent.info.files || [torrent.info] 180 | result.files = files.map((file, i) => { 181 | const parts = [].concat(result.name, file['path.utf-8'] || file.path || []).map(p => ArrayBuffer.isView(p) ? arr2text(p) : p) 182 | sum += file.length 183 | return { 184 | path: path.join.apply(null, [path.sep].concat(parts)).slice(1), 185 | name: parts[parts.length - 1], 186 | length: file.length, 187 | offset: sum - file.length 188 | } 189 | }) 190 | 191 | result.length = sum 192 | 193 | const lastFile = result.files[result.files.length - 1] 194 | 195 | result.pieceLength = torrent.info['piece length'] 196 | result.lastPieceLength = ((lastFile.offset + lastFile.length) % result.pieceLength) || result.pieceLength 197 | result.pieces = splitPieces(torrent.info.pieces) 198 | 199 | return result 200 | } 201 | 202 | /** 203 | * Convert a parsed torrent object back into a .torrent file buffer. 204 | * @param {Object} parsed parsed torrent 205 | * @return {Uint8Array} 206 | */ 207 | function encodeTorrentFile (parsed) { 208 | const torrent = { 209 | info: parsed.info 210 | } 211 | 212 | torrent['announce-list'] = (parsed.announce || []).map(url => { 213 | if (!torrent.announce) torrent.announce = url 214 | url = text2arr(url) 215 | return [url] 216 | }) 217 | 218 | torrent['url-list'] = parsed.urlList || [] 219 | 220 | if (parsed.private !== undefined) { 221 | torrent.private = Number(parsed.private) 222 | } 223 | 224 | if (parsed.created) { 225 | torrent['creation date'] = (parsed.created.getTime() / 1000) | 0 226 | } 227 | 228 | if (parsed.createdBy) { 229 | torrent['created by'] = parsed.createdBy 230 | } 231 | 232 | if (parsed.comment) { 233 | torrent.comment = parsed.comment 234 | } 235 | 236 | return bencode.encode(torrent) 237 | } 238 | 239 | /** 240 | * Check if `obj` is a W3C `Blob` or `File` object 241 | * @param {*} obj 242 | * @return {boolean} 243 | */ 244 | function isBlob (obj) { 245 | return typeof Blob !== 'undefined' && obj instanceof Blob 246 | } 247 | 248 | function splitPieces (buf) { 249 | const pieces = [] 250 | for (let i = 0; i < buf.length; i += 20) { 251 | pieces.push(arr2hex(buf.slice(i, i + 20))) 252 | } 253 | return pieces 254 | } 255 | 256 | function ensure (bool, fieldName) { 257 | if (!bool) throw new Error(`Torrent is missing required field: ${fieldName}`) 258 | } 259 | 260 | export default parseTorrent 261 | const toMagnetURI = encode 262 | export { parseTorrentRemote as remote, encodeTorrentFile as toTorrentFile, toMagnetURI } 263 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [11.0.19](https://github.com/webtorrent/parse-torrent/compare/v11.0.18...v11.0.19) (2025-10-07) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * Greatly improve efficiency with torrents with a large number of files ([#198](https://github.com/webtorrent/parse-torrent/issues/198)) ([964e080](https://github.com/webtorrent/parse-torrent/commit/964e0805e1ddb0561b626fab21fda5dd3f82fa3c)) 7 | 8 | ## [11.0.18](https://github.com/webtorrent/parse-torrent/compare/v11.0.17...v11.0.18) (2025-01-04) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * **deps:** update dependency magnet-uri to ^7.0.7 ([#192](https://github.com/webtorrent/parse-torrent/issues/192)) ([aaeb8d5](https://github.com/webtorrent/parse-torrent/commit/aaeb8d59be09c29ed740a679e4f14c54a4bfbd28)) 14 | 15 | ## [11.0.17](https://github.com/webtorrent/parse-torrent/compare/v11.0.16...v11.0.17) (2024-06-29) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * **deps:** update dependency uint8-util to ^2.2.5 ([#185](https://github.com/webtorrent/parse-torrent/issues/185)) ([c0c72ce](https://github.com/webtorrent/parse-torrent/commit/c0c72ceb2ca7484434cf4c25563191697febf12a)) 21 | 22 | ## [11.0.16](https://github.com/webtorrent/parse-torrent/compare/v11.0.15...v11.0.16) (2024-01-16) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * **deps:** update dependency uint8-util to ^2.2.4 ([#167](https://github.com/webtorrent/parse-torrent/issues/167)) ([8983eac](https://github.com/webtorrent/parse-torrent/commit/8983eaccd2d0e94cb953fb4c37d54ed6d9c8dba6)) 28 | 29 | ## [11.0.15](https://github.com/webtorrent/parse-torrent/compare/v11.0.14...v11.0.15) (2024-01-16) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * build badges url ([#180](https://github.com/webtorrent/parse-torrent/issues/180)) ([15134f5](https://github.com/webtorrent/parse-torrent/commit/15134f5d753f96fdbc643ebb72d298f80be94d37)) 35 | 36 | ## [11.0.14](https://github.com/webtorrent/parse-torrent/compare/v11.0.13...v11.0.14) (2023-08-11) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * **deps:** update dependency uint8-util to ^2.2.2 ([#162](https://github.com/webtorrent/parse-torrent/issues/162)) ([15ba6e0](https://github.com/webtorrent/parse-torrent/commit/15ba6e022c53d17d8a15deebbac887736638af4e)) 42 | 43 | ## [11.0.13](https://github.com/webtorrent/parse-torrent/compare/v11.0.12...v11.0.13) (2023-08-10) 44 | 45 | 46 | ### Bug Fixes 47 | 48 | * **deps:** update dependency bencode to v4 ([#164](https://github.com/webtorrent/parse-torrent/issues/164)) ([89b0b2b](https://github.com/webtorrent/parse-torrent/commit/89b0b2b76414a6773b9ce0c31ce8cc004ed7e3b8)) 49 | 50 | ## [11.0.12](https://github.com/webtorrent/parse-torrent/compare/v11.0.11...v11.0.12) (2023-05-31) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * **deps:** update dependency magnet-uri to ^7.0.5 ([#156](https://github.com/webtorrent/parse-torrent/issues/156)) ([0784624](https://github.com/webtorrent/parse-torrent/commit/0784624754efeeeb6c2360822231bbd908572dfc)) 56 | 57 | ## [11.0.11](https://github.com/webtorrent/parse-torrent/compare/v11.0.10...v11.0.11) (2023-05-30) 58 | 59 | 60 | ### Bug Fixes 61 | 62 | * remove unused hack ([#155](https://github.com/webtorrent/parse-torrent/issues/155)) ([ea0cc5e](https://github.com/webtorrent/parse-torrent/commit/ea0cc5eb589375d97698426b11963acacc5345b8)) 63 | 64 | ## [11.0.10](https://github.com/webtorrent/parse-torrent/compare/v11.0.9...v11.0.10) (2023-05-27) 65 | 66 | 67 | ### Bug Fixes 68 | 69 | * **deps:** update dependency magnet-uri to ^7.0.4 ([#154](https://github.com/webtorrent/parse-torrent/issues/154)) ([446a0ba](https://github.com/webtorrent/parse-torrent/commit/446a0ba598b20bef7d7fa4e4d475754144801f54)) 70 | 71 | ## [11.0.9](https://github.com/webtorrent/parse-torrent/compare/v11.0.8...v11.0.9) (2023-05-27) 72 | 73 | 74 | ### Bug Fixes 75 | 76 | * **deps:** update dependency uint8-util to ^2.1.9 ([#146](https://github.com/webtorrent/parse-torrent/issues/146)) ([e0c1db2](https://github.com/webtorrent/parse-torrent/commit/e0c1db2f089e1bb02ee8d89fc8348fc5582506a7)) 77 | 78 | ## [11.0.8](https://github.com/webtorrent/parse-torrent/compare/v11.0.7...v11.0.8) (2023-04-03) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * **deps:** update dependency magnet-uri to ^7.0.3 ([#150](https://github.com/webtorrent/parse-torrent/issues/150)) ([6406b7b](https://github.com/webtorrent/parse-torrent/commit/6406b7ba31631718aad0aa5a918045804ddf4cf7)) 84 | 85 | ## [11.0.7](https://github.com/webtorrent/parse-torrent/compare/v11.0.6...v11.0.7) (2023-01-31) 86 | 87 | 88 | ### Bug Fixes 89 | 90 | * **deps:** update dependency cross-fetch-ponyfill to ^1.0.3 ([#144](https://github.com/webtorrent/parse-torrent/issues/144)) ([6df9d6e](https://github.com/webtorrent/parse-torrent/commit/6df9d6ec56fc3e82c87aa3690ee5fccc8d79c3d8)) 91 | * **deps:** update webtorrent ([#142](https://github.com/webtorrent/parse-torrent/issues/142)) ([6f865fe](https://github.com/webtorrent/parse-torrent/commit/6f865fe41386c9870fdaad57880d4bb82b4bc779)), closes [#145](https://github.com/webtorrent/parse-torrent/issues/145) 92 | 93 | ## [11.0.6](https://github.com/webtorrent/parse-torrent/compare/v11.0.5...v11.0.6) (2023-01-31) 94 | 95 | 96 | ### Bug Fixes 97 | 98 | * **deps:** update dependency uint8-util to ^2.1.7 ([#143](https://github.com/webtorrent/parse-torrent/issues/143)) ([bfa0190](https://github.com/webtorrent/parse-torrent/commit/bfa019012a5c294de2760564f10e4407eebf5bcc)) 99 | 100 | ## [11.0.5](https://github.com/webtorrent/parse-torrent/compare/v11.0.4...v11.0.5) (2023-01-31) 101 | 102 | 103 | ### Bug Fixes 104 | 105 | * fs polyfill ([#140](https://github.com/webtorrent/parse-torrent/issues/140)) ([a39ed02](https://github.com/webtorrent/parse-torrent/commit/a39ed029c087a40be44c0de2b92e8dcca07cadf7)) 106 | 107 | ## [11.0.4](https://github.com/webtorrent/parse-torrent/compare/v11.0.3...v11.0.4) (2023-01-27) 108 | 109 | 110 | ### Bug Fixes 111 | 112 | * ESM imports ([#139](https://github.com/webtorrent/parse-torrent/issues/139)) ([fc29cd8](https://github.com/webtorrent/parse-torrent/commit/fc29cd8099f051753c503d90e3abc7ceed91150a)) 113 | 114 | ## [11.0.3](https://github.com/webtorrent/parse-torrent/compare/v11.0.2...v11.0.3) (2023-01-26) 115 | 116 | 117 | ### Bug Fixes 118 | 119 | * move global to package.json ([#138](https://github.com/webtorrent/parse-torrent/issues/138)) ([1112d2a](https://github.com/webtorrent/parse-torrent/commit/1112d2a8423972f727dac91e0dfe7806c8ac8a1c)) 120 | 121 | ## [11.0.2](https://github.com/webtorrent/parse-torrent/compare/v11.0.1...v11.0.2) (2023-01-26) 122 | 123 | 124 | ### Bug Fixes 125 | 126 | * **deps:** update dependency uint8-util to ^2.1.5 ([#135](https://github.com/webtorrent/parse-torrent/issues/135)) ([a2d03c5](https://github.com/webtorrent/parse-torrent/commit/a2d03c553e1ff332574a8b938988b597fab32ad9)) 127 | 128 | ## [11.0.1](https://github.com/webtorrent/parse-torrent/compare/v11.0.0...v11.0.1) (2023-01-25) 129 | 130 | 131 | ### Performance Improvements 132 | 133 | * drop simple-get ([#131](https://github.com/webtorrent/parse-torrent/issues/131)) ([0176518](https://github.com/webtorrent/parse-torrent/commit/01765183c3a032d503c5258467f0ff09587df9fc)) 134 | 135 | # [11.0.0](https://github.com/webtorrent/parse-torrent/compare/v10.0.2...v11.0.0) (2023-01-25) 136 | 137 | 138 | ### Bug Fixes 139 | 140 | * **deps:** update dependency bencode to v3 ([#122](https://github.com/webtorrent/parse-torrent/issues/122)) ([2bfae53](https://github.com/webtorrent/parse-torrent/commit/2bfae532e57f83c3babd80d0564f48af8d28292f)) 141 | * **deps:** update dependency get-stdin to v9 ([#93](https://github.com/webtorrent/parse-torrent/issues/93)) ([e36a99e](https://github.com/webtorrent/parse-torrent/commit/e36a99e3d4176a37956f41c873c701f4fd0cc570)) 142 | * **deps:** update dependency uint8-util to ^2.1.4 ([#129](https://github.com/webtorrent/parse-torrent/issues/129)) ([35ede29](https://github.com/webtorrent/parse-torrent/commit/35ede29dc83651fd59d8d752106938d19874e295)) 143 | * drop rusha ([#117](https://github.com/webtorrent/parse-torrent/issues/117)) ([0d3be61](https://github.com/webtorrent/parse-torrent/commit/0d3be61f453d79ab5ba7751bd30e460ccea2f69b)) 144 | * release config ([#134](https://github.com/webtorrent/parse-torrent/issues/134)) ([ec9bf75](https://github.com/webtorrent/parse-torrent/commit/ec9bf750b4a44bd20d5fcb1fec3218c54fa57f7c)) 145 | 146 | 147 | ### BREAKING CHANGES 148 | 149 | * perf: drop rusha, buffer 150 | 151 | * fix: error throw tests 152 | 153 | ## [10.0.2](https://github.com/webtorrent/parse-torrent/compare/v10.0.1...v10.0.2) (2023-01-11) 154 | 155 | ## [10.0.1](https://github.com/webtorrent/parse-torrent/compare/v10.0.0...v10.0.1) (2022-12-04) 156 | 157 | 158 | ### Bug Fixes 159 | 160 | * cli ([#124](https://github.com/webtorrent/parse-torrent/issues/124)) ([b67d213](https://github.com/webtorrent/parse-torrent/commit/b67d213a66fbd526bc961488af8b2bf65d08a108)) 161 | 162 | # [10.0.0](https://github.com/webtorrent/parse-torrent/compare/v9.1.5...v10.0.0) (2022-11-28) 163 | 164 | 165 | ### Features 166 | 167 | * esm ([#118](https://github.com/webtorrent/parse-torrent/issues/118)) ([51551a5](https://github.com/webtorrent/parse-torrent/commit/51551a5d7d464df7d8c81cc70c97648d5d2ddefb)) 168 | 169 | 170 | ### BREAKING CHANGES 171 | 172 | * ESM only 173 | 174 | ## [9.1.5](https://github.com/webtorrent/parse-torrent/compare/v9.1.4...v9.1.5) (2022-03-26) 175 | 176 | 177 | ### Bug Fixes 178 | 179 | * **deps:** update dependency simple-get to ^4.0.1 ([7860eda](https://github.com/webtorrent/parse-torrent/commit/7860edad8bb5dd9ba2cc7135452aea173f70ccc1)) 180 | 181 | ## [9.1.4](https://github.com/webtorrent/parse-torrent/compare/v9.1.3...v9.1.4) (2021-08-04) 182 | 183 | 184 | ### Bug Fixes 185 | 186 | * **deps:** update dependency bencode to ^2.0.2 ([#98](https://github.com/webtorrent/parse-torrent/issues/98)) ([38d9dc3](https://github.com/webtorrent/parse-torrent/commit/38d9dc33b74f9f320e01ea52cb4a2796625617cc)) 187 | * **deps:** update webtorrent ([f5e7992](https://github.com/webtorrent/parse-torrent/commit/f5e79929eb0b5f397fe4a4e5e3ee5b54285211fb)) 188 | --------------------------------------------------------------------------------