electorrent
Version:
An Electron/Node/AngularJS remote client app for uTorrent server
455 lines (395 loc) • 15.4 kB
JavaScript
'use strict';
angular.module('torrentApp')
.service('transmissionService', ["$http", "$q", "TorrentT", "transmissionConfig", "notificationService", function($http, $q, TorrentT, transmissionConfig, $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 = 'Transmission';
/*
* Good practise is keeping a configuration object for your communication with the API
*/
const config = {
ip: '',
port: '',
session: undefined,
encoded: '',
}
const fields = transmissionConfig.fields;
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}/transmission/rpc${path}`;
}
function updateSession(session) {
if (!session) return;
config.session = session;
}
function saveConnection(ip, port, encoded, session) {
config.ip = ip;
config.port = port;
config.encoded = encoded;
config.session = session;
}
/**
* 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) {
var defer = $q.defer();
var encoded = new Buffer(`${user}:${pass}`).toString('base64');
$http.get(url(ip,port),{
timeout: 5000,
headers: {
'Authorization': "Basic " + encoded
}
}).success(function(str, status, headers) {
var session = headers('X-Transmission-Session-Id');
saveConnection(ip, port, encoded, session);
defer.resolve(str);
}).error(function(err, status, headers, config){
if(status === 409){
var session = headers('X-Transmission-Session-Id');
saveConnection(ip, port, encoded, session);
defer.resolve(err);
return ;
}
defer.reject(err);
});
return defer.promise;
}
/**
* 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() {
var defer = $q.defer();
// downloadedEver and uploadedEver continue to count the second time you download that torrent.
/*var fields = ['id','name','totalSize','percentDone', 'downloadedEver',
'uploadedEver', 'uploadRatio','rateUpload','rateDownload','eta','comment',
'peersConnected','maxConnectedPeers','peersGettingToUs','seedsGettingFromUs',
'queuePosition','status','addedDate','doneDate','downloadDir','recheckProgress',
'isFinished','priorities'];
*/
var data = {
"arguments": {
"fields": fields
},
"method": "torrent-get"
}
$http.post(url(),data,{
headers:{
'Authorization':'Basic ' + config.encoded,
'X-Transmission-Session-Id': config.session
}
}).success(function(data, status, headers) {
var session = headers('X-Transmission-Session-Id');
updateSession(session);
defer.resolve(processData(data));
}).error(function(err){
defer.reject(err);
});
return defer.promise;
}
function processData(data){
var torrents = {
dirty: true,
labels: [],
all: [],
changed: [],
deleted: [],
trackers: []
};
torrents.all = data.arguments.torrents.map(build);
torrents.trackers = getTrackers(torrents.all)
return torrents;
}
function build(data){
return new TorrentT(data);
}
function getTrackers(torrents) {
let trackers = new Set()
torrents.forEach((torrent) => {
torrent.trackers.forEach((tracker) => trackers.add(tracker))
})
return Array.from(trackers).map((tracker) => 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]
}
}
/**
* 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) {
// Torrent-add
var data = {
"arguments": {
"filename": magnet
},
"method": "torrent-add"
}
return $http.post(url(), data, {
headers:{
'Authorization':'Basic ' + config.encoded,
'X-Transmission-Session-Id': config.session
}
}).then(function(response){
var session = response.headers('X-Transmission-Session-Id');
updateSession(session);
if ('torrent-duplicate' in response.data.arguments) return $q.reject('torrentDuplicate')
return $q.resolve();
}).catch(function(err){
if (err === 'torrentDuplicate'){
$notify.alert('Duplicate!',' This torrent is already added');
} else {
$notify.alert('Undefined error!', err);
}
return $q.reject()
})
}
/**
* 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) {
var defer = $q.defer();
var blob = new Blob([buffer]);
var base64data = '';
// Convert blob file object to base64 encoded.
var reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = function() {
/* The use of split is necessary because the reader returns the type of the data
* in the same string as the actual data, but we only need to send the actual data.*/
base64data = reader.result.split(',')[1];
// Torrent-add
var data = {
"arguments": {
"metainfo": base64data
},
"method": "torrent-add"
}
$http.post(url(), data, {
headers: {
'Authorization': 'Basic ' + config.encoded,
'X-Transmission-Session-Id': config.session
}
}).then(function(response){
var session = response.headers('X-Transmission-Session-Id');
updateSession(session);
if ('torrent-duplicate' in response.data.arguments) return $q.reject('torrentDuplicate');
defer.resolve();
}).catch(function(err){
if (err === 'torrentDuplicate'){
$notify.alert('Duplicate!',' This torrent is already added.');
} else {
$notify.alert('Undefined error!', err.msg);
}
defer.reject();
})
}
return defer.promise;
}
function doAction(command, torrents, mutator, value) {
if (!Array.isArray(torrents)) {
return $notify.alert('Error', 'Action was passed incorrect arguments')
}
var hashes = torrents.map(function(torrent) {
return torrent.hash
})
var data = {
"arguments": {},
"method": command
}
if (hashes.length){
data.arguments.ids = hashes;
}
if (mutator) {
data.arguments[mutator] = value;
}
return $http.post(url(), data, {
headers: {
'Authorization': 'Basic ' + config.encoded,
'X-Transmission-Session-Id': config.session
}
})
}
function doGlobalAction(command) {
doAction(command, []);
}
/**
* 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('torrent-start', torrents);
}
this.stop = function(torrents) {
return doAction('torrent-stop', torrents);
}
this.verify = function(torrents) {
return doAction('torrent-verify', torrents);
}
this.pauseAll = function() {
return doGlobalAction('torrent-stop');
}
this.resumeAll = function() {
return doGlobalAction('torrent-start');
}
this.queueUp = function(torrents) {
return doAction('queue-move-up', torrents);
}
this.queueDown = function(torrents) {
return doAction('queue-move-down', torrents);
}
this.remove = function(torrents) {
return doAction('torrent-remove', torrents)
}
this.removeAndLocal = function(torrents) {
return doAction('torrent-remove', torrents, 'delete-local-data', true)
}
/**
* 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.start,
icon: 'play'
},
{
label: 'Stop',
type: 'button',
color: 'red',
click: this.stop,
icon: 'pause'
},
{
label: 'More',
type: 'dropdown',
color: 'blue',
icon: 'plus',
actions: [
{
label: 'Pause All',
click: this.pauseAll
},
{
label: 'Resume All',
click: this.resumeAll
}
]
}
]
/**
* 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: 'Start',
click: this.start,
icon: 'play'
},
{
label: 'Pause',
click: this.stop,
icon: 'pause'
},
{
label: 'Verify',
click: this.verify,
icon: 'checkmark'
},
{
label: 'Move Up Queue',
click: this.queueUp,
icon: 'arrow up'
},
{
label: 'Move Queue Down',
click: this.queueDown,
icon: 'arrow down'
},
{
label: 'Remove',
menu: [
{
label: 'Torrent',
icon: 'remove',
click: this.remove
},
{
label: 'Torrent and Local Data',
icon: 'remove',
click: this.removeAndLocal
}
]
}
];
}]);