From 6a666528fdaa23230d45676f24bbb5f3b899ada1 Mon Sep 17 00:00:00 2001 From: Feross Aboukhadijeh Date: Fri, 9 Jan 2015 22:03:19 -0800 Subject: command line: add `info` command Fix #233 --- bin/cmd.js | 624 +++++++++++++++++++++++++++++++++---------------------------- 1 file changed, 333 insertions(+), 291 deletions(-) (limited to 'bin') diff --git a/bin/cmd.js b/bin/cmd.js index d3df10a..cf202bb 100755 --- a/bin/cmd.js +++ b/bin/cmd.js @@ -6,6 +6,7 @@ var fs = require('fs') var minimist = require('minimist') var moment = require('moment') var networkAddress = require('network-address') +var parseTorrent = require('parse-torrent') var path = require('path') var prettyBytes = require('pretty-bytes') var WebTorrent = require('../') @@ -51,14 +52,42 @@ var argv = minimist(process.argv.slice(2), { } }) -if (argv.version) { - console.log(require('../package.json').version) - done() +if (process.env.DEBUG || argv.stdout) { + argv.quiet = argv.q = true +} + +var torrentId +var command = argv._[0] + +if (command === 'help' || argv.help) { + help() +} else if (command === 'version' || argv.version) { + version() +} else if (command === 'info') { + torrentId = argv._[1] + info(torrentId) +} else if (command === 'download') { + torrentId = argv._[1] + download(torrentId) +} else if (command) { + // assume download when no command specified + torrentId = command + download(torrentId) +} else { + help() +} + +function errorAndExit (err) { + clivas.line('{red:ERROR:} ' + (err.message || err)) + process.exit(1) } -var torrentId = argv._[0] +function version () { + console.log(require('../package.json').version) + process.exit(0) +} -if (argv.help || !torrentId) { +function help () { fs.readFileSync(path.join(__dirname, 'ascii-logo.txt'), 'utf8') .split('\n') .forEach(function (line) { @@ -67,15 +96,22 @@ if (argv.help || !torrentId) { console.log(function () {/* Usage: - webtorrent + webtorrent [command] + + Example: + webtorrent download "magnet:?xt=urn:btih:..." --vlc + + Available commands: + download download a torrent + info show info for a .torrent file or magnet uri - Download the torrent from: - * magnet uri - * http url to .torrent file - * filesystem path to .torrent file - * info hash (hex string) + Specify torrents as one of the following: + * magnet uri + * http url to .torrent file + * filesystem path to .torrent file + * info hash (hex string) - Streaming options: + Options (streaming): --airplay Apple TV --chromecast Chromecast --mplayer MPlayer @@ -85,7 +121,7 @@ if (argv.help || !torrentId) { --xbmc XBMC --stdout standard out (implies --quiet) - Options: + Options (all): -o, --out [path] set download destination [default: /tmp/webtorrent] -l, --list list files in torrent (with indexes) -i, --index [index] stream a particular file from torrent (by index) @@ -98,334 +134,340 @@ if (argv.help || !torrentId) { Please report bugs! https://github.com/feross/webtorrent/issues */}.toString().split(/\n/).slice(1, -1).join('\n')) - done() + process.exit(0) } -if (process.env.DEBUG || argv.stdout) { - argv.quiet = argv.q = true -} - -var VLC_ARGS = process.env.DEBUG - ? '-q --play-and-exit' - : '--play-and-exit --extraintf=http:logger --verbose=2 --file-logging --logfile=vlc-log.txt' -var MPLAYER_EXEC = 'mplayer -ontop -really-quiet -noidx -loop 0' -var MPV_EXEC = 'mpv --ontop --really-quiet --loop=no' -var OMX_EXEC = 'omxplayer -r -o ' + (typeof argv.omx === 'string') - ? argv.omx - : 'hdmi' - -if (argv.subtitles) { - VLC_ARGS += ' --sub-file=' + argv.subtitles - MPLAYER_EXEC += ' -sub ' + argv.subtitles - MPV_EXEC += ' --sub-file=' + argv.subtitles - OMX_EXEC += ' --subtitles ' + argv.subtitles -} - -function error (err) { - clivas.line('{red:ERROR:} ' + (err.message || err)) -} +function info (torrentId) { + var parsedTorrent = parseTorrent(torrentId) + if (!parsedTorrent || !parsedTorrent.infoHash) { + try { + parsedTorrent = parseTorrent(fs.readFileSync(torrentId)) + } catch (err) { + errorAndExit(err) + } + } + if (!parsedTorrent) errorAndExit('Invalid torrent identifier') -function errorAndExit (err) { - error(err) - process.exit(1) -} + delete parsedTorrent.info + delete parsedTorrent.infoBuffer -var started = Date.now() -function getRuntime () { - return Math.floor((Date.now() - started) / 1000) + console.log(JSON.stringify(parsedTorrent, undefined, 2)) } -var client = new WebTorrent({ - blocklist: argv.blocklist -}) -.on('error', errorAndExit) +function download (torrentId) { + var VLC_ARGS = process.env.DEBUG + ? '-q --play-and-exit' + : '--play-and-exit --extraintf=http:logger --verbose=2 --file-logging --logfile=vlc-log.txt' + var MPLAYER_EXEC = 'mplayer -ontop -really-quiet -noidx -loop 0' + var MPV_EXEC = 'mpv --ontop --really-quiet --loop=no' + var OMX_EXEC = 'omxplayer -r -o ' + (typeof argv.omx === 'string') + ? argv.omx + : 'hdmi' + + if (argv.subtitles) { + VLC_ARGS += ' --sub-file=' + argv.subtitles + MPLAYER_EXEC += ' -sub ' + argv.subtitles + MPV_EXEC += ' --sub-file=' + argv.subtitles + OMX_EXEC += ' --subtitles ' + argv.subtitles + } -if (!argv.out) { // If no output file has been specified - process.on('SIGINT', remove) - process.on('SIGTERM', remove) -} + var started = Date.now() + function getRuntime () { + return Math.floor((Date.now() - started) / 1000) + } -function remove () { - process.removeListener('SIGINT', remove) - process.removeListener('SIGTERM', remove) + var client = new WebTorrent({ + blocklist: argv.blocklist + }) + .on('error', errorAndExit) - // destroying can take a while, so print a message to the user - clivas.line('') - clivas.line('{green:webtorrent is exiting...}') + if (!argv.out) { // If no output file has been specified + process.on('SIGINT', remove) + process.on('SIGTERM', remove) + } - client.destroy(process.exit) -} + function remove () { + process.removeListener('SIGINT', remove) + process.removeListener('SIGTERM', remove) -var torrent = client.add(torrentId, (argv.out ? { tmp: argv.out } : {})) + // destroying can take a while, so print a message to the user + clivas.line('') + clivas.line('{green:webtorrent is exiting...}') -torrent.on('infoHash', function () { - function updateMetadata () { - var numPeers = torrent.swarm.numPeers - clivas.clear() - clivas.line('{green:fetching torrent metadata from} {bold:'+numPeers+'} {green:peers}') + client.destroy(process.exit) } - if (!argv.quiet && !argv.list) { - torrent.swarm.on('wire', updateMetadata) - torrent.on('metadata', function () { - torrent.swarm.removeListener('wire', updateMetadata) - }) - updateMetadata() - } -}) + var torrent = client.add(torrentId, (argv.out ? { tmp: argv.out } : {})) -var filename, swarm, wires, server, serving + torrent.on('infoHash', function () { + function updateMetadata () { + var numPeers = torrent.swarm.numPeers + clivas.clear() + clivas.line('{green:fetching torrent metadata from} {bold:'+numPeers+'} {green:peers}') + } -if (argv.list) torrent.once('ready', onReady) -else { - server = torrent.createServer() - server.listen(argv.port, function () { - if (torrent.ready) onReady() - else torrent.once('ready', onReady) - }).once('connection', function () { - serving = true + if (!argv.quiet && !argv.list) { + torrent.swarm.on('wire', updateMetadata) + torrent.on('metadata', function () { + torrent.swarm.removeListener('wire', updateMetadata) + }) + updateMetadata() + } }) -} - -function done () { - if (!serving) { - process.exit(0) - } -} -function onReady () { - filename = torrent.name - swarm = torrent.swarm - wires = torrent.swarm.wires + var filename, swarm, wires, server, serving - if (argv.list) { - torrent.files.forEach(function (file, i) { - clivas.line('{3+bold:'+i+'} : {magenta:'+file.name+'} {blue:('+prettyBytes(file.length)+')}') + if (argv.list) torrent.once('ready', onReady) + else { + server = torrent.createServer() + server.listen(argv.port, function () { + if (torrent.ready) onReady() + else torrent.once('ready', onReady) + }).once('connection', function () { + serving = true }) - return done() } - torrent.on('verifying', function (data) { - if (argv.quiet) return - clivas.clear() - clivas.line( - '{green:verifying existing torrent} {bold:'+Math.floor(data.percentDone)+'%} ' + - '({bold:'+Math.floor(data.percentVerified)+'%} {green:passed verification})' - ) - }) - - torrent.on('done', function () { - if (!argv.quiet) { - // TODO: expose this data from bittorrent-swarm - var numActiveWires = torrent.swarm.wires.reduce(function (num, wire) { - return num + (wire.downloaded > 0) - }, 0) - clivas.line( - 'torrent downloaded {green:successfully} from ' + - '{bold:'+numActiveWires+'/'+torrent.swarm.wires.length+'} {green:peers} ' + - 'in {bold:'+getRuntime()+'s}!' - ) + function done () { + if (!serving) { + process.exit(0) } - done() - }) + } - var cmd, player - var playerName = argv.airplay ? 'Airplay' - : argv.chromecast ? 'Chromecast' - : argv.xbmc ? 'XBMC' - : argv.vlc ? 'VLC' - : argv.mplayer ? 'MPlayer' - : argv.mpv ? 'mpv' - : argv.omx ? 'OMXPlayer' - : null - - // if no index specified, use largest file - var index = (typeof argv.index === 'number') - ? argv.index - : torrent.files.indexOf(torrent.files.reduce(function (a, b) { - return a.length > b.length ? a : b - })) - var href = 'http://' + networkAddress() + ':' + argv.port + '/' + index - var localHref = 'http://localhost:' + argv.port + '/' + index - - if (playerName) torrent.files[index].select() - if (argv.stdout) torrent.files[index].createReadStream().pipe(process.stdout) - - if (argv.vlc && process.platform === 'win32') { - var registry = require('windows-no-runnable').registry - var key - if (process.arch === 'x64') { - try { - key = registry('HKLM/Software/Wow6432Node/VideoLAN/VLC') - } catch (e) {} - } else { - try { - key = registry('HKLM/Software/VideoLAN/VLC') - } catch (err) {} - } + function onReady () { + filename = torrent.name + swarm = torrent.swarm + wires = torrent.swarm.wires - if (key) { - var vlcPath = key.InstallDir.value + path.sep + 'vlc' - VLC_ARGS = VLC_ARGS.split(' ') - VLC_ARGS.unshift(localHref) - cp.execFile(vlcPath, VLC_ARGS, function (err) { - if (err) return errorAndExit(err) - done() + if (argv.list) { + torrent.files.forEach(function (file, i) { + clivas.line('{3+bold:'+i+'} : {magenta:'+file.name+'} {blue:('+prettyBytes(file.length)+')}') }) + return done() } - } else if (argv.vlc) { - var root = '/Applications/VLC.app/Contents/MacOS/VLC' - var home = (process.env.HOME || '') + root - cmd = 'vlc ' + localHref + ' ' + VLC_ARGS + ' || ' + - root + ' ' + localHref + ' ' + VLC_ARGS + ' || ' + - home + ' ' + localHref + ' ' + VLC_ARGS - } else if (argv.mplayer) { - cmd = MPLAYER_EXEC + ' ' + localHref - } else if (argv.mpv) { - cmd = MPV_EXEC + ' ' + localHref - } else if (argv.omx) { - cmd = OMX_EXEC + ' ' + localHref - } - if (cmd) { - player = cp.exec(cmd, function (err) { - if (err) return errorAndExit(err) + torrent.on('verifying', function (data) { + if (argv.quiet) return + clivas.clear() + clivas.line( + '{green:verifying existing torrent} {bold:'+Math.floor(data.percentDone)+'%} ' + + '({bold:'+Math.floor(data.percentVerified)+'%} {green:passed verification})' + ) + }) + + torrent.on('done', function () { + if (!argv.quiet) { + // TODO: expose this data from bittorrent-swarm + var numActiveWires = torrent.swarm.wires.reduce(function (num, wire) { + return num + (wire.downloaded > 0) + }, 0) + clivas.line( + 'torrent downloaded {green:successfully} from ' + + '{bold:'+numActiveWires+'/'+torrent.swarm.wires.length+'} {green:peers} ' + + 'in {bold:'+getRuntime()+'s}!' + ) + } done() }) - } - if (argv.airplay) { - var airplay = require('airplay-js') - airplay.createBrowser() - .on('deviceOn', function (device) { - device.play(href, 0, function () {}) - }) - .start() - // TODO: handle case where user closes airplay. do same thing as when VLC is closed - } + var cmd, player + var playerName = argv.airplay ? 'Airplay' + : argv.chromecast ? 'Chromecast' + : argv.xbmc ? 'XBMC' + : argv.vlc ? 'VLC' + : argv.mplayer ? 'MPlayer' + : argv.mpv ? 'mpv' + : argv.omx ? 'OMXPlayer' + : null + + // if no index specified, use largest file + var index = (typeof argv.index === 'number') + ? argv.index + : torrent.files.indexOf(torrent.files.reduce(function (a, b) { + return a.length > b.length ? a : b + })) + var href = 'http://' + networkAddress() + ':' + argv.port + '/' + index + var localHref = 'http://localhost:' + argv.port + '/' + index + + if (playerName) torrent.files[index].select() + if (argv.stdout) torrent.files[index].createReadStream().pipe(process.stdout) + + if (argv.vlc && process.platform === 'win32') { + var registry = require('windows-no-runnable').registry + var key + if (process.arch === 'x64') { + try { + key = registry('HKLM/Software/Wow6432Node/VideoLAN/VLC') + } catch (e) {} + } else { + try { + key = registry('HKLM/Software/VideoLAN/VLC') + } catch (err) {} + } - if (argv.chromecast) { - var chromecast = require('chromecast-js') - new chromecast.Browser() - .on('deviceOn', function (device) { - device.connect() - device.on('connected', function () { - device.play(href) + if (key) { + var vlcPath = key.InstallDir.value + path.sep + 'vlc' + VLC_ARGS = VLC_ARGS.split(' ') + VLC_ARGS.unshift(localHref) + cp.execFile(vlcPath, VLC_ARGS, function (err) { + if (err) return errorAndExit(err) + done() }) - }) - } + } + } else if (argv.vlc) { + var root = '/Applications/VLC.app/Contents/MacOS/VLC' + var home = (process.env.HOME || '') + root + cmd = 'vlc ' + localHref + ' ' + VLC_ARGS + ' || ' + + root + ' ' + localHref + ' ' + VLC_ARGS + ' || ' + + home + ' ' + localHref + ' ' + VLC_ARGS + } else if (argv.mplayer) { + cmd = MPLAYER_EXEC + ' ' + localHref + } else if (argv.mpv) { + cmd = MPV_EXEC + ' ' + localHref + } else if (argv.omx) { + cmd = OMX_EXEC + ' ' + localHref + } - if (argv.xbmc) { - var xbmc = require('nodebmc') - new xbmc.Browser() - .on('deviceOn', function (device) { - device.play(href, function () {}) + if (cmd) { + player = cp.exec(cmd, function (err) { + if (err) return errorAndExit(err) + done() }) - } + } - var hotswaps = 0 - torrent.on('hotswap', function () { - hotswaps += 1 - }) + if (argv.airplay) { + var airplay = require('airplay-js') + airplay.createBrowser() + .on('deviceOn', function (device) { + device.play(href, 0, function () {}) + }) + .start() + // TODO: handle case where user closes airplay. do same thing as when VLC is closed + } - if (!argv.quiet) { - process.stdout.write(new Buffer('G1tIG1sySg==', 'base64')) // clear for drawing + if (argv.chromecast) { + var chromecast = require('chromecast-js') + new chromecast.Browser() + .on('deviceOn', function (device) { + device.connect() + device.on('connected', function () { + device.play(href) + }) + }) + } - setInterval(draw, 500) - } + if (argv.xbmc) { + var xbmc = require('nodebmc') + new xbmc.Browser() + .on('deviceOn', function (device) { + device.play(href, function () {}) + }) + } - function active (wire) { - return !wire.peerChoking - } + var hotswaps = 0 + torrent.on('hotswap', function () { + hotswaps += 1 + }) - function draw () { - var unchoked = wires.filter(active) - var linesremaining = clivas.height - var peerslisted = 0 - var speed = swarm.downloadSpeed() - var estimatedSecondsRemaining = Math.max(0, torrent.length - swarm.downloaded) / (speed > 0 ? speed : -1) - var estimate = moment.duration(estimatedSecondsRemaining, 'seconds').humanize() + if (!argv.quiet) { + process.stdout.write(new Buffer('G1tIG1sySg==', 'base64')) // clear for drawing - clivas.clear() + setInterval(draw, 500) + } - if (playerName) - clivas.line('{green:Streaming to} {bold:' + playerName + '}') - if (server) - clivas.line('{green:server running at} {bold:' + href + '}') - if (argv.out) - clivas.line('{green:downloading to} {bold:' + argv.out + '}') + function active (wire) { + return !wire.peerChoking + } - clivas.line('') - clivas.line('{green:downloading:} {bold:' + filename + '}') - clivas.line( - '{green:speed: }{bold:' + prettyBytes(speed) + '/s} ' + - '{green:downloaded:} {bold:' + prettyBytes(swarm.downloaded) + '}' + - '/{bold:' + prettyBytes(torrent.length) + '} ' + - '{green:uploaded:} {bold:' + prettyBytes(swarm.uploaded) + '} ' + - '{green:peers:} {bold:' + unchoked.length + '/' + wires.length + '} ' + - '{green:hotswaps:} {bold:' + hotswaps + '}' - ) - clivas.line( - '{green:time remaining:} {bold:' + estimate + ' remaining} ' + - '{green:total time:} {bold:' + getRuntime() + 's} ' + - '{green:queued peers:} {bold:' + swarm.numQueued + '} ' + - '{green:blocked:} {bold:' + torrent.numBlockedPeers + '}' - ) - clivas.line('{80:}') - linesremaining -= 8 - - var pieces = torrent.storage.pieces - for (var i = 0; i < pieces.length; i++) { - var piece = pieces[i] - if (piece.verified || piece.blocksWritten === 0) { - continue; - } - var bar = '' - for (var j = 0; j < piece.blocks.length; j++) { - bar += piece.blocks[j] ? '{green:█}' : '{red:█}'; + function draw () { + var unchoked = wires.filter(active) + var linesremaining = clivas.height + var peerslisted = 0 + var speed = swarm.downloadSpeed() + var estimatedSecondsRemaining = Math.max(0, torrent.length - swarm.downloaded) / (speed > 0 ? speed : -1) + var estimate = moment.duration(estimatedSecondsRemaining, 'seconds').humanize() + + clivas.clear() + + if (playerName) + clivas.line('{green:Streaming to} {bold:' + playerName + '}') + if (server) + clivas.line('{green:server running at} {bold:' + href + '}') + if (argv.out) + clivas.line('{green:downloading to} {bold:' + argv.out + '}') + + clivas.line('') + clivas.line('{green:downloading:} {bold:' + filename + '}') + clivas.line( + '{green:speed: }{bold:' + prettyBytes(speed) + '/s} ' + + '{green:downloaded:} {bold:' + prettyBytes(swarm.downloaded) + '}' + + '/{bold:' + prettyBytes(torrent.length) + '} ' + + '{green:uploaded:} {bold:' + prettyBytes(swarm.uploaded) + '} ' + + '{green:peers:} {bold:' + unchoked.length + '/' + wires.length + '} ' + + '{green:hotswaps:} {bold:' + hotswaps + '}' + ) + clivas.line( + '{green:time remaining:} {bold:' + estimate + ' remaining} ' + + '{green:total time:} {bold:' + getRuntime() + 's} ' + + '{green:queued peers:} {bold:' + swarm.numQueued + '} ' + + '{green:blocked:} {bold:' + torrent.numBlockedPeers + '}' + ) + clivas.line('{80:}') + linesremaining -= 8 + + var pieces = torrent.storage.pieces + for (var i = 0; i < pieces.length; i++) { + var piece = pieces[i] + if (piece.verified || piece.blocksWritten === 0) { + continue; + } + var bar = '' + for (var j = 0; j < piece.blocks.length; j++) { + bar += piece.blocks[j] ? '{green:█}' : '{red:█}'; + } + clivas.line('{4+cyan:' + i + '} ' + bar); + linesremaining -= 1 } - clivas.line('{4+cyan:' + i + '} ' + bar); + clivas.line('{80:}') linesremaining -= 1 - } - clivas.line('{80:}') - linesremaining -= 1 - - wires.every(function (wire) { - var progress = '?' - if (torrent.parsedTorrent) { - var bits = 0 - var piececount = Math.ceil(torrent.parsedTorrent.length / torrent.parsedTorrent.pieceLength) - for (var i = 0; i < piececount; i++) { - if (wire.peerPieces.get(i)) { - bits++ + + wires.every(function (wire) { + var progress = '?' + if (torrent.parsedTorrent) { + var bits = 0 + var piececount = Math.ceil(torrent.parsedTorrent.length / torrent.parsedTorrent.pieceLength) + for (var i = 0; i < piececount; i++) { + if (wire.peerPieces.get(i)) { + bits++ + } } + progress = bits === piececount ? 'S' : Math.floor(100 * bits / piececount) + '%' } - progress = bits === piececount ? 'S' : Math.floor(100 * bits / piececount) + '%' - } - var tags = [] - if (wire.peerChoking) tags.push('choked') - var reqStats = wire.requests.map(function (req) { - return req.piece; + var tags = [] + if (wire.peerChoking) tags.push('choked') + var reqStats = wire.requests.map(function (req) { + return req.piece; + }) + clivas.line( + '{3:' + progress + '} ' + + '{25+magenta:' + wire.remoteAddress + '} {10:'+prettyBytes(wire.downloaded)+'} ' + + '{10+cyan:' + prettyBytes(wire.downloadSpeed()) + '/s} ' + + '{10+red:' + prettyBytes(wire.uploadSpeed()) + '/s} ' + + '{15+grey:' + tags.join(', ') + '}' + + '{15+cyan:' + reqStats.join(' ') + '}' + ) + peerslisted++ + return linesremaining - peerslisted > 4 }) - clivas.line( - '{3:' + progress + '} ' + - '{25+magenta:' + wire.remoteAddress + '} {10:'+prettyBytes(wire.downloaded)+'} ' + - '{10+cyan:' + prettyBytes(wire.downloadSpeed()) + '/s} ' + - '{10+red:' + prettyBytes(wire.uploadSpeed()) + '/s} ' + - '{15+grey:' + tags.join(', ') + '}' + - '{15+cyan:' + reqStats.join(' ') + '}' - ) - peerslisted++ - return linesremaining - peerslisted > 4 - }) - linesremaining -= peerslisted + linesremaining -= peerslisted + + if (wires.length > peerslisted) { + clivas.line('{80:}') + clivas.line('... and '+(wires.length - peerslisted)+' more') + } - if (wires.length > peerslisted) { clivas.line('{80:}') - clivas.line('... and '+(wires.length - peerslisted)+' more') + clivas.flush(true) } - - clivas.line('{80:}') - clivas.flush(true) } } -- cgit v1.2.3