├── .gitignore ├── .travis.yml ├── bin ├── ascii-logo.txt ├── update-authors.sh └── cmd.js ├── README.md ├── LICENSE ├── AUTHORS.md ├── package.json ├── CONTRIBUTING.md └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'node' 4 | - '4' 5 | -------------------------------------------------------------------------------- /bin/ascii-logo.txt: -------------------------------------------------------------------------------- 1 | _ _ _ 2 | __ _____| |__ | |_ ___ _ __ _ __ ___ _ __ | |_ 3 | \ \ /\ / / _ \ '_ \| __/ _ \| '__| '__/ _ \ '_ \| __| 4 | \ V V / __/ |_) | || (_) | | | | | __/ | | | |_ 5 | \_/\_/ \___|_.__/ \__\___/|_| |_| \___|_| |_|\__| 6 | -------------------------------------------------------------------------------- /bin/update-authors.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Update AUTHORS.md based on git history. 3 | 4 | git log --reverse --format='%aN <%aE>' | perl -we ' 5 | BEGIN { 6 | %seen = (), @authors = (); 7 | } 8 | while (<>) { 9 | next if $seen{$_}; 10 | next if //; 11 | next if /<.*\@users.noreply.github.com>/; 12 | $seen{$_} = push @authors, "- ", $_; 13 | } 14 | END { 15 | print "# Authors\n\n"; 16 | print "#### Ordered by first contribution.\n\n"; 17 | print @authors, "\n"; 18 | print "#### Generated by bin/update-authors.sh.\n\n"; 19 | } 20 | ' > AUTHORS.md 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a reference implementation for the Decentralized Mutable Torrents extension proposed here: https://github.com/lmatteis/bittorrent.org/blob/master/beps/bep_0046.rst 2 | 3 | Follow discussion about this BEP here: https://github.com/bittorrent/bittorrent.org/issues/34 4 | 5 | ### Install 6 | 7 | ```bash 8 | npm install 9 | ``` 10 | 11 | ### Usage 12 | 13 | ```bash 14 | $ ./bin/cmd.js --help 15 | 16 | - mutable torrents commands: 17 | keypair Create new public-private keypair 18 | publish Publish new torrent 19 | consume Downloads mutable torrent 20 | ``` 21 | 22 | ### License 23 | 24 | MIT. 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 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 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Authors 2 | 3 | #### Ordered by first contribution. 4 | 5 | - Feross Aboukhadijeh 6 | - cagedwisdom 7 | - Shyam S Kumar 8 | - fisch0920 9 | - gtuk 10 | - opfl 11 | - Chris 12 | - Astro 13 | - Sindre Sorhus 14 | - grunjol 15 | - Johnny Tong 16 | - Josip Janžić 17 | - Simba Zhang 18 | - Gilles De Mey 19 | - Linus Unnebäck 20 | - Joseph Frazier 21 | - Valérian Galliat 22 | - Yoann Ciabaud 23 | - Ivan Vučica 24 | - Romain Beaumont 25 | - Wim 26 | - Joseph Frazier <1212jtraceur@gmail.com> 27 | - James Halliday 28 | - Autarc 29 | - Nate Goldman 30 | - Diego Rodríguez Baquero 31 | - Christophe 32 | 33 | #### Generated by bin/update-authors.sh. 34 | 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webtorrent-cli", 3 | "description": "WebTorrent, the streaming torrent client. For the command line.", 4 | "version": "1.2.3", 5 | "author": { 6 | "name": "WebTorrent, LLC", 7 | "email": "feross@webtorrent.io", 8 | "url": "https://webtorrent.io" 9 | }, 10 | "bin": { 11 | "webtorrent": "./bin/cmd.js" 12 | }, 13 | "bugs": { 14 | "url": "https://github.com/feross/webtorrent-cli/issues" 15 | }, 16 | "dependencies": { 17 | "bencode": "^0.10.0", 18 | "clivas": "^0.2.0", 19 | "create-torrent": "^3.23.1", 20 | "ed25519-supercop": "^1.0.2", 21 | "executable": "^3.0.0", 22 | "minimist": "^1.2.0", 23 | "moment": "^2.12.0", 24 | "network-address": "^1.1.0", 25 | "parse-torrent": "^5.7.3", 26 | "prettier-bytes": "^1.0.3", 27 | "vlc-command": "^1.0.0", 28 | "webtorrent": "0.x", 29 | "winreg": "^1.0.1" 30 | }, 31 | "devDependencies": { 32 | "cross-spawn-async": "^2.1.9", 33 | "standard": "^6.0.8", 34 | "tape": "^4.5.1", 35 | "webtorrent-fixtures": "^1.2.0", 36 | "xtend": "^4.0.1" 37 | }, 38 | "homepage": "https://webtorrent.io", 39 | "keywords": [ 40 | "bittorrent", 41 | "bittorrent client", 42 | "download", 43 | "mad science", 44 | "streaming", 45 | "torrent", 46 | "webrtc", 47 | "webrtc data", 48 | "webtorrent" 49 | ], 50 | "license": "MIT", 51 | "main": "index.js", 52 | "optionalDependencies": { 53 | "airplay-js": "^0.2.3", 54 | "chromecasts": "^1.5.3", 55 | "nodebmc": "0.0.6" 56 | }, 57 | "preferGlobal": true, 58 | "repository": { 59 | "type": "git", 60 | "url": "git://github.com/feross/webtorrent-cli.git" 61 | }, 62 | "scripts": { 63 | "test": "standard && tape test.js", 64 | "update-authors": "./bin/update-authors.sh" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Contributions welcome! 4 | 5 | **Before spending lots of time on something, ask for feedback on your idea first!** 6 | 7 | Please search issues and pull requests before adding something new to avoid duplicating 8 | efforts and conversations. 9 | 10 | This project welcomes non-code contributions, too! The following types of contributions 11 | are welcome: 12 | 13 | - **Ideas**: participate in an issue thread or start your own to have your voice heard. 14 | - **Writing**: contribute your expertise in an area by helping expand the included docs. 15 | - **Copy editing**: fix typos, clarify language, and improve the quality of the docs. 16 | - **Formatting**: help keep docs easy to read with consistent formatting. 17 | 18 | ## Code Style 19 | 20 | [![standard][standard-image]][standard-url] 21 | 22 | This repository uses [`standard`][standard-url] to maintain code style and consistency, 23 | and to avoid style arguments. `npm test` runs `standard` automatically, so you don't have 24 | to! 25 | 26 | [standard-image]: https://cdn.rawgit.com/feross/standard/master/badge.svg 27 | [standard-url]: https://github.com/feross/standard 28 | 29 | ## Project Governance 30 | 31 | Individuals making significant and valuable contributions are given commit-access to the 32 | project to contribute as they see fit. This project is more like an open wiki than a 33 | standard guarded open source project. 34 | 35 | ### Rules 36 | 37 | There are a few basic ground-rules for contributors: 38 | 39 | 1. **No `--force` pushes** or modifying the Git history in any way. 40 | 2. **Non-master branches** should be used for ongoing work. 41 | 3. **Significant modifications** like API changes should be subject to a **pull request** 42 | to solicit feedback from other contributors. 43 | 4. **Pull requests** are *encouraged* for all contributions to solicit feedback, but left to 44 | the discretion of the contributor. 45 | 46 | ### Releases 47 | 48 | Declaring formal releases remains the prerogative of the project maintainer. 49 | 50 | ### Changes to this arrangement 51 | 52 | This is an experiment and feedback is welcome! This document may also be subject to pull- 53 | requests or changes by contributors where you believe you have something valuable to add 54 | or change. 55 | 56 | ## Developer's Certificate of Origin 1.1 57 | 58 | By making a contribution to this project, I certify that: 59 | 60 | - (a) The contribution was created in whole or in part by me and I have the right to 61 | submit it under the open source license indicated in the file; or 62 | 63 | - (b) The contribution is based upon previous work that, to the best of my knowledge, is 64 | covered under an appropriate open source license and I have the right under that license 65 | to submit that work with modifications, whether created in whole or in part by me, under 66 | the same open source license (unless I am permitted to submit under a different 67 | license), as indicated in the file; or 68 | 69 | - (c) The contribution was provided directly to me by some other person who certified 70 | (a), (b) or (c) and I have not modified it. 71 | 72 | - (d) I understand and agree that this project and the contribution are public and that a 73 | record of the contribution (including all personal information I submit with it, 74 | including my sign-off) is maintained indefinitely and may be redistributed consistent 75 | with this project or the open source license(s) involved. 76 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var cp = require('child_process') 2 | var extend = require('xtend') 3 | var fixtures = require('webtorrent-fixtures') 4 | var parseTorrent = require('parse-torrent') 5 | var path = require('path') 6 | var spawn = require('cross-spawn-async') 7 | var test = require('tape') 8 | 9 | var CMD_PATH = path.resolve(__dirname, 'bin', 'cmd.js') 10 | var CMD = 'node ' + CMD_PATH 11 | 12 | test('Command line: webtorrent help', function (t) { 13 | t.plan(6) 14 | 15 | cp.exec(CMD + ' help', function (err, data) { 16 | t.error(err) // no error, exit code 0 17 | t.ok(data.toLowerCase().indexOf('usage') !== -1) 18 | }) 19 | 20 | cp.exec(CMD + ' --help', function (err, data) { 21 | t.error(err) // no error, exit code 0 22 | t.ok(data.toLowerCase().indexOf('usage') !== -1) 23 | }) 24 | 25 | cp.exec(CMD, function (err, data) { 26 | t.error(err) // no error, exit code 0 27 | t.ok(data.toLowerCase().indexOf('usage') !== -1) 28 | }) 29 | }) 30 | 31 | test('Command line: webtorrent version', function (t) { 32 | t.plan(6) 33 | var expectedVersion = require('./package.json').version + '\n' 34 | 35 | cp.exec(CMD + ' version', function (err, data) { 36 | t.error(err) 37 | t.ok(data.indexOf(expectedVersion)) 38 | }) 39 | 40 | cp.exec(CMD + ' --version', function (err, data) { 41 | t.error(err) 42 | t.ok(data.indexOf(expectedVersion)) 43 | }) 44 | 45 | cp.exec(CMD + ' -v', function (err, data) { 46 | t.error(err) 47 | t.ok(data.indexOf(expectedVersion)) 48 | }) 49 | }) 50 | 51 | test('Command line: webtorrent info /path/to/file.torrent', function (t) { 52 | t.plan(3) 53 | 54 | cp.exec(CMD + ' info ' + fixtures.leaves.torrentPath, function (err, data) { 55 | t.error(err) 56 | data = JSON.parse(data) 57 | var parsedTorrent = extend(fixtures.leaves.parsedTorrent) 58 | delete parsedTorrent.info 59 | delete parsedTorrent.infoBuffer 60 | delete parsedTorrent.infoHashBuffer 61 | t.deepEqual(data, JSON.parse(JSON.stringify(parsedTorrent, undefined, 2))) 62 | }) 63 | 64 | cp.exec(CMD + ' info /bad/path', function (err) { 65 | t.ok(err instanceof Error) 66 | }) 67 | }) 68 | 69 | test('Command line: webtorrent info magnet_uri', function (t) { 70 | t.plan(2) 71 | 72 | var leavesMagnetURI = 'magnet:?xt=urn:btih:d2474e86c95b19b8bcfdb92bc12c9d44667cfa36&dn=Leaves+of+Grass+by+Walt+Whitman.epub&tr=http%3A%2F%2Ftracker.example.com%2Fannounce&tr=http%3A%2F%2Ftracker.example2.com%2Fannounce&tr=udp%3A%2F%2Ftracker.example3.com%3A3310%2Fannounce&tr=udp%3A%2F%2Ftracker.example4.com%3A80&tr=udp%3A%2F%2Ftracker.example5.com%3A80&tr=udp%3A%2F%2Ftracker.example6.com%3A80' 73 | 74 | cp.exec(CMD + ' info "' + leavesMagnetURI + '"', function (err, data) { 75 | t.error(err) 76 | data = JSON.parse(data) 77 | var parsedTorrent = parseTorrent(leavesMagnetURI) 78 | delete parsedTorrent.infoHashBuffer 79 | t.deepEqual(data, JSON.parse(JSON.stringify(parsedTorrent, undefined, 2))) 80 | }) 81 | }) 82 | 83 | test('Command line: webtorrent create /path/to/file', function (t) { 84 | t.plan(1) 85 | 86 | var child = spawn('node', [ CMD_PATH, 'create', fixtures.leaves.contentPath ]) 87 | child.on('error', function (err) { t.fail(err) }) 88 | 89 | var chunks = [] 90 | child.stdout.on('data', function (chunk) { 91 | chunks.push(chunk) 92 | }) 93 | child.stdout.on('end', function () { 94 | var buf = Buffer.concat(chunks) 95 | var parsedTorrent = parseTorrent(new Buffer(buf, 'binary')) 96 | t.deepEqual(parsedTorrent.infoHash, 'd2474e86c95b19b8bcfdb92bc12c9d44667cfa36') 97 | }) 98 | }) 99 | 100 | test('Command line: webtorrent download (with local content)', function (t) { 101 | t.plan(2) 102 | 103 | var fixturesPath = path.join(path.dirname(require.resolve('webtorrent-fixtures')), 'fixtures') 104 | 105 | cp.exec(CMD + ' download ' + fixtures.leaves.torrentPath + ' --out ' + fixturesPath, function (err, data) { 106 | t.error(err) 107 | t.ok(data.indexOf('successfully') !== -1) 108 | }) 109 | }) 110 | -------------------------------------------------------------------------------- /bin/cmd.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var clivas = require('clivas') 4 | var cp = require('child_process') 5 | var createTorrent = require('create-torrent') 6 | var executable = require('executable') 7 | var fs = require('fs') 8 | var minimist = require('minimist') 9 | var moment = require('moment') 10 | var networkAddress = require('network-address') 11 | var parseTorrent = require('parse-torrent') 12 | var path = require('path') 13 | var crypto = require('crypto') 14 | var bencode = require('bencode') 15 | var prettierBytes = require('prettier-bytes') 16 | var vlcCommand = require('vlc-command') 17 | var WebTorrent = require('webtorrent') 18 | 19 | var ed = require('ed25519-supercop') 20 | 21 | process.title = 'WebTorrent' 22 | 23 | var expectedError = false 24 | process.on('exit', function (code) { 25 | if (code === 0 || expectedError) return // normal exit 26 | if (code === 130) return // intentional exit with Control-C 27 | 28 | clivas.line('\n{red:UNEXPECTED ERROR:} If this is a bug in WebTorrent, report it!') 29 | clivas.line('{green:OPEN AN ISSUE:} https://github.com/feross/webtorrent/issues\n') 30 | clivas.line( 31 | 'DEBUG INFO: ' + 32 | 'webtorrent-cli ' + require('../package.json').version + ', ' + 33 | 'webtorrent ' + require('webtorrent/package.json').version + ', ' + 34 | 'node ' + process.version + ', ' + 35 | process.platform + ' ' + process.arch + ', ' + 36 | 'exit ' + code 37 | ) 38 | }) 39 | 40 | process.on('SIGINT', gracefulExit) 41 | process.on('SIGTERM', gracefulExit) 42 | 43 | var argv = minimist(process.argv.slice(2), { 44 | alias: { 45 | p: 'port', 46 | b: 'blocklist', 47 | t: 'subtitles', 48 | s: 'select', 49 | o: 'out', 50 | a: 'announce', 51 | q: 'quiet', 52 | h: 'help', 53 | v: 'version' 54 | }, 55 | boolean: [ // options that are always boolean 56 | 'airplay', 57 | 'chromecast', 58 | 'mplayer', 59 | 'mpv', 60 | 'vlc', 61 | 'xbmc', 62 | 'stdout', 63 | 'quiet', 64 | 'keep-seeding', 65 | 'help', 66 | 'version', 67 | 'verbose' 68 | ], 69 | string: [ // options that are always strings 70 | 'out', 71 | 'announce', 72 | 'blocklist', 73 | 'subtitles', 74 | 'on-done', 75 | 'on-exit' 76 | ], 77 | default: { 78 | port: 8000 79 | } 80 | }) 81 | 82 | if (process.env.DEBUG || argv.stdout) { 83 | argv.quiet = argv.q = true 84 | } 85 | 86 | var started = Date.now() 87 | function getRuntime () { 88 | return Math.floor((Date.now() - started) / 1000) 89 | } 90 | 91 | var VLC_ARGS = '--play-and-exit --video-on-top --quiet' 92 | if (process.env.DEBUG) { 93 | VLC_ARGS += ' --extraintf=http:logger --verbose=2 --file-logging --logfile=vlc-log.txt' 94 | } 95 | var MPLAYER_EXEC = 'mplayer -ontop -really-quiet -noidx -loop 0' 96 | var MPV_EXEC = 'mpv --ontop --really-quiet --loop=no' 97 | var OMX_EXEC = 'omxplayer -r -o ' + (typeof argv.omx === 'string' ? argv.omx : 'hdmi') 98 | 99 | if (argv.subtitles) { 100 | VLC_ARGS += ' --sub-file=' + argv.subtitles 101 | MPLAYER_EXEC += ' -sub ' + argv.subtitles 102 | MPV_EXEC += ' --sub-file=' + argv.subtitles 103 | OMX_EXEC += ' --subtitles ' + argv.subtitles 104 | } 105 | 106 | function checkPermission (filename) { 107 | try { 108 | if (!executable.sync(filename)) { 109 | errorAndExit('Script "' + filename + '" is not executable') 110 | } 111 | } catch (err) { 112 | errorAndExit('Script "' + filename + '" does not exist') 113 | } 114 | } 115 | 116 | if (argv['on-done']) { 117 | checkPermission(argv['on-done']) 118 | argv['on-done'] = fs.realpathSync(argv['on-done']) 119 | } 120 | 121 | if (argv['on-exit']) { 122 | checkPermission(argv['on-exit']) 123 | argv['on-exit'] = fs.realpathSync(argv['on-exit']) 124 | } 125 | 126 | playerName = argv.airplay ? 'Airplay' 127 | : argv.chromecast ? 'Chromecast' 128 | : argv.xbmc ? 'XBMC' 129 | : argv.vlc ? 'VLC' 130 | : argv.mplayer ? 'MPlayer' 131 | : argv.mpv ? 'mpv' 132 | : argv.omx ? 'OMXPlayer' 133 | : null 134 | 135 | var command = argv._[0] 136 | 137 | if (['info', 'create', 'download', 'add', 'seed'].indexOf(command) !== -1 && argv._.length !== 2) { 138 | runHelp() 139 | } else if (command === 'help' || argv.help) { 140 | runHelp() 141 | } else if (command === 'version' || argv.version) { 142 | runVersion() 143 | } else if (command === 'info') { 144 | runInfo(/* torrentId */ argv._[1]) 145 | } else if (command === 'create') { 146 | runCreate(/* input */ argv._[1]) 147 | } else if (command === 'download' || command === 'add') { 148 | runDownload(/* torrentId */ argv._[1]) 149 | } else if (command === 'seed') { 150 | runSeed(/* input */ argv._[1]) 151 | } else if (command === 'keypair') { 152 | runKeyPair() 153 | } else if (command === 'publish') { 154 | runPublish(/* pubkey */ argv._[1], /* privkey */ argv._[2], /* info-hash */ argv._[3]) 155 | } else if (command === 'consume') { 156 | runConsume(/* pubkey */ argv._[1]) 157 | } else if (command) { 158 | // assume command is "download" when not specified 159 | runDownload(/* torrentId */ command) 160 | } else { 161 | runHelp() 162 | } 163 | 164 | function runVersion () { 165 | console.log( 166 | require('../package.json').version + 167 | ' (webtorrent ' + require('webtorrent/package.json').version + ')' 168 | ) 169 | process.exit(0) 170 | } 171 | 172 | function runHelp () { 173 | fs.readFileSync(path.join(__dirname, 'ascii-logo.txt'), 'utf8') 174 | .split('\n') 175 | .forEach(function (line) { 176 | clivas.line('{bold:' + line.substring(0, 20) + '}{red:' + line.substring(20) + '}') 177 | }) 178 | 179 | console.log(function () { 180 | /* 181 | Usage: 182 | webtorrent [command] 183 | 184 | Example: 185 | webtorrent download "magnet:..." --vlc 186 | 187 | Commands: 188 | download Download a torrent 189 | seed Seed a file or folder 190 | create Create a .torrent file 191 | info Show info for a .torrent file or magnet uri 192 | 193 | - mutable torrents commands: 194 | keypair Create new public-private keypair 195 | publish Publish new torrent 196 | consume Downloads mutable torrent 197 | 198 | Specify as one of: 199 | * magnet uri 200 | * http url to .torrent file 201 | * filesystem path to .torrent file 202 | * info hash (hex string) 203 | 204 | Options (streaming): 205 | --airplay Apple TV 206 | --chromecast Chromecast 207 | --mplayer MPlayer 208 | --mpv MPV 209 | --omx [jack] omx [default: hdmi] 210 | --vlc VLC 211 | --xbmc XBMC 212 | --stdout standard out (implies --quiet) 213 | 214 | Options (simple): 215 | -o, --out [path] set download destination [default: current directory] 216 | -s, --select [index] select specific file in torrent (omit index for file list) 217 | -t, --subtitles [path] load subtitles file 218 | -v, --version print the current version 219 | 220 | Options (advanced): 221 | -p, --port [number] change the http server port [default: 8000] 222 | -b, --blocklist [path] load blocklist file/http url 223 | -a, --announce [url] tracker URL to announce to 224 | -q, --quiet don't show UI on stdout 225 | --keep-seeding don't quit when done downloading 226 | --on-done [script] run script after torrent download is done 227 | --on-exit [script] run script before program exit 228 | --verbose show torrent protocol details 229 | 230 | */ 231 | }.toString().split(/\n/).slice(2, -2).join('\n')) 232 | process.exit(0) 233 | } 234 | 235 | function runInfo (torrentId) { 236 | var parsedTorrent 237 | try { 238 | parsedTorrent = parseTorrent(torrentId) 239 | } catch (err) { 240 | // If torrent fails to parse, it could be a filesystem path, so don't consider it 241 | // an error yet. 242 | } 243 | 244 | if (!parsedTorrent || !parsedTorrent.infoHash) { 245 | try { 246 | parsedTorrent = parseTorrent(fs.readFileSync(torrentId)) 247 | } catch (err) { 248 | return errorAndExit(err) 249 | } 250 | } 251 | 252 | delete parsedTorrent.info 253 | delete parsedTorrent.infoBuffer 254 | delete parsedTorrent.infoHashBuffer 255 | 256 | var output = JSON.stringify(parsedTorrent, undefined, 2) 257 | if (argv.out) { 258 | fs.writeFileSync(argv.out, output) 259 | } else { 260 | process.stdout.write(output) 261 | } 262 | } 263 | 264 | function runCreate (input) { 265 | if (!argv.createdBy) { 266 | argv.createdBy = 'WebTorrent ' 267 | } 268 | createTorrent(input, argv, function (err, torrent) { 269 | if (err) return errorAndExit(err) 270 | if (argv.out) { 271 | fs.writeFileSync(argv.out, torrent) 272 | } else { 273 | process.stdout.write(torrent) 274 | } 275 | }) 276 | } 277 | 278 | var client, href, playerName, server, serving 279 | 280 | function runDownload (torrentId) { 281 | if (!argv.out && !argv.stdout && !playerName) { 282 | argv.out = process.cwd() 283 | } 284 | 285 | client = new WebTorrent({ blocklist: argv.blocklist }) 286 | client.on('error', fatalError) 287 | 288 | var torrent = client.add(torrentId, { path: argv.out, announce: argv.announce }) 289 | 290 | torrent.on('infoHash', function () { 291 | function updateMetadata () { 292 | clivas.clear() 293 | clivas.line( 294 | '{green:fetching torrent metadata from} {bold:%s} {green:peers}', 295 | torrent.numPeers 296 | ) 297 | } 298 | 299 | if (!argv.quiet) { 300 | updateMetadata() 301 | torrent.on('wire', updateMetadata) 302 | torrent.on('metadata', function () { 303 | clivas.clear() 304 | torrent.removeListener('wire', updateMetadata) 305 | }) 306 | } 307 | }) 308 | 309 | torrent.on('verifying', function (data) { 310 | if (argv.quiet) return 311 | clivas.clear() 312 | clivas.line( 313 | '{green:verifying existing torrent} {bold:%s%} ({bold:%s%} {green:verified})', 314 | Math.floor(data.percentDone), 315 | Math.floor(data.percentVerified) 316 | ) 317 | }) 318 | 319 | torrent.on('done', function () { 320 | if (!argv.quiet) { 321 | var numActiveWires = torrent.wires.reduce(function (num, wire) { 322 | return num + (wire.downloaded > 0) 323 | }, 0) 324 | clivas.line('') 325 | clivas.line( 326 | 'torrent downloaded {green:successfully} from {bold:%s/%s} {green:peers} ' + 327 | 'in {bold:%ss}!', 328 | numActiveWires, 329 | torrent.numPeers, 330 | getRuntime() 331 | ) 332 | } 333 | torrentDone() 334 | }) 335 | 336 | // Start http server 337 | server = torrent.createServer() 338 | 339 | function initServer () { 340 | if (torrent.ready) onReady() 341 | else torrent.once('ready', onReady) 342 | } 343 | 344 | server.listen(argv.port, initServer) 345 | .on('error', function (err) { 346 | if (err.code === 'EADDRINUSE' || err.code === 'EACCES') { 347 | // If port is taken, pick one a free one automatically 348 | return server.listen(0, initServer) 349 | } 350 | fatalError(err) 351 | }) 352 | 353 | server.once('connection', function () { 354 | serving = true 355 | }) 356 | 357 | function onReady () { 358 | if (typeof argv.select === 'boolean') { 359 | clivas.line('Select a file to download:') 360 | torrent.files.forEach(function (file, i) { 361 | clivas.line( 362 | '{2+bold+magenta:%s} %s {blue:(%s)}', 363 | i, file.name, prettierBytes(file.length) 364 | ) 365 | }) 366 | clivas.line('\nTo select a specific file, re-run `webtorrent` with "--select [index]"') 367 | clivas.line('Example: webtorrent download "magnet:..." --select 0') 368 | process.exit(0) 369 | } 370 | 371 | // if no index specified, use largest file 372 | var index = (typeof argv.select === 'number') 373 | ? argv.select 374 | : torrent.files.indexOf(torrent.files.reduce(function (a, b) { 375 | return a.length > b.length ? a : b 376 | })) 377 | onSelection(index) 378 | } 379 | 380 | function onSelection (index) { 381 | href = (argv.airplay || argv.chromecast || argv.xbmc) 382 | ? 'http://' + networkAddress() + ':' + server.address().port + '/' + index 383 | : 'http://localhost:' + server.address().port + '/' + index 384 | 385 | if (playerName) torrent.files[index].select() 386 | if (argv.stdout) torrent.files[index].createReadStream().pipe(process.stdout) 387 | 388 | var cmd 389 | if (argv.vlc) { 390 | vlcCommand(function (err, cmd) { 391 | if (err) return fatalError(err) 392 | if (process.platform === 'win32') { 393 | var args = [].concat(href, VLC_ARGS.split(' ')) 394 | unref(cp.execFile(cmd, args, function (err) { 395 | if (err) return fatalError(err) 396 | torrentDone() 397 | })) 398 | } else { 399 | unref(cp.exec(cmd + ' ' + href + ' ' + VLC_ARGS, function (err) { 400 | if (err) return fatalError(err) 401 | torrentDone() 402 | })) 403 | } 404 | }) 405 | } else if (argv.mplayer) { 406 | cmd = MPLAYER_EXEC + ' ' + href 407 | } else if (argv.mpv) { 408 | cmd = MPV_EXEC + ' ' + href 409 | } else if (argv.omx) { 410 | cmd = OMX_EXEC + ' ' + href 411 | } 412 | 413 | if (cmd) { 414 | unref(cp.exec(cmd, function (err) { 415 | if (err) return fatalError(err) 416 | torrentDone() 417 | })) 418 | } 419 | 420 | if (argv.airplay) { 421 | var airplay = require('airplay-js') 422 | airplay.createBrowser() 423 | .on('deviceOn', function (device) { 424 | device.play(href, 0, function () {}) 425 | }) 426 | .start() 427 | } 428 | 429 | if (argv.chromecast) { 430 | var chromecasts = require('chromecasts')() 431 | chromecasts.on('update', function (player) { 432 | player.play(href, { 433 | title: torrent.name 434 | }) 435 | player.on('error', function (err) { 436 | err.message = 'Chromecast: ' + err.message 437 | errorAndExit(err) 438 | }) 439 | }) 440 | } 441 | 442 | if (argv.xbmc) { 443 | var xbmc = require('nodebmc') 444 | new xbmc.Browser() 445 | .on('deviceOn', function (device) { 446 | device.play(href, function () {}) 447 | }) 448 | } 449 | 450 | drawTorrent(torrent) 451 | } 452 | } 453 | 454 | function runSeed (input) { 455 | if (path.extname(input).toLowerCase() === '.torrent' || /^magnet:/.test(input)) { 456 | // `webtorrent seed` is meant for creating a new torrent based on a file or folder 457 | // of content, not a torrent id (.torrent or a magnet uri). If this command is used 458 | // incorrectly, let's just do the right thing. 459 | runDownload(input) 460 | return 461 | } 462 | 463 | client = new WebTorrent({ blocklist: argv.blocklist }) 464 | client.on('error', fatalError) 465 | 466 | client.seed(input, { announce: argv.announce }, function (torrent) { 467 | if (argv.quiet) console.log(torrent.magnetURI) 468 | drawTorrent(torrent) 469 | }) 470 | } 471 | 472 | function runKeyPair () { 473 | var keypair = ed.createKeyPair(ed.createSeed()) 474 | clivas.line('public key: ' + '{green:' + keypair.publicKey.toString('hex') + '}') 475 | clivas.line('secret key: ' + '{red:' + keypair.secretKey.toString('hex') + '}') 476 | } 477 | 478 | function runPublish (publicKey, secretKey, infoHash) { 479 | var buffPubKey = Buffer(publicKey, 'hex') 480 | var buffSecKey = Buffer(secretKey, 'hex') 481 | var targetID = crypto.createHash('sha1').update(buffPubKey).digest('hex') // XXX missing salt 482 | 483 | client = new WebTorrent({ dht: {verify: ed.verify }}) 484 | client.on('error', fatalError) 485 | 486 | var dht = client.dht 487 | 488 | clivas.write('connecting to DHT... ') 489 | dht.on('ready', function () { 490 | clivas.line('{green:done}') 491 | 492 | var opts = { 493 | k: buffPubKey, 494 | //seq: 0, 495 | v: { 496 | ih: new Buffer(infoHash, 'hex') 497 | }, 498 | sign: function (buf) { 499 | return ed.sign(buf, buffPubKey, buffSecKey) 500 | } 501 | } 502 | 503 | clivas.write('looking up target ID ' + targetID + ' ... ') 504 | dht.get(targetID, function (err, res) { 505 | if (err || !res) { 506 | clivas.line('{red:not found}') 507 | publishSeq(0) 508 | } else { 509 | clivas.line('{green:done}') 510 | publishSeq(res.seq + 1) 511 | } 512 | function publishSeq(seq) { 513 | opts.seq = seq 514 | clivas.line('making request:') 515 | console.log(opts) 516 | clivas.write('... ') 517 | dht.put(opts, function (err, hash) { 518 | if (err) clivas.line('{red:error publishing}') 519 | if (hash) clivas.line('{green:done}') 520 | client.destroy() 521 | }) 522 | 523 | } 524 | }) 525 | 526 | 527 | }) 528 | 529 | } 530 | 531 | function runConsume (publicKey) { 532 | var buffPubKey = Buffer(publicKey, 'hex') 533 | var targetID = crypto.createHash('sha1').update(buffPubKey).digest('hex') // XXX missing salt 534 | 535 | client = new WebTorrent({ dht: {verify: ed.verify }}) 536 | client.on('error', fatalError) 537 | 538 | var dht = client.dht 539 | 540 | clivas.write('connecting to DHT... ') 541 | dht.on('ready', function () { 542 | clivas.line('{green:done}') 543 | 544 | clivas.write('looking up target ID ' + targetID + ' ... ') 545 | 546 | dht.get(targetID, function (err, res) { 547 | if (err || !res) { 548 | clivas.line('{red:not found}') 549 | client.destroy(); 550 | } else { 551 | clivas.line('{green:done}') 552 | console.log('response:') 553 | console.log(res) 554 | // clivas.line('response value: {grey:' + res.v.toString('hex') +'}') 555 | client.destroy(); 556 | 557 | //runDownload(res.v) 558 | } 559 | }) 560 | 561 | }) 562 | } 563 | 564 | var drawInterval 565 | function drawTorrent (torrent) { 566 | if (!argv.quiet) { 567 | process.stdout.write(new Buffer('G1tIG1sySg==', 'base64')) // clear for drawing 568 | drawInterval = setInterval(draw, 1000) 569 | unref(drawInterval) 570 | } 571 | 572 | var hotswaps = 0 573 | torrent.on('hotswap', function () { 574 | hotswaps += 1 575 | }) 576 | 577 | var blockedPeers = 0 578 | torrent.on('blockedPeer', function () { 579 | blockedPeers += 1 580 | }) 581 | 582 | function draw () { 583 | var unchoked = torrent.wires.filter(function (wire) { 584 | return !wire.peerChoking 585 | }) 586 | var linesRemaining = clivas.height 587 | var peerslisted = 0 588 | var speed = torrent.downloadSpeed 589 | var estimate = moment.duration(torrent.timeRemaining / 1000, 'seconds').humanize() 590 | 591 | clivas.clear() 592 | 593 | line( 594 | '{green:' + (seeding ? 'Seeding' : 'Downloading') + ': }' + 595 | '{bold:' + torrent.name + '}' 596 | ) 597 | var seeding = torrent.done 598 | if (seeding) line('{green:Info hash: }' + torrent.infoHash) 599 | if (playerName) { 600 | line( 601 | '{green:Streaming to: }{bold:' + playerName + '} ' + 602 | '{green:Server running at: }{bold:' + href + '}' 603 | ) 604 | } else if (server) { 605 | line('{green:Server running at: }{bold:' + href + '}') 606 | } 607 | if (argv.out) line('{green:Downloading to: }{bold:' + argv.out + '}') 608 | line( 609 | '{green:Speed: }{bold:' + prettierBytes(speed) + '/s} ' + 610 | '{green:Downloaded:} {bold:' + prettierBytes(torrent.downloaded) + '}' + 611 | '/{bold:' + prettierBytes(torrent.length) + '} ' + 612 | '{green:Uploaded:} {bold:' + prettierBytes(torrent.uploaded) + '}' 613 | ) 614 | line( 615 | '{green:Running time:} {bold:' + getRuntime() + 's} ' + 616 | '{green:Time remaining:} {bold:' + estimate + '} ' + 617 | '{green:Peers:} {bold:' + unchoked.length + '/' + torrent.numPeers + '}' 618 | ) 619 | if (argv.verbose) { 620 | line( 621 | '{green:Queued peers:} {bold:' + torrent.numQueued + '} ' + 622 | '{green:Blocked peers:} {bold:' + blockedPeers + '} ' + 623 | '{green:Hotswaps:} {bold:' + hotswaps + '}' 624 | ) 625 | } 626 | line('') 627 | 628 | torrent.wires.every(function (wire) { 629 | var progress = '?' 630 | if (torrent.length) { 631 | var bits = 0 632 | var piececount = Math.ceil(torrent.length / torrent.pieceLength) 633 | for (var i = 0; i < piececount; i++) { 634 | if (wire.peerPieces.get(i)) { 635 | bits++ 636 | } 637 | } 638 | progress = bits === piececount ? 'S' : Math.floor(100 * bits / piececount) + '%' 639 | } 640 | 641 | var str = '{3:%s} {25+magenta:%s} {10:%s} {12+cyan:%s/s} {12+red:%s/s}' 642 | var args = [ 643 | progress, 644 | wire.remoteAddress 645 | ? (wire.remoteAddress + ':' + wire.remotePort) 646 | : 'Unknown', 647 | prettierBytes(wire.downloaded), 648 | prettierBytes(wire.downloadSpeed()), 649 | prettierBytes(wire.uploadSpeed()) 650 | ] 651 | if (argv.verbose) { 652 | str += ' {15+grey:%s} {10+grey:%s}' 653 | var tags = [] 654 | if (wire.requests.length > 0) tags.push(wire.requests.length + ' reqs') 655 | if (wire.peerChoking) tags.push('choked') 656 | var reqStats = wire.requests.map(function (req) { return req.piece }) 657 | args.push(tags.join(', '), reqStats.join(' ')) 658 | } 659 | line.apply(undefined, [].concat(str, args)) 660 | 661 | peerslisted += 1 662 | return linesRemaining > 4 663 | }) 664 | 665 | line('{60:}') 666 | if (torrent.numPeers > peerslisted) { 667 | line('... and %s more', torrent.numPeers - peerslisted) 668 | } 669 | 670 | clivas.flush(true) 671 | 672 | function line () { 673 | clivas.line.apply(clivas, arguments) 674 | linesRemaining -= 1 675 | } 676 | } 677 | } 678 | 679 | function torrentDone () { 680 | if (argv['on-done']) unref(cp.exec(argv['on-done'])) 681 | if (!playerName && !serving && argv.out && !argv['keep-seeding']) gracefulExit() 682 | } 683 | 684 | function fatalError (err) { 685 | clivas.line('{red:Error:} ' + (err.message || err)) 686 | process.exit(1) 687 | } 688 | 689 | function errorAndExit (err) { 690 | clivas.line('{red:Error:} ' + (err.message || err)) 691 | expectedError = true 692 | process.exit(1) 693 | } 694 | 695 | function gracefulExit () { 696 | process.removeListener('SIGINT', gracefulExit) 697 | process.removeListener('SIGTERM', gracefulExit) 698 | clearInterval(drawInterval) 699 | 700 | clivas.line('\n{green:webtorrent is exiting...}') 701 | 702 | if (!client) return 703 | 704 | if (argv['on-exit']) unref(cp.exec(argv['on-exit'])) 705 | 706 | client.destroy(function (err) { 707 | if (err) return fatalError(err) 708 | 709 | // Quit after 1 second. This is only necessary for `webtorrent-hybrid` since 710 | // the `wrtc` package makes node never quit :( 711 | unref(setTimeout(function () { process.exit(0) }, 1000)) 712 | }) 713 | } 714 | 715 | function unref (iv) { 716 | if (iv && typeof iv.unref === 'function') iv.unref() 717 | } 718 | --------------------------------------------------------------------------------