UNPKG

@electorrent/node-rtorrent

Version:

rtorrent api implementation for electorrent

511 lines (429 loc) 16.4 kB
var url = require("url") var fs = require('fs'); var xmlrpc = require("@electorrent/xmlrpc"); const URL_REGEX = /^[a-z]+:\/\/(?:[a-z0-9-]+\.)*((?:[a-z0-9-]+\.)[a-z]+)/ function Rtorrent(option) { this.mode = (option && option['mode']) || "xmlrpc"; this.host = (option && option['host']) || "127.0.0.1"; this.port = (option && option['port']) || 80; this.path = (option && option['path']) || "/RPC2"; this.user = (option && option['user']) || null; this.pass = (option && option['pass']) || null; this.ssl = (option && option['ssl']) || false; this.ca = (option && option['ca']) || undefined; this.timeout = (option && option['timeout']) || 5000; this.client = null; if (this.mode == 'xmlrpc') { options = { host: this.host, port: this.port, path: this.path, headers: { 'User-Agent': 'NodeJS XML-RPC Client', 'Content-Type': 'text/xml', 'Accept': 'text/xml', 'Accept-Charset': 'UTF8', 'Connection': 'Close' }, ca: this.ca, timeout: this.timeout, } if (this.user && this.pass) { options.username = this.user options.password = this.pass } this.client = (this.ssl) ? xmlrpc.createSecureClient(options) : xmlrpc.createClient(options); } else { throw new Error('unknown mode: '+this.mode+' (available: xmlrpc)'); } }; Rtorrent.prototype.get = function(method, param, callback) { return this.getXmlrpc(method, param, callback); }; Rtorrent.prototype.getXmlrpc = function(method, params, callback) { this.client.methodCall(method, params, callback); }; Rtorrent.prototype.execute = function(cmd, callback) { return this.get('execute.capture', ['bash', '-c', cmd], callback); }; Rtorrent.prototype.getMulticall = function(method, param, cmds, callback) { var self = this; var cmdarray = param; for (var c in cmds) cmdarray.push(postfix(cmds[c])); self.get(method, cmdarray, function (err, data) { if (err) return callback(err); var res = doublearray2hash(data, Object.keys(cmds)); callback(err, res); }); }; Rtorrent.prototype.getMulticallHashes = function(hashes, cmds, params, callback) { var array = []; for (var h in hashes) { for (var c in cmds) { var param = params[c] param = param === undefined ? [] : [param] array.push({ 'methodName': cmds[c], 'params': [hashes[h], ...param], }); } } this.getXmlrpc('system.multicall', [array], callback); } Rtorrent.prototype.getAll = function(callback) { var self = this; self.getGlobals(function (err, globals) { if (err) return callback(err); self.getTorrents(function (err, torrents) { if (err) return callback(err); var array = []; for (var t in torrents) { var params = []; params.push(torrents[t].hash); params.push(''); for (var f in fields.files) params.push(fields.files[f]+'='); array.push({'methodName': 'f.multicall', params: params}) } for (var t in torrents) { var params = []; params.push(torrents[t].hash); params.push(''); for (var f in fields.trackers) params.push(fields.trackers[f]+'='); array.push({'methodName': 't.multicall', params: params}) } for (var t in torrents) { var params = []; params.push(torrents[t].hash); params.push(''); for (var f in fields.peers) params.push(fields.peers[f]+'='); array.push({'methodName': 'p.multicall', params: params}) } self.getXmlrpc('system.multicall', [array], function (err, data) { var nb = torrents.length; for (var i = 0; i < nb; i++) { torrents[i]['files'] = doublearray2hash(data[i][0], Object.keys(fields.files)); torrents[i]['trackers'] = doublearray2hash(data[i+nb][0], Object.keys(fields.trackers)); torrents[i]['peers'] = doublearray2hash(data[i+nb+nb][0], Object.keys(fields.peers)); } for (var t in torrents) globals.free_disk_space = torrents[t].free_disk_space; globals.torrents = torrents; callback(err, globals) }); }); }); }; Rtorrent.prototype.getTorrentsExtra = function(callback) { var self = this; this.getTorrents(function (err, torrents) { if (err) return callback(err); var array = []; for (var t in torrents) { var params = []; params.push(torrents[t].hash); params.push(''); for (var f in fields.trackers) params.push(fields.trackers[f]+'='); array.push({'methodName': 't.multicall', params: params}) } self.getXmlrpc('system.multicall', [array], function (err, data) { var nb = torrents.length; for (var i = 0; i < nb; i++) { var trackerdata = doublearray2hash(data[i][0], Object.keys(fields.trackers)); for (var t in trackerdata) { stringsToBooleans(trackerdata[t], ['enabled', 'open']) stringsToNumbers(trackerdata[t]) } torrents[i]['trackerdata'] = trackerdata torrents[i]['trackers'] = trackerdata.map(t => t.url) torrents[i]['tracker'] = trackerdata[0] && urlHostname(trackerdata[0]['url']) torrents[i]['leechers_total'] = trackerdata.reduce((s,t) => s+t.scrape_incomplete, 0) torrents[i]['seeders_total'] = trackerdata.reduce((s,t) => s+t.scrape_complete, 0) } var labels = torrents.reduce((s,t) => s.add(t.label), new Set()) var trackers = torrents.reduce((s,t) => s.add(t.tracker), new Set()) callback(err, { torrents: torrents, labels: Array.from(labels).filter(l => !!l), trackers: Array.from(trackers).filter(t => !!t), }) }); }); } Rtorrent.prototype.getTorrents = function(callback) { var self = this; self.getMulticall('d.multicall2', ['', 'main'], fields.torrents, function (err, data) { if (err) return callback(err); var bools = ['active', 'open', 'complete', 'hashing', 'hashed'] for (var i in data) { stringsToBooleans(data[i], bools) stringsToNumbers(data[i]) data[i]['label'] = decodeURIComponent(data[i]['label'] || '') if (data[i]['down_total'] < data[i]['completed']) data[i]['down_total'] = data[i]['completed']; data[i]['ratio'] = data[i]['up_total']/data[i]['down_total']; } callback(err, data) }); }; Rtorrent.prototype.getTorrentTrackers = function(hash, callback) { this.getMulticall('t.multicall', [hash, ''], fields.trackers, callback); }; Rtorrent.prototype.getTorrentFiles = function(hash, callback) { this.getMulticall('f.multicall', [hash, ''], fields.files, callback); }; Rtorrent.prototype.getTorrentPeers = function(hash, callback) { this.getMulticall('p.multicall', [hash, ''], fields.peers, callback); }; Rtorrent.prototype.systemMulticall = function(cmds, callback) { var array = []; for (i in cmds) array.push({ 'methodName': cmds[i], 'params': [], }); this.getXmlrpc('system.multicall', [array], function (err, data) { if (err) return callback(err); var res = {}; var i = 0; for (var key in cmds) res[key] = data[i++][0]; callback(err, res); }); }; Rtorrent.prototype.getGlobals = function(callback) { this.systemMulticall(fields.global, callback); }; Rtorrent.prototype.start = function(hashes, callback) { var self = this; this.getMulticallHashes(hashes, ['d.open'], [], function(err, data) { if(err) return callback(err); self.getMulticallHashes(hashes, ['d.start'], [], callback) }) } Rtorrent.prototype.pause = function(hashes, callback) { this.getMulticallHashes(hashes, ['d.pause'], [], callback) } Rtorrent.prototype.stop = function(hashes, callback) { var self = this; this.getMulticallHashes(hashes, ['d.stop'], [], function(err, data) { if(err) return callback(err); self.getMulticallHashes(hashes, ['d.close'], [], callback) }) } Rtorrent.prototype.remove = function(hashes, callback) { this.getMulticallHashes(hashes, ['d.erase'], [], callback) }; Rtorrent.prototype.removeAndErase = function(hashes, callback) { this.getMulticallHashes(hashes, ['d.custom5.set', 'd.delete_tied', 'd.erase'], ['1'], callback) } Rtorrent.prototype.setLabel = function(hashes, label, callback) { this.getMulticallHashes(hashes, ['d.custom1.set'], [label], callback) } Rtorrent.prototype.setPriorityHigh = function(hashes, callback) { this.getMulticallHashes(hashes, ['d.priority.set'], [3], callback) } Rtorrent.prototype.setPriorityNormal = function(hashes, callback) { this.getMulticallHashes(hashes, ['d.priority.set'], [2], callback) } Rtorrent.prototype.setPriorityLow = function(hashes, callback) { this.getMulticallHashes(hashes, ['d.priority.set'], [1], callback) } Rtorrent.prototype.setPriorityOff = function(hashes, callback) { this.getMulticallHashes(hashes, ['d.priority.set'], [0], callback) } Rtorrent.prototype.recheck = function(hashes, callback) { this.getMulticallHashes(hashes, ['d.check_hash'], [], callback) } Rtorrent.prototype.loadLink = function(link, callback) { this.get('load.start', ['', link], callback); }; Rtorrent.prototype.loadFile = function(filePath, callback) { var file = fs.readFileSync(filePath); this.loadFileContent(file, callback); }; Rtorrent.prototype.loadFileContent = function(filecontent, callback) { if (!Buffer.isBuffer(filecontent)) { filecontent = Buffer.from(filecontent) } this.get('load.raw_start', ['', filecontent], callback); }; Rtorrent.prototype.setPath = function(hash, directory, callback) { this.get('d.directory.set', [hash, directory], callback); }; module.exports = Rtorrent; var fields = { global: { up_rate: 'throttle.global_up.rate', down_rate: 'throttle.global_down.rate', up_total: 'throttle.global_up.total', down_total: 'throttle.global_down.total', bind: 'network.bind_address', check_hash: 'pieces.hash.on_completion', dht_port: 'dht.port', directory: 'directory.default', download_rate: 'throttle.global_down.max_rate', http_cacert: 'network.http.cacert', http_capath: 'network.http.capath', http_proxy: 'network.http.proxy_address', ip: 'network.local_address', max_downloads_div: 'throttle.max_downloads.div', max_downloads_global: 'throttle.max_downloads.global', max_file_size: 'system.file.max_size', max_memory_usage: 'pieces.memory.max', max_open_files: 'network.max_open_files', max_open_http: 'network.http.max_open', max_peers: 'throttle.max_peers.normal', max_peers_seed: 'throttle.max_peers.seed', max_uploads: 'throttle.max_uploads', max_uploads_global: 'throttle.max_uploads.global', min_peers_seed: 'throttle.min_peers.seed', min_peers: 'throttle.min_peers.normal', peer_exchange: 'protocol.pex', port_open: 'network.port_open', upload_rate: 'throttle.global_up.max_rate', port_random: 'network.port_random', port_range: 'network.port_range', preload_min_size: 'pieces.preload.min_size', preload_required_rate: 'pieces.preload.min_rate', preload_type: 'pieces.preload.type', proxy_address: 'network.proxy_address', receive_buffer_size: 'network.receive_buffer.size', safe_sync: 'pieces.sync.always_safe', scgi_dont_route: 'network.scgi.dont_route', send_buffer_size: 'network.send_buffer.size', session: 'session.path', session_lock: 'session.use_lock', session_on_completion: 'session.on_completion', split_file_size: 'system.file.split_size', split_suffix: 'system.file.split_suffix', timeout_safe_sync: 'pieces.sync.timeout_safe', timeout_sync: 'pieces.sync.timeout', tracker_numwant: 'trackers.numwant', use_udp_trackers: 'trackers.use_udp', max_uploads_div: 'throttle.max_uploads.div', max_open_sockets: 'network.max_open_sockets' }, peers: { address: 'p.address', client_version: 'p.client_version', completed_percent: 'p.completed_percent', down_rate: 'p.down_rate', down_total: 'p.down_total', id: 'p.id', port: 'p.port', up_rate: 'p.up_rate', up_total: 'p.up_total' }, files: { range_first: 'f.range_first', range_second: 'f.range_second', size: 'f.size_bytes', chunks: 'f.size_chunks', completed_chunks: 'f.completed_chunks', fullpath: 'f.frozen_path', path: 'f.path', priority: 'f.priority', is_created: 'f.is_created=', is_open: 'f.is_open=', last_touched: 'f.last_touched=', match_depth_next: 'f.match_depth_next=', match_depth_prev: 'f.match_depth_prev=', offset: 'f.offset=', path_components: 'f.path_components=', path_depth: 'f.path_depth=', }, trackers: { id: 't.id', group: 't.group', type: 't.type', url: 't.url', enabled: 't.is_enabled', open: 't.is_open', min_interval: 't.min_interval', normal_interval: 't.normal_interval', scrape_complete: 't.scrape_complete', scrape_downloaded: 't.scrape_downloaded', scrape_incomplete: 't.scrape_incomplete', scrape_time_last: 't.scrape_time_last', }, torrents: { hash: 'd.hash', torrent: 'd.tied_to_file', torrentsession: 'd.loaded_file', path: 'd.base_path', name: 'd.name', size: 'd.size_bytes', skip: 'd.skip.total', completed: 'd.completed_bytes', down_rate: 'd.down.rate', down_total: 'd.down.total', up_rate: 'd.up.rate', up_total: 'd.up.total', message: 'd.message', bitfield: 'd.bitfield', chunk_size: 'd.chunk_size', chunk_completed: 'd.completed_chunks', createdAt: 'd.creation_date', active: 'd.is_active', open: 'd.is_open', complete: 'd.complete', hashing: 'd.is_hash_checking', hashed: 'd.is_hash_checked', leechers: 'd.peers_accounted', seeders: 'd.peers_complete', free_disk_space: 'd.free_diskspace', left_bytes: 'd.left_bytes', label: 'd.custom1', addtime: 'd.custom=addtime', }, }; function postfix(param) { if (param.includes('=')) { return param } else { return param+'=' } } function urlHostname(url) { var match = url.match(URL_REGEX) return match && match[1] } function stringsToNumbers(object) { let keys = Object.keys(object) for (let key of keys) { if (key === 'hash' || key === 'name') continue let number = parseFloat(object[key]) if (!isNaN(number)) object[key] = number } } function stringsToBooleans(object, keys) { for (var key of keys) { object[key] = !!parseInt(object[key]) } } function array2hash(array, keys) { var i = 0; var res = {}; for (var k in keys) { res[keys[k]] = array[i++]; } return res; } function doublearray2hash(array, keys) { for (var i in array) array[i] = array2hash(array[i], keys); return array; }