module.exports = Client var debug = require('debug')('bittorrent-tracker') var EventEmitter = require('events').EventEmitter var inherits = require('inherits') var once = require('once') var url = require('url') var common = require('./lib/common') var HTTPTracker = require('./lib/http-tracker') var UDPTracker = require('./lib/udp-tracker') var WebSocketTracker = require('./lib/websocket-tracker') inherits(Client, EventEmitter) /** * A Client manages tracker connections for a torrent. * * @param {string} peerId peer id * @param {Number} port torrent client listening port * @param {Object} torrent parsed torrent * @param {Object} opts options object * @param {Number} opts.numWant number of peers to request * @param {Number} opts.interval announce interval (in ms) * @param {Number} opts.rtcConfig RTCPeerConnection configuration object */ function Client (peerId, port, torrent, opts) { var self = this if (!(self instanceof Client)) return new Client(peerId, port, torrent, opts) EventEmitter.call(self) if (!opts) opts = {} // required self._peerId = Buffer.isBuffer(peerId) ? peerId : new Buffer(peerId, 'hex') self._port = port self._infoHash = Buffer.isBuffer(torrent.infoHash) ? torrent.infoHash : new Buffer(torrent.infoHash, 'hex') self.torrentLength = torrent.length // optional self._numWant = opts.numWant || common.DEFAULT_ANNOUNCE_PEERS self._intervalMs = opts.interval || common.DEFAULT_ANNOUNCE_INTERVAL debug('new client %s', self._infoHash.toString('hex')) if (typeof torrent.announce === 'string') torrent.announce = [ torrent.announce ] self._trackers = (torrent.announce || []) .filter(function (announceUrl) { var protocol = url.parse(announceUrl).protocol return [ 'udp:', 'http:', 'https:', 'ws:', 'wss:' ].indexOf(protocol) !== -1 }) .map(function (announceUrl) { var trackerOpts = { interval: self._intervalMs } var protocol = url.parse(announceUrl).protocol if (protocol === 'http:' || protocol === 'https:') { return new HTTPTracker(self, announceUrl, trackerOpts) } else if (protocol === 'udp:') { return new UDPTracker(self, announceUrl, trackerOpts) } else if (protocol === 'ws:' || protocol === 'wss:') { return new WebSocketTracker(self, announceUrl, trackerOpts) } }) } /** * Simple convenience function to scrape a tracker for an infoHash without * needing to create a Client, pass it a parsed torrent, etc. * @param {string} announceUrl * @param {string} infoHash * @param {function} cb */ Client.scrape = function (announceUrl, infoHash, cb) { cb = once(cb) var peerId = new Buffer('01234567890123456789') // dummy value var port = 6881 // dummy value var torrent = { infoHash: infoHash, announce: [ announceUrl ] } var client = new Client(peerId, port, torrent) client.once('error', cb) client.once('scrape', function (data) { cb(null, data) }) client.scrape() } /** * Send a `start` announce to the trackers. * @param {Object} opts * @param {number=} opts.uploaded * @param {number=} opts.downloaded * @param {number=} opts.left (if not set, calculated automatically) */ Client.prototype.start = function (opts) { var self = this debug('send `start`') opts = self._defaultAnnounceOpts(opts) opts.event = 'started' self._announce(opts) // start announcing on intervals self._trackers.forEach(function (tracker) { tracker.setInterval(self._intervalMs) }) } /** * Send a `stop` announce to the trackers. * @param {Object} opts * @param {number=} opts.uploaded * @param {number=} opts.downloaded * @param {number=} opts.left (if not set, calculated automatically) */ Client.prototype.stop = function (opts) { var self = this debug('send `stop`') opts = self._defaultAnnounceOpts(opts) opts.event = 'stopped' self._announce(opts) self.destroy() } /** * Send a `complete` announce to the trackers. * @param {Object} opts * @param {number=} opts.uploaded * @param {number=} opts.downloaded * @param {number=} opts.left (if not set, calculated automatically) */ Client.prototype.complete = function (opts) { var self = this debug('send `complete`') if (!opts) opts = {} if (opts.downloaded == null && self.torrentLength != null) { opts.downloaded = self.torrentLength } opts = self._defaultAnnounceOpts(opts) opts.event = 'completed' self._announce(opts) } /** * Send a `update` announce to the trackers. * @param {Object} opts * @param {number=} opts.uploaded * @param {number=} opts.downloaded * @param {number=} opts.left (if not set, calculated automatically) */ Client.prototype.update = function (opts) { var self = this debug('send `update`') opts = self._defaultAnnounceOpts(opts) if (opts.event) delete opts.event self._announce(opts) } Client.prototype._announce = function (opts) { var self = this self._trackers.forEach(function (tracker) { tracker.announce(opts) }) } /** * Send a scrape request to the trackers. * @param {Object} opts * @param {number=} opts.uploaded * @param {number=} opts.downloaded * @param {number=} opts.left (if not set, calculated automatically) */ Client.prototype.scrape = function (opts) { var self = this debug('send `scrape`') if (!opts) opts = {} self._trackers.forEach(function (tracker) { tracker.scrape(opts) }) } Client.prototype.setInterval = function (intervalMs) { var self = this debug('setInterval') self._intervalMs = intervalMs self._trackers.forEach(function (tracker) { tracker.setInterval(intervalMs) }) } Client.prototype.destroy = function () { var self = this debug('destroy') self._trackers.forEach(function (tracker) { if (tracker.destroy) tracker.destroy() tracker.setInterval(0) // stop announcing on intervals }) } Client.prototype._defaultAnnounceOpts = function (opts) { var self = this if (!opts) opts = {} if (opts.numWant == null) opts.numWant = self._numWant if (opts.uploaded == null) opts.uploaded = 0 if (opts.downloaded == null) opts.downloaded = 0 if (opts.left == null && self.torrentLength != null) { opts.left = self.torrentLength - opts.downloaded } return opts }