Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ var log4js = require("log4js")

var LOGGER = log4js.getLogger('client.js');

Client()

/**
* Create a new torrent client.
*
* Options:
* { id: '-NT0000-' || Buffer,
* downloadPath: '.',
* portRange: { start: 6881, end: 6889 },
* logLevel: 'TRACE' || 'DEBUG' || 'INFO' || ... }
* @param { Object } options
* @param { '-NT0000-' | Buffer} options.id
* @param { String } options.downloadPath Defaults to '.'
* @param { Object } options.portRange
* @param { number } options.portRange.start Defaults to 6881
* @param { number } options.portRange.end Defaults to 6889
* @param { 'TRACE' | 'DEBUG' | 'INFO'} options.logLevel Not used
*/
var Client = function(options) {

Expand All @@ -33,9 +36,18 @@ var Client = function(options) {
this.id = padId(id);
}

/**
* @type { Object.<String, Torrent> }
*/
this.torrents = {};
/**
* @type { String }
*/
this.downloadPath = options.downloadPath || '.';
this._server = net.createServer(this._handleConnection.bind(this));
/**
* @type { number }
*/
this.port = listen(this._server, options.portRange);

this._extensions = [
Expand All @@ -45,10 +57,18 @@ var Client = function(options) {
dht.init();
};

/**
* @param { function(Torrent) } ExtensionClass
*/
Client.prototype.addExtension = function(ExtensionClass) {
this._extensions.push(ExtensionClass);
};

/**
*
* @param { String } url Torrent file path or Magnet URL / link
* @returns { Torrent } The added torrent
*/
Client.prototype.addTorrent = function(url) {
var torrent = new Torrent(this.id, this.port, this.downloadPath, url, this._extensions.slice(0));
var client = this;
Expand All @@ -63,6 +83,10 @@ Client.prototype.addTorrent = function(url) {
return torrent;
};

/**
*
* @param { Torrent } torrent
*/
Client.prototype.removeTorrent = function(torrent) {
if (this.torrents[torrent.infoHash]) {
this.torrents[torrent.infoHash].stop();
Expand Down
30 changes: 30 additions & 0 deletions lib/torrent/torrent.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@

const Tracker = require('../tracker/tracker');
var bencode = require('../util/bencode'),
util = require('util');

Expand All @@ -19,6 +20,13 @@ var BitField = require('../util/bitfield'),

var LOGGER = require('log4js').getLogger('torrent.js');

/**
* @param { String } clientId
* @param { number } clientPort
* @param { String } downloadPath
* @param { String } dataUrl
* @param { Array<function(Torrent)> } extensions
*/
function Torrent(clientId, clientPort, downloadPath, dataUrl, extensions) {
EventEmitter.call(this);

Expand All @@ -28,16 +36,29 @@ function Torrent(clientId, clientPort, downloadPath, dataUrl, extensions) {
this.infoHash = null;
this.name = null;

/**
* @type { {downloaded: number, downloadRate: number, uploaded: number, uploadRate: number} }
* @readOnly
*/
this.stats = {
downloaded: 0,
downloadRate: 0,
uploaded: 0,
uploadRate: 0
};

/**
* @type { Object.<string, Peer> }
*/
this.peers = {};
/**
* @type { Array<Tracker> }
*/
this.trackers = [];
this.bitfield = null;
/**
* @type { Torrent.COMPLETE | Torrent.ERROR | Torrent.INFO_HASH | Torrent.LOADING | Torrent.PEER | Torrent.PROGRESS | Torrent.READY }
*/
this.status = null;

this._setStatus(Torrent.LOADING);
Expand Down Expand Up @@ -168,16 +189,25 @@ Torrent.prototype.addPeer = function(/* peer | id, address, port */) {
}
};

/**
* @param { Tracker } tracker
*/
Torrent.prototype.addTracker = function(tracker) {
this.trackers.push(tracker);
tracker.setTorrent(this);
// tracker.on(Tracker.PEER, this.addPeer.bind(this));
};

/**
* @returns { boolean }
*/
Torrent.prototype.hasMetadata = function() {
return this._metadata.isComplete();
};

/**
* @returns { boolean }
*/
Torrent.prototype.isComplete = function() {
return this.bitfield.cardinality() === this.bitfield.length;
};
Expand Down
184 changes: 99 additions & 85 deletions lib/tracker/tracker.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@

var bencode = require('../util/bencode'),
protocol = require('./protocol'),
util = require('util');
protocol = require('./protocol'),
util = require('util'),
Torrent = require('../torrent');

var EventEmitter = require('events').EventEmitter;

Expand All @@ -15,111 +16,124 @@ var WAITING = 'waiting';
var ANNOUNCE_START_INTERVAL = 5;

var Tracker = function(urls) {
EventEmitter.call(this);
if (!Array.isArray(urls)) {
this._urls = [urls];
} else {
this._urls = urls;
}
// TODO: need to step through URLs as part of announce process
this.url = require('url').parse(this._urls[0]);
this.torrent = null;
this.state = STOPPED;
this.seeders = 0;
this.leechers = 0;
EventEmitter.call(this);
if (!Array.isArray(urls)) {
this._urls = [urls];
} else {
this._urls = urls;
}
// TODO: need to step through URLs as part of announce process
this.url = require('url').parse(this._urls[0]);
/**
* @type { Torrent }
*/
this.torrent = null;
this.state = STOPPED;
this.seeders = 0;
this.leechers = 0;
};
util.inherits(Tracker, EventEmitter);

/**
*
* @param { Torrent } torrent
*/
Tracker.prototype.setTorrent = function(torrent) {
this.torrent = torrent;
this.torrent = torrent;
};

new Tracker().start

/**
*
* @param { function(String , String, number) } callback Parameters : peer_id: String, peer_ip: String, peer_port: number
*/
Tracker.prototype.start = function(callback) {
this.callback = callback;
this._announce('started');
this.callback = callback;
this._announce('started');
};

Tracker.prototype.stop = function() {
this._announce('stopped');
this._announce('stopped');
};

Tracker.prototype._announce = function(event) {

LOGGER.debug('Announce' + (event ? ' ' + event : ''));

var handlerClass = protocol[this.url.protocol],
tracker = this;

if (handlerClass) {
var handler = new handlerClass();
var data = {
peer_id: this.torrent.clientId,
info_hash: this.torrent.infoHash,
port: this.torrent.clientPort
};
this.state = CONNECTING;
handler.handle(this, data, event, function(info, error) {
if (error) {
LOGGER.warn('announce error from ' + tracker.url.href + ': ' + error.message);
tracker.state = ERROR;
tracker.errorMessage = error.message;
if (event === 'started') {
LOGGER.warn('retry announce \'started\' in ' + ANNOUNCE_START_INTERVAL + 's');
setTimeout(function() {
tracker._announce('started');
}, ANNOUNCE_START_INTERVAL * 1000);
}
} else {
if (info.trackerId) {
tracker.trackerId = info.trackerId;
}
tracker.state = WAITING;
if (event === 'started') {
var interval = info.interval;
if (tracker.timeoutId) {
clearInterval(tracker.timeoutId);
}
if (interval) {
tracker.timeoutId = setInterval(function() {
tracker._announce(null);
}, interval * 1000);
}
} else if (event === 'stopped') {
clearInterval(tracker.timeoutId);
delete tracker.timeoutId;
tracker.state = STOPPED;
}

LOGGER.debug('Announce' + (event ? ' ' + event : ''));

var handlerClass = protocol[this.url.protocol],
tracker = this;

if (handlerClass) {
var handler = new handlerClass();
var data = {
peer_id: this.torrent.clientId,
info_hash: this.torrent.infoHash,
port: this.torrent.clientPort
};
this.state = CONNECTING;
handler.handle(this, data, event, function(info, error) {
if (error) {
LOGGER.warn('announce error from ' + tracker.url.href + ': ' + error.message);
tracker.state = ERROR;
tracker.errorMessage = error.message;
if (event === 'started') {
LOGGER.warn('retry announce \'started\' in ' + ANNOUNCE_START_INTERVAL + 's');
setTimeout(function() {
tracker._announce('started');
}, ANNOUNCE_START_INTERVAL * 1000);
}
} else {
if (info.trackerId) {
tracker.trackerId = info.trackerId;
}
tracker.state = WAITING;
if (event === 'started') {
var interval = info.interval;
if (tracker.timeoutId) {
clearInterval(tracker.timeoutId);
}
tracker._updateInfo(info);
});
if (interval) {
tracker.timeoutId = setInterval(function() {
tracker._announce(null);
}, interval * 1000);
}
} else if (event === 'stopped') {
clearInterval(tracker.timeoutId);
delete tracker.timeoutId;
tracker.state = STOPPED;
}
}
tracker._updateInfo(info);
});
}
};

Tracker.prototype._updateInfo = function(data) {
LOGGER.debug('Updating details from tracker. ' + (data && data.peers ? data.peers.length : 0) + ' new peers');
if (data) {
this.seeders = data.seeders || 0;
this.leechers = data.leechers || 0;
if (data.peers) {
for (var i = 0; i < data.peers.length; i++) {
var peer = data.peers[i];
this.callback(peer.peer_id, peer.ip, peer.port);
}
}
this.emit('updated');
LOGGER.debug('Updating details from tracker. ' + (data && data.peers ? data.peers.length : 0) + ' new peers');
if (data) {
this.seeders = data.seeders || 0;
this.leechers = data.leechers || 0;
if (data.peers) {
for (var i = 0; i < data.peers.length; i++) {
var peer = data.peers[i];
this.callback(peer.peer_id, peer.ip, peer.port);
}
}
this.emit('updated');
}
};

Tracker.createTrackers = function(announce, announceList) {
var trackers = [];
if (announceList) {
announceList.forEach(function(announce) {
trackers.push(new Tracker(announce));
});
} else {
trackers.push(new Tracker(announce));
}
return trackers;
var trackers = [];
if (announceList) {
announceList.forEach(function(announce) {
trackers.push(new Tracker(announce));
});
} else {
trackers.push(new Tracker(announce));
}
return trackers;
};

module.exports = Tracker;