// TODO: dhtPort and torrentPort should be consistent between restarts // TODO: peerId and nodeId should be consistent between restarts module.exports = WebTorrent var createTorrent = require('create-torrent') var debug = require('debug')('webtorrent') var DHT = require('bittorrent-dht/client') // browser exclude var EventEmitter = require('events').EventEmitter var extend = require('extend.js') var FileReadStream = require('filestream/read') var FSStorage = require('./lib/fs-storage') // browser exclude var hat = require('hat') var inherits = require('inherits') var loadIPSet = require('load-ip-set') // browser exclude var parallel = require('run-parallel') var parseTorrent = require('parse-torrent') var speedometer = require('speedometer') var Storage = require('./lib/storage') var stream = require('stream') var Torrent = require('./lib/torrent') inherits(WebTorrent, EventEmitter) /** * WebTorrent Client * @param {Object} opts */ function WebTorrent (opts) { var self = this if (!(self instanceof WebTorrent)) return new WebTorrent(opts) if (!opts) opts = {} EventEmitter.call(self) if (!debug.enabled) self.setMaxListeners(0) self.torrentPort = opts.torrentPort || 0 self.tracker = (opts.tracker !== undefined) ? opts.tracker : true self.torrents = [] self.downloadSpeed = speedometer() self.uploadSpeed = speedometer() self.storage = typeof opts.storage === 'function' ? opts.storage : (opts.storage !== false && typeof FSStorage === 'function' /* browser exclude */) ? FSStorage : Storage self.peerId = opts.peerId === undefined ? new Buffer('-WW0001-' + hat(48), 'utf8') : typeof opts.peerId === 'string' ? new Buffer(opts.peerId, 'utf8') : opts.peerId self.peerIdHex = self.peerId.toString('hex') self.nodeId = opts.nodeId === undefined ? new Buffer(hat(160), 'hex') : typeof opts.nodeId === 'string' ? new Buffer(opts.nodeId, 'hex') : opts.nodeId self.nodeIdHex = self.nodeId.toString('hex') // TODO: implement webtorrent-dht if (opts.dht !== false && typeof DHT === 'function' /* browser exclude */) { // use a single DHT instance for all torrents, so the routing table can be reused self.dht = new DHT(extend({ nodeId: self.nodeId }, opts.dht)) self.dht.listen(opts.dhtPort) } debug('new webtorrent (peerId %s, nodeId %s)', self.peerIdHex, self.nodeIdHex) if (typeof loadIPSet === 'function') { loadIPSet(opts.blocklist, function (err, ipSet) { self.blocked = ipSet ready() }) } else process.nextTick(ready) function ready () { self.ready = true self.emit('ready') } } /** * Seed ratio for all torrents in the client. * @type {number} */ Object.defineProperty(WebTorrent.prototype, 'ratio', { get: function () { var self = this var uploaded = self.torrents.reduce(function (total, torrent) { return total + torrent.uploaded }, 0) var downloaded = self.torrents.reduce(function (total, torrent) { return total + torrent.downloaded }, 0) || 1 return uploaded / downloaded } }) /** * Returns the torrent with the given `torrentId`. Convenience method. Easier than * searching through the `client.torrents` array. * * @param {string|Buffer|Object} torrentId * @return {Torrent} */ WebTorrent.prototype.get = function (torrentId) { var self = this var parsed = parseTorrent(torrentId) if (!parsed || !parsed.infoHash) return null for (var i = 0, len = self.torrents.length; i < len; i++) { var torrent = self.torrents[i] if (torrent.infoHash === parsed.infoHash) return torrent } return null } /** * Start downloading a new torrent. Aliased as `client.download`. * * `torrentId` can be one of: * - magnet uri (utf8 string) * - torrent file (buffer) * - info hash (hex string or buffer) * - parsed torrent (from [parse-torrent](https://github.com/feross/parse-torrent)) * - http/https url to a torrent file (string) * - filesystem path to a torrent file (string) * * @param {string|Buffer|Object} torrentId * @param {Object} opts torrent-specific options * @param {function=} ontorrent called when the torrent is ready (has metadata) */ WebTorrent.prototype.add = WebTorrent.prototype.download = function (torrentId, opts, ontorrent) { var self = this debug('add %s', torrentId) if (typeof opts === 'function') { ontorrent = opts opts = {} } if (!opts) opts = {} opts.client = self opts.storage = opts.storage || self.storage if (opts.tmp) opts.storageOpts = { tmp: opts.tmp } var torrent = new Torrent(torrentId, extend({ client: self }, opts)) self.torrents.push(torrent) function clientOnTorrent (_torrent) { if (torrent.infoHash === _torrent.infoHash) { ontorrent(torrent) self.removeListener('torrent', clientOnTorrent) } } if (ontorrent) self.on('torrent', clientOnTorrent) torrent.on('error', function (err) { self.emit('error', err, torrent) }) torrent.on('listening', function (port) { self.emit('listening', port, torrent) }) torrent.on('ready', function () { // Emit 'torrent' when a torrent is ready to be used debug('torrent') self.emit('torrent', torrent) }) return torrent } /** * Start seeding a new torrent. * * `input` can be any of the following: * - path to the file or folder on filesystem (string) * - W3C File object (from an `` or drag and drop) * - W3C FileList object (basically an array of `File` objects) * - Array of `File` objects * * @param {string|File|FileList|Blob|Buffer|Array.} input * @param {Object} opts * @param {function} onseed */ WebTorrent.prototype.seed = function (input, opts, onseed) { var self = this if (typeof opts === 'function') { onseed = opts opts = {} } if (!opts) opts = {} // TODO: support `input` as string, or array of strings if (typeof FileList !== 'undefined' && input instanceof FileList) input = Array.prototype.slice.call(input) if (isBlob(input) || Buffer.isBuffer(input)) { input = [ input ] } var streams = input.map(function (item) { if (isBlob(item)) return new FileReadStream(item) else if (Buffer.isBuffer(item)) { var s = new stream.PassThrough() s.end(item) return s } else throw new Error('unsupported input type to `seed`') }) var torrent createTorrent(input, opts, function (err, torrentBuf) { if (err) return self.emit('error', err) self.add(torrentBuf, opts, function (_torrent) { torrent = _torrent torrent.storage.load( streams, function (err) { if (err) return self.emit('error', err) self.emit('seed', torrent) }) }) }) function clientOnSeed (_torrent) { if (torrent.infoHash === _torrent.infoHash) { onseed(torrent) self.removeListener('seed', clientOnSeed) } } if (onseed) self.on('seed', clientOnSeed) } /** * Remove a torrent from the client. * * @param {string|Buffer} torrentId * @param {function} cb */ WebTorrent.prototype.remove = function (torrentId, cb) { var self = this var torrent = self.get(torrentId) if (!torrent) throw new Error('No torrent with id ' + torrentId) debug('remove') self.torrents.splice(self.torrents.indexOf(torrent), 1) torrent.destroy(cb) } /** * Destroy the client, including all torrents and connections to peers. * * @override * @param {function} cb */ WebTorrent.prototype.destroy = function (cb) { var self = this debug('destroy') var tasks = self.torrents.map(function (torrent) { return function (cb) { self.remove(torrent.infoHash, cb) } }) if (self.dht) tasks.push(function (cb) { self.dht.destroy(cb) }) parallel(tasks, cb) } /** * Check if `obj` is a W3C Blob object (which is the superclass of W3C File) * @param {*} obj * @return {boolean} */ function isBlob (obj) { return typeof Blob !== 'undefined' && obj instanceof Blob }