UNPKG

electorrent

Version:

An Electron/Node/AngularJS remote client app for uTorrent server

399 lines (343 loc) 13.4 kB
'use strict'; angular.module('torrentApp').service('rtorrentService', ["$http", "$q", "xmlrpc", "TorrentR", "rtorrentConfig", "notificationService", function($http, $q, $xmlrpc, TorrentR, rtorrentConfig, $notify) { const URL_REGEX = /^(https?)\:\/\/((?:(?:[^:\/?#]+)+\.)?([^\.:\/?#]+\.([a-z]+)))(?:\:([0-9]+))?([\/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/ /* * Please rename all occurences of __serviceName__ (including underscores) with the name of your service. * Best practise is naming your service starting with the client name in lower case followed by 'Service' * (remember capital 'S' e.g qbittorrentService for qBittorrent, utorrentService for µTorrent ect.). * The real name of your client for display purposes can be changes in the field 'this.name' below. */ this.name = 'rTorrent'; /* * Good practise is keeping a configuration object for your communication with the API */ const config = { version: undefined, ip: '', port: '' } const fields = rtorrentConfig.fields.map(fieldTransform); const custom = rtorrentConfig.custom.map(customTransform); const trackerfields = rtorrentConfig.trackers.map(trackerTransform); function url() { var ip, port, path; if (arguments.length === 1){ ip = config.ip; port = config.port; path = arguments[0] || ''; } else { ip = arguments[0] port = arguments[1] path = arguments[2] || ''; } return `http://${ip}:${port}${path}`; } function saveConnection(ip, port) { config.ip = ip; config.port = port; } function fieldTransform(field) { return 'd.' + field + '='; } function trackerTransform(field) { return 't.' + field + '=' } function customTransform(custom) { return 'd.get_custom=' + custom; } /** * Connect to the server upon initial startup, changing connection settings ect. The function * should return a promise that the connection was successfull. A standard http timeout of 5 seconds * must be implemented. When successfull the service should save login details for later use. * @param {string} ip * @param {integer} port * @param {string} user * @param {string} password * @return {promise} connection */ this.connect = function(ip, port, user, pass) { $xmlrpc.config({ hostName: url(ip, port), pathName: "/RPC2" }) var encoded = new Buffer(`${user}:${pass}`).toString('base64'); $http.defaults.headers.common.Authorization = 'Basic ' + encoded; return $xmlrpc.callMethod('system.client_version') .then(function(data) { config.version = data; return $q.resolve('Sucessfully connected to rTorrent'); }).catch(function(err) { console.error("Could not connect to rTorrent", err); return $q.reject(err); }) } /** * Return any new information about torrents to be rendered in the GUI. Should return a * promise with the required information to be updated. Will be executed by controllers * very frequently. You can find a template of the data to be returned in the function. * Here you will need: * labels {array}: array of string of each label * all {array}: array of objects inherited from 'AbstractTorrent' that are not currently known. * This means they have just been added or never seen before since the last startup. * changed {array}: array of objects inherited from 'AbstractTorrent' that have allready been seend before. * This means they may contain partial information in which case they ar merged with any present infomation. * deleted {array}: array of string containg the hashes of which torrents to be removed from the list in the GUI. * @return {promise} data */ this.torrents = function() { let torrents = null return $xmlrpc.callMethod('d.multicall', ['main', ...fields, ...custom]) .then(function(data) { torrents = processData(data) return $q.resolve(torrents); }).then(function(torrents) { return getTrackers(torrents.all) }).then(function(trackers) { torrents.trackers = trackers return $q.resolve(torrents) }).catch(function(err) { console.error("Torrent error", err); return $q.reject(err); }) } function processData(data) { var torrents = { dirty: true, labels: [], all: [], changed: [], deleted: [] }; torrents.all = data.map(build); torrents.labels = torrents.all.reduce(fetchLabels, []).map(decodeURIComponent) return torrents } function getTrackers(torrents) { if (!torrents.length) return let calls = [] torrents.forEach((torrent) => { calls.push({'methodName': 't.multicall', 'params': [torrent.hash, '', ...trackerfields]}) }) return $xmlrpc.callMethod('system.multicall', [calls]) .then(function(data) { let trackers = processTrackerData(torrents, data) return $q.resolve(trackers) }) } function processTrackerData(torrents, data) { let trackers = new Set() let url = rtorrentConfig.trackers.indexOf('get_url') torrents.forEach((torrent, index) => { let tracker = data[index][0][0][url] torrent.trackers = [tracker] trackers.add(tracker) }) return Array.from(trackers).map((tracker) => { return parseUrl(tracker).hostname }) } function parseUrl(url) { var match = url.match(URL_REGEX) return match && { protocol: match[1], domain: match[2], hostname: match[3], extension: match[4], port: match[5], path: match[6], params: match[7], hash: match[8] } } function build(array) { return new TorrentR(array); } function fetchLabels(prev, current) { if (current.label) prev.push(current.label) return prev } /** * Add a torrent to the client by sending a magnet link to the API. Should return * a promise that the torrent has been added successfully to the client. * @param {string} magnetURL * @return {promise} isAdded */ this.addTorrentUrl = function(magnet) { $xmlrpc.callMethod('load_start', [magnet]) .then(function(data) { return $q.resolve(data); }).catch(function(err) { return $q.reject(err); }); } /** * Add a torrent file with the .torrent extension to the client through the API. Should * return a promise that the torrent was added sucessfully. File data is given as a blob * more information here: https://developer.mozilla.org/en/docs/Web/API/Blob. You may use * the existing implementation as a helping hand * @param {blob} filedata * @param {string} filename * @return {promise} isAdded */ this.uploadTorrent = function(buffer, filename) { $xmlrpc.callMethod('load_raw_start', [buffer]) .then(function(data) { return $q.resolve(data); }).catch(function(err) { console.error("Action error", err); return $q.reject(err); }) } function doAction(action, torrents, param) { var hashes = torrents.map(function(torrent) { return torrent.hash }) var calls = [] hashes.forEach(function(hash) { var call = { 'methodName': action, 'params': [hash] } if (param !== undefined) call.params.push(param) calls.push(call) }) return $xmlrpc.callMethod('system.multicall', [calls]) .then(function(data) { return $q.resolve(data); }).catch(function(err) { console.error("Action error", err); return $q.reject(err); }) } /** * Example action function. You will have to implement several of these to support the various * actions in your bittorrent client. Each action is supplied an array of the hashes on which * the action should be applied. * @param {array} hashes * @return {promise} actionIsDone */ this.start = function(torrents) { return doAction('d.start', torrents); } this.pause = function(torrents) { return doAction('d.pause', torrents); } this.resume = function(torrents) { return doAction('d.resume', torrents); } this.stop = function(torrents) { return doAction('d.try_stop', torrents); } this.close = function(torrents) { return doAction('d.close', torrents); } this.open = function(torrents) { return doAction('d.open', torrents); } this.label = function(torrents, label) { return doAction('d.set_custom1', torrents, label) } this.delete = function(torrents) { return doAction('d.erase', torrents) } this.recheck = function(torrents) { return doAction('d.check_hash', torrents) } this.priority = {} this.priority.high = function(torrents) { return doAction('d.set_priority', torrents, 3) } this.priority.normal = function(torrents) { return doAction('d.set_priority', torrents, 2) } this.priority.low = function(torrents) { return doAction('d.set_priority', torrents, 1) } this.priority.off = function(torrents) { return doAction('d.set_priority', torrents, 0) } /** * Whether the client supports sorting by trackers or not */ this.enableTrackerFilter = true /** * Represents the buttons and GUI elements to be displayed in the top navigation bar of the windows. * You may customize the GUI to your liking or to better accommodate the specific bittorrent client. * Every action must have a click function that corresponds to an action like the one showed above. * An object in the array should consist of the following information: * label [string]: Name of the button/element * type [string]: Can be 'button' or 'dropdown' or 'labels' * color [string]: Can be 'red', 'orange', 'yellow', 'olive', 'green', 'teal', 'blue', 'violet', 'purple', 'pink', 'brown', 'grey', 'black' * click [function]: The function to be executed when the when the button/element is pressed * icon [string]: The icon of the button. See here: http://semantic-ui.com/elements/icon.html */ this.actionHeader = [ { label: 'Start', type: 'button', color: 'green', click: this.resume, icon: 'play' }, { label: 'Pause', type: 'button', color: 'yellow', click: this.pause, icon: 'pause' }, { label: 'Stop', type: 'button', color: 'red', click: this.close, icon: 'stop' }, { label: 'Labels', click: this.label, type: 'labels' } ] /** * Represents the actions available in the context menu. Can be customized to your liking or * to better accommodate your bittorrent client. Every action must have a click function implemented. * Each element has an: * label [string]: The name of the action * click [function]: The function to be executed when clicked * icon [string]: The icon of the action. See here: http://semantic-ui.com/elements/icon.html */ this.contextMenu = [ { label: 'Recheck', click: this.recheck, icon: 'checkmark' }, { label: 'Priority', menu: [ { label: 'High', click: this.priority.high }, { label: 'Normal', click: this.priority.normal }, { label: 'Low', click: this.priority.low }, { label: 'Don\'t Download', click: this.priority.off } ] }, { label: 'Remove', click: this.delete, icon: 'remove' } ]; }]);