UNPKG

electorrent

Version:

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

1,642 lines (1,386 loc) 173 kB
var torrentApp = angular.module("torrentApp", ["ngResource", "ngAnimate", "ngTableResize", "infinite-scroll", "hc.marked", "xml-rpc", "ui.sortable"]); // Register torrent clients torrentApp.constant('$btclients', { 'utorrent': { name: 'µTorrent', service: 'utorrentService', icon: 'utorrent' }, 'qbittorrent': { name: 'qBittorrent', service: 'qbittorrentService', icon: 'qbittorrent' }, 'transmission': { name: 'Transmission', service: 'transmissionService', icon: 'transmission' }, 'rtorrent': { name: 'rTorrent', service: 'rtorrentService', icon: 'rtorrent' } }); // Configure the client torrentApp.run(["$rootScope", "$bittorrent", "configService", function($rootScope, $bittorrent, config){ config.initSettings(); }]); // Set application menu torrentApp.run(['menuWin', 'menuMac', 'electron', 'configService', function(menuWin, menuMac, electron, config){ var menuTemplate = null; if (electron.is.macOS()) { menuTemplate = menuMac; } else { menuTemplate = menuWin; } var appMenu = electron.menu.buildFromTemplate(menuTemplate); electron.menu.setApplicationMenu(appMenu); config.renderServerMenu() }]); angular.module("torrentApp").controller("mainController", ["$rootScope", "$scope", "$timeout", "$bittorrent", "electron", "configService", "notificationService", function ($rootScope, $scope, $timeout, $bittorrent, electron, config, $notify) { const PAGE_SETTINGS = 'settings'; const PAGE_WELCOME = 'welcome'; const PAGE_SERVERS = 'servers'; let settings = config.getAllSettings() $scope.servers = config.getServers() $scope.showTorrents = false; $scope.showLoading = true; $scope.statusText = "Loading"; var page = null; $rootScope.$on('ready', function() { if (!electron.program.debug) { electron.updater.checkForUpdates(); } if (!settings.servers.length) { pageWelcome(); return; } if (settings.startup === 'default') { let server = config.getDefaultServer() if (server){ connectToServer(server); } else { pageServers(); $notify.ok('No default server', 'Please choose a server to connect to') } } else if (settings.startup === 'latest') { let server = config.getRecentServer() console.log("Connecting to last used", server); if (server){ connectToServer(server) } else { pageServers() $notify.ok('No recent servers', 'Please choose a server to connect to') } } else { /* Ask or unknown*/ pageServers() } }); $scope.connectToServer = function(server) { connectToServer(server) } function connectToServer(server){ pageLoading() $bittorrent.setServer(server) $scope.statusText = "Connecting to " + $rootScope.$btclient.name; $rootScope.$btclient.connect(server.ip, server.port, server.user, server.password) .then(function(){ pageTorrents(); requestMagnetLinks(); requestTorrentFiles(); }).catch(function(){ pageSettings('connection'); }).finally(function() { config.renderServerMenu() }); } // Send a request to the main process for magnet links function requestMagnetLinks(){ electron.ipc.send('send:magnets'); } function requestTorrentFiles() { console.log("Requestion torrent files"); electron.ipc.send('send:torrentfiles') } // Listen for incomming magnet links from the main process electron.ipc.on('magnet', function(event, data){ data.forEach(function(magnet){ $rootScope.$btclient.addTorrentUrl(magnet); }) }) // Listen for incomming torrent files from the main process electron.ipc.on('torrentfiles', function uploadTorrent(event, buffer, filename){ console.log("Got torrent file", filename); $rootScope.$btclient.uploadTorrent(buffer, filename) .catch(function(err) { console.error("Error", err); }) }) function pageTorrents(){ $scope.showTorrents = true; $scope.showLoading = false; $scope.$broadcast('start:torrents'); page = null; } function pageLoading() { $scope.showLoading = true; } function pageSettings(settingsPage){ $scope.showLoading = false; if (settingsPage){ $scope.$broadcast('settings:page', settingsPage); } page = PAGE_SETTINGS; } function pageServers() { $scope.showLoading = false; $scope.showTorrents = false; page = PAGE_SERVERS; } function pageWelcome(){ $scope.showLoading = false; page = PAGE_WELCOME; } $scope.$on('add:server', function() { $scope.$broadcast('stop:torrents') $rootScope.$btclient = null pageWelcome() $scope.$apply(); }) $scope.$on('connect:server', function(event, server) { console.log("Connecting to server", server.getNameAtAddress()); pageLoading() $scope.$broadcast('stop:torrents') $scope.$broadcast('clear:torrents') $rootScope.$btclient = null $rootScope.$server = null $bittorrent.setServer(server) connectToServer(server) $scope.$broadcast('start:torrents', true) // Full update $scope.$apply(); }) $scope.$on('show:settings', function() { if (page === PAGE_WELCOME) return; if (page === PAGE_SERVERS) return; page = PAGE_SETTINGS; $scope.$apply(); }) $scope.$on('show:welcome', function() { page = PAGE_WELCOME; $scope.$apply(); }) $scope.$on('show:torrents', function(){ pageTorrents(); }) $scope.$on('emit:new:settings', function(event, data) { $scope.$broadcast('new:settings', data) }) $scope.showSettings = function(){ return page === PAGE_SETTINGS; } $scope.showWelcome = function() { return page === PAGE_WELCOME; } $scope.showServers = function() { return page === PAGE_SERVERS; } }]); "use strict"; angular.module("torrentApp").controller("torrentsController", ["$rootScope", "$scope", "$timeout", "$filter", "$q", "$bittorrent", "notificationService", "configService", "AbstractTorrent", function ($rootScope, $scope, $timeout, $filter, $q, $bittorrent, $notify, config, AbstractTorrent) { const TIMEOUT = 2000; const LIMIT = 25; var selected = []; var lastSelected = null; var timeout; var reconnect; var settings = config.getAllSettings(); $scope.connectionLost = false; $scope.torrents = {}; $scope.arrayTorrents = []; $scope.contextMenu = null; $scope.showDragAndDrop = false; $scope.labelsDrowdown = null; $scope.torrentLimit = LIMIT; $scope.labels = []; $scope.trackers = [] $scope.resizeMode = settings.ui.resizeMode; $scope.filters = { status: 'all' }; $rootScope.$on('show:draganddrop', function(event, show) { $scope.showDragAndDrop = show; $scope.$apply(); }) $scope.$on('new:settings', function(event, data) { $scope.resizeMode = data.ui.resizeMode; resetAll(); }); $scope.showMore = function() { $scope.torrentLimit += LIMIT; }; $scope.debug = function(){ for (var i = 0; i < selected.length; i++){ console.info(selected[i]); } return $q.when(); }; $scope.activeOn = function(filter) { if ($scope.filters.status === filter){ return 'active'; } else { return ''; } } $scope.setLabel = function(label){ $rootScope.$btclient.setLabel(getSelectedHashes(), label) .then(function() { $scope.update(); }) .catch(function() { $notify.alert("Could not set label", "The label could not be changed. Please go to settings and configure your connection"); }) } $scope.$on('start:torrents', function(event, fullupdate){ $scope.update(!!fullupdate); startTimer(); }); $scope.$on('clear:torrents', function(){ clearAll() $scope.filters = { status: 'all' }; }) $scope.$on('stop:torrents', function(){ stopTimer(); }) $scope.$on('show:settings', function(){ stopTimer(); }); function startTimer(fullupdate){ if (timeout) { $timeout.cancel(timeout) } timeout = $timeout(function(){ //console.info("Update!"); $scope.update(fullupdate) .then(function(){ startTimer(); $scope.connectionLost = false; }).catch(function() { $scope.connectionLost = true; startReconnect(); }); }, TIMEOUT); } function startReconnect() { $notify.disableAll(); var data = config.getServer(); reconnect = $timeout(function() { $rootScope.$btclient.connect(data.ip, data.port, data.user, data.password) .then(function() { $notify.enableAll(); startTimer(true); }).catch(function() { startReconnect() }) }, TIMEOUT) } function clearAll() { $scope.torrents = {}; $scope.arrayTorrents = []; $scope.labels = []; $scope.trackers = []; } function resetAll() { clearAll() $scope.update(true); console.log("Reset!") } function stopTimer(){ console.info("Torrents stopped"); $timeout.cancel(timeout); } $scope.filterByStatus = function(status){ deselectAll(); lastSelected = null; $scope.filters.status = status; $scope.torrentLimit = LIMIT; refreshTorrents(); } $scope.filterByLabel = function(label){ deselectAll(); lastSelected = null; $scope.filters.label = label; $scope.torrentLimit = LIMIT; refreshTorrents(); } $scope.activeLabel = function(label) { return $scope.filters.label === label; } $scope.filterByTracker = function(tracker) { deselectAll(); lastSelected = null; $scope.filters.tracker = tracker; $scope.torrentLimit = LIMIT; refreshTorrents(); } $scope.activeTracker = function(tracker) { return $scope.filters.tracker === tracker; } $scope.showContextMenu = function(event, torrent /*, index*/) { if (!torrent.selected){ singleSelect(torrent); } $scope.contextMenu.show(event); }; $scope.numInFilter = function(status) { var num = 0; var filter = torrentFilter(status); angular.forEach($scope.torrents, function(torrent /*, hash*/) { if (filter(torrent)) { num++; } }); return num; } $scope.noneSelected = function(){ return selected.length === 0; } function toggleSelect(torrent){ if (!torrent.selected){ selected.push(torrent); } else { selected = selected.filter(function(item){ return item.hash !== torrent.hash; }); } torrent.selected = !torrent.selected; lastSelected = torrent; } function deselectAll(){ for (var i = 0; i < selected.length; i++){ selected[i].selected = false; } selected = []; } function singleSelect(torrent){ deselectAll(); torrent.selected = true; selected.push(torrent); lastSelected = torrent; } function multiSelect(index){ var lastIndex = $scope.arrayTorrents.indexOf(lastSelected); if (lastIndex < 0){ return; } var i, j; if (lastIndex < index){ i = lastIndex; j = index; } else { i = index; j = lastIndex; } deselectAll(); while (i<=j){ $scope.arrayTorrents[i].selected = true; selected.push($scope.arrayTorrents[i]); i++; } } $scope.setSelected = function(event, torrent, index) { if (event.ctrlKey || event.metaKey){ toggleSelect(torrent); } else if (event.shiftKey){ multiSelect(index); } else { singleSelect(torrent); } } function getSelectedHashes(){ var hashes = []; angular.forEach(selected, function(torrent){ hashes.push(torrent.hash) }) return hashes; } $scope.doAction = function(action, name, data) { action(selected, data) .then(function(){ $scope.update(); }) .catch(function(err) { console.error('Action error', err) $notify.alert("Invalid action", "The action could not be performed because the server responded with a faulty reply"); }); }; $scope.doContextAction = function(action, name) { action(selected) .then(function(){ $scope.update(); }) .catch(function(err) { console.error('Context action error', err) $notify.alert("Invalid action", "The action could not be performed because the server responded with a faulty reply"); }) } function fetchTorrents(){ var results = []; angular.forEach($scope.torrents, function(torrent /*, hash*/) { results.push(torrent); }); return results; } $scope.changeSorting = function(sortName, descending) { $scope.torrentLimit = LIMIT; $scope.filters.sort = sortName; $scope.filters.order = descending; refreshTorrents(); } function torrentSorter(){ var sort = $scope.filters.sort || 'dateAdded'; var desc = $scope.filters.order; var sorter = AbstractTorrent.sort(sort); var descSort = function(a, b) { return sorter(a[sort], b[sort]); } var ascSort = function(a, b) { return sorter(a[sort], b[sort]) * (-1); } if (desc) return descSort else return ascSort } function statusFilter(torrent, status) { switch (status) { case 'all': return true; case 'finished': return torrent.isStatusCompleted(); case 'downloading': return torrent.isStatusDownloading() || torrent.isStatusPaused(); case 'paused': return torrent.isStatusPaused(); case 'queued': return torrent.isStatusQueued(); case 'seeding': return torrent.isStatusSeeding(); case 'error': return torrent.isStatusError(); case 'stopped': return torrent.isStatusStopped(); } } function torrentFilter(status, label, tracker){ var filterStatus = status || $scope.filters.status; var filterLabel = label || $scope.filters.label; var filterTracker = tracker || $scope.filters.tracker; return function(torrent){ var keep = []; if (filterStatus) { keep.push(statusFilter(torrent, filterStatus)); } if (filterLabel) { keep.push(torrent.label === filterLabel) } if (filterTracker) { keep.push(torrent.trackers && torrent.trackers.some((tracker) => { return tracker && tracker.includes(filterTracker) })) } return keep.every(function(shouldkeep) { return shouldkeep; }); } } function refreshTorrents(){ var torrents = fetchTorrents(); torrents = torrents.filter(torrentFilter()); torrents = torrents.sort(torrentSorter()); $scope.arrayTorrents = torrents; } function reassignSelected() { var newSelected = [] selected.forEach(function(torrent){ var delegate = $scope.torrents[torrent.hash]; if (delegate){ delegate.selected = true newSelected.push(delegate) } }) selected = newSelected reassignLastSelected() } function reassignLastSelected() { if(!lastSelected) return var lastDelegate = $scope.torrents[lastSelected.hash]; if(lastDelegate) { lastDelegate.selected = true lastSelected = lastDelegate } else { lastSelected = null } } $scope.update = function(fullupdate) { var q = $rootScope.$btclient.torrents(fullupdate) q.then(function(torrents) { newTorrents(torrents); deleteTorrents(torrents); changeTorrents(torrents); updateLabels(torrents); updateTrackers(torrents); }) return q; }; function checkNotification(old, updated) { if (!old || !updated) return if (updated.percent === 1000 && old.percent < 1000) { if (settings.ui.notifications === true){ $notify.torrentComplete(old); } } } function newTorrents(torrents){ if ((torrents.all && torrents.all.length > 0) || torrents.dirty === true) { var torrentMap = {}; for (var i = 0; i < torrents.all.length; i++){ var torrent = torrents.all[i]; torrentMap[torrent.hash] = torrent; var old = $scope.torrents[torrent.hash]; checkNotification(old, torrent); } $scope.torrents = torrentMap; reassignSelected() refreshTorrents() } } function deleteTorrents(torrents){ if (torrents.deleted && torrents.deleted.length > 0) { for (var i = 0; i < torrents.deleted.length; i++) { delete $scope.torrents[torrents.deleted[i]]; } refreshTorrents(); } } function changeTorrents(torrents){ if (torrents.changed && torrents.changed.length > 0) { for (var i = 0; i < torrents.changed.length; i++) { var torrent = torrents.changed[i]; var existing = $scope.torrents[torrent.hash]; checkNotification(existing, torrent); if (existing) { existing.update(torrent); } else { $scope.torrents[torrent.hash] = torrent; } } refreshTorrents() } } function updateLabels(torrents){ if (torrents.labels && torrents.labels.length > 0) { torrents.labels.forEach(function(label /*, index*/){ if (!$scope.labels.includes(label)) { $scope.labels.push(label) } }) } } function updateTrackers(torrents) { if (torrents.trackers && torrents.trackers.length > 0) { torrents.trackers.forEach(function(tracker) { if (!$scope.trackers.includes(tracker)) { $scope.trackers.push(tracker) } }) } } }]) angular.module("torrentApp").controller("settingsController", ["$rootScope", "$scope", "$injector", "$bittorrent", "$btclients", "configService", "notificationService", "electron", function($rootScope, $scope, $injector, $bittorrent, $btclients, config, $notify, electron) { // External Settings reference $scope.settings = {}; $scope.server = {} $scope.btclients = $btclients; // Internal settings reference $scope.general = { magnet: false } $scope.appVersion = electron.app.getVersion() $scope.nodeVersion = process.versions.node; $scope.chromeVersion = process.versions.chrome; $scope.electronVersion = process.versions.electron; $scope.connecting = false; $scope.page = 'general'; loadAllSettings(); // $scope.$watch(function() { // return $scope.settings // }); function loadAllSettings() { $scope.settings = config.getAllSettings(); $scope.server = $bittorrent.getServer(); $scope.general = { magnets: electron.app.isDefaultProtocolClient('magnet') } } function subscribeToMagnets() { if ($scope.general.magnets) { electron.app.setAsDefaultProtocolClient('magnet'); } else { electron.app.removeAsDefaultProtocolClient('magnet'); } } $scope.$on('show:settings', function() { loadAllSettings(); }) function writeSettings() { config.saveAllSettings($scope.settings) .then(function() { $scope.close(); $notify.ok("Saved Settings", "You settings has been updated") }).catch(function(err) { $notify.alert("Settings could not be saved", err) }) config.updateServer($scope.server) .then(function() { $bittorrent.setServer($scope.server) }).catch(function() { $notify.alert("Settings error", "Could not save new server") }) subscribeToMagnets(); } $scope.close = function() { $scope.$emit('show:torrents'); loadAllSettings(); } $scope.save = function() { $scope.connecting = true; var ip = $scope.server.ip; var port = $scope.server.port; var user = $scope.server.user; var password = $scope.server.password; var client = $scope.server.client; var btclient = $bittorrent.getClient(client); btclient.connect(ip, port, user, password) .then(function() { writeSettings(); $bittorrent.setClient(btclient); $rootScope.$broadcast('new:settings', $scope.settings) }).catch(function(err) { console.error("Oh noes!", err); }).finally(function() { $scope.connecting = false; }) } $scope.activeOn = function(page) { var value = ''; if ($scope.page === page) value = 'active'; return value; } $scope.toggleDefaultServer = function(server) { if (server.default === true) { config.setDefault(server, true /* Skip saving */) } console.log("Toggle default server", server); } $scope.removeServer = function(server) { if ($rootScope.$server === server) { $notify.alert('Server in use', 'Can\'t remove a server that is currently being used') } else { config.removeServer(server) } } $scope.gotoPage = function(page) { $scope.page = page; } $scope.$on('settings:page', function(event, page){ $scope.gotoPage(page); }) }]); angular.module("torrentApp").controller("welcomeController", ["$scope", "$timeout", "$bittorrent", "$btclients", "electron", "configService", "notificationService", "Server", function ($scope, $timeout, $bittorrent, $btclients, electron, config, $notify, Server) { $scope.connecting = false; $scope.btclients = $btclients; function clearForm() { $scope.ip = '' $scope.port = '' $scope.username = '' $scope.password = '' $scope.client = undefined } $scope.connect = function() { var ip = $scope.ip || ''; var port = $scope.port || ''; var user = $scope.username || ''; var password = $scope.password || ''; var client = $scope.client || ''; var btclient = $bittorrent.getClient(client); if (!btclient) { $notify.alert("Opps!", "Please select a client to connect to!") $scope.connecting = false; return; } $scope.connecting = true; btclient.connect(ip, port, user, password).then(function() { $timeout(function(){ $bittorrent.setClient(btclient); saveServer(ip, port, user, password, client); }, 500) }).catch(function(err) { $timeout(function(){ console.error(err); }, 500) }).finally(function() { $scope.connecting = false; }) } function saveServer(ip, port, username, password, client){ let server = new Server(ip, port, username, password, client) $bittorrent.setServer(server) config.saveServer(server).then(function(){ $scope.$emit('show:torrents'); clearForm() $notify.ok("Success!", "Hooray! Welcome to Electorrent") }).catch(function(){ $notify.alert("Oops!", "Could not save settings?!") }) } }]); angular.module("torrentApp").controller("notificationsController", ["$scope", "$rootScope", "$timeout", "electron", "$http", function($scope, $rootScope, $timeout, electron, $http) { var id = 0; $scope.updateData = { releaseDate: "Just now...", updateUrl: "http://www.update.this.app.com" }; $scope.notifications = []; $scope.close = function(index){ $scope.notifications.splice(index, 1); } $rootScope.$on('notification', function(event, data){ id++; data.notificationId = id; $scope.notifications.push(data); removeAlert(data, data.delay || 5000); }) function removeAlert(data, delay){ $timeout(function(){ $scope.notifications = $scope.notifications.filter(function(value){ return value.notificationId !== data.notificationId; }) }, delay); } // Listen for software update event from main process electron.ipc.on('autoUpdate', function(event, data){ $scope.manualUpdate = false; $http.get(data.updateUrl, { timeout: 10000 }) .success(function(releaseData){ data.releaseNotes = releaseData.notes; data.releaseDate = releaseData.pub_date; }) .catch(function(){ data.releaseNotes = "Not available. Please go to the website for more info" }) .then(function() { $scope.updateData = data $('#updateModal').modal('show'); }) }) // Listen for manual updates from the main process electron.ipc.on('manualUpdate', function(event, data){ $scope.updateData = data $scope.manualUpdate = true; $timeout(function(){ $('#updateModal').modal('show'); }, 500) }); $scope.installUpdate = function() { if ($scope.manualUpdate){ electron.updater.manualQuitAndUpdate(); //electron.ipc.send('startUpdate', null); } else { electron.autoUpdater.quitAndInstall(); } } }]); angular.module('torrentApp').factory("httpFormService", function() { // I prepare the request data for the form post. function transformRequest(data, getHeaders) { var headers = getHeaders(); headers["Content-Type"] = "application/x-www-form-urlencoded"; return(serializeData(data)); } // Return the factory value. return(transformRequest); function serializeData(data) { // If this is not an object, defer to native stringification. if(!angular.isObject(data)) { return((data === null) ? "" : data.toString()); } var buffer = []; // Serialize each key in the object. for(var name in data) { if(!data.hasOwnProperty(name)) { continue; } var value = data[name]; buffer.push(parseComponent(name, value)); } // Serialize the buffer and clean it up for transportation. return buffer.join("&") } function parseComponent(name, value) { return encodeURIComponent(name) + "=" + parseValue(value); } function parseValue(value){ if (Array.isArray(value)) { var encoded = value.map(encodeURIComponent); return encoded.join('|'); } else if (value !== null) { return encodeURIComponent(value) } else { return ""; } } }); 'use strict'; angular.module('torrentApp').service('$bittorrent', ['$rootScope', '$injector', '$btclients', 'configService', 'notificationService', 'electron', function($rootScope, $injector, $btclients, config, $notify, electron){ this.getClient = function(clientName) { if (clientName) { return fetchClientManual(clientName); } else { return fetchClientAuto(); } } this.changeClient = function(clientName) { var service = this.getClient(clientName); this.setClient(service); } this.setClient = function(service) { $rootScope.$btclient = service; console.info("Changed client to:", service.name || "<service missing name>"); } this.setServer = function(server) { $rootScope.$btclient = this.getClient(server.client) $rootScope.$server = server server.updateLastUsed() config.saveAllSettings() console.info("Changed server to:", $rootScope.$server) } this.getServer = function() { return $rootScope.$server } function fetchClientAuto() { var server = config.getDefaultServer(); if (!server) return return fetchClientManual(server.client); } function fetchClientManual(name) { var client = $btclients[name]; if (client){ return $injector.get(client.service); } else { console.error('Bittorrent client "' + name + '" not available'); } } this.uploadFromClipboard = function() { var magnet = electron.clipboard.readText(); var protocol = ['magnet', 'http']; var supported = protocol.some(function(protocol) { return magnet.startsWith(protocol); }) if (!supported) { $notify.alert('Wait a minute?', 'Your clipboard doesn\'t contain a magnet link'); return; } $rootScope.$btclient.addTorrentUrl(magnet); } }]); angular.module('torrentApp').factory("electron", [function() { var o = {}; // Get the Electron remote const remote = require('electron').remote; // Directly accesible modules o.ipc = require('electron').ipcRenderer; o.shell = require('electron').shell; //Remote moudles from main process o.remote = remote; o.app = remote.app; o.browserWindow = remote.BrowserWindow; o.clipboard = remote.clipboard; o.dialog = remote.dialog; o.menu = remote.Menu; o.menuItem = remote.MenuItem; o.nativeImage = remote.nativeImage; o.powerMonitor = remote.powerMonitor; o.protocol = remote.protocol; o.screen = remote.screen; o.tray = remote.shell; o.capturer = remote.capturer; o.autoUpdater = remote.autoUpdater; // Custom resources o.config = remote.require('./lib/config.js'); o.updater = remote.require('./lib/update.js'); o.is = remote.require('electron-is'); o.program = remote.require('yargs').argv; o.torrents = remote.require('./lib/torrents.js') // Return object return o; }]) 'use strict'; angular.module('torrentApp').service('configService', ['$rootScope', 'notificationService', 'electron', '$q', 'Server', function($rootScope, $notify, electron, $q, Server) { const MenuItem = electron.menuItem const config = electron.config; var settings = { startup: 'default', server: { ip: '', port: '', user: '', password: '', type: '', }, ui: { resizeMode: '', notifications: true }, servers: [] }; this.initSettings = function() { var org = config.settingsReference(); angular.merge(settings, org) settings.servers = settings.servers.map((server) => { return new Server(server) }) console.log("Settings", settings); } // Angular wrapper for saving to config function put(key, value) { var q = $q.defer(); config.put(key, value, function(err) { if(err) q.reject(err) else q.resolve(); }); return q.promise; } // Angular wrapper for getting config function get(value) { return config.get(value); } function isDefault(server) { return server.default === true } this.appendServer = function(server) { settings.servers.push(server) this.renderServerMenu() } this.getAllSettings = function() { return settings; } this.setCurrentServerAsDefault = function() { if (!$rootScope.$server) { $notify.warning('Can\'t set default server', 'You need to chose a server to set it as default') } console.log("Set default", $rootScope.$server); this.setDefault($rootScope.$server) } this.setDefault = function(server, skipsave) { let found = this.getServer(server.id) if (!found) return settings.servers.forEach(function(value) { value.default = false }) found.default = true if (!skipsave) { this.saveAllSettings().then(() => { $notify.ok('Default server saved', 'You default server is now ' + server.getNameAtAddress()) }).catch(function() { $notify.alert('I/O Error', 'Could not save default server. Local configuration file could not be written to?!') }) } } function settingsToJson() { let copy = {} angular.copy(settings, copy) copy.servers = copy.servers.map((server) => { return server.json() }) return copy } this.saveAllSettings = function(newSettings) { var q = $q.defer(); angular.merge(settings, newSettings) config.saveAll(settingsToJson(), function(err) { if(err) q.reject(err) else q.resolve(); }); return q.promise; } this.saveServer = function(ip, port, user, password, client) { if(arguments.length === 1) { this.appendServer(arguments[0]); } else { this.appendServer(new Server(ip, port, user, password, client)) } console.info("Servers:", settings.servers); return this.saveAllSettings() } this.removeServer = function(server) { settings.servers = settings.servers.filter((s) => { return s.id !== server.id }) this.renderServerMenu() } this.updateServer = function(update) { let server = this.getServer(update.id); if(!server) return $q.reject('Server with id ' + update.id + ' not found') angular.merge(server, update) console.info("Servers:", settings.servers); return this.saveAllSettings() } this.getServer = function(id) { return settings.servers.find((server) => server.id === id) } this.getServers = function() { return settings.servers } this.getDefaultServer = function() { return settings.servers.find(isDefault) } this.getRecentServer = function() { let maxServer = settings.servers[0] settings.servers.forEach(function(server){ if (server.lastused > maxServer.lastused){ maxServer = server } }) return maxServer } function getMenu(menu, name) { return menu.items.find((menuItem) => menuItem.label === name) } this.renderServerMenu = function() { let menu = electron.menu.getApplicationMenu() let serverMenu = getMenu(menu, 'Servers').submenu serverMenu.clear() serverMenu.append(new MenuItem({ label: 'Add new server...', click: () => $rootScope.$broadcast('add:server'), })) serverMenu.append(new MenuItem({ label: 'Set current as default', click: () => this.setCurrentServerAsDefault(), enabled: !!$rootScope.$server })) serverMenu.append(new MenuItem({type: 'separator'})) renderServerMenuOptions(serverMenu, this.getServers()) electron.menu.setApplicationMenu(menu) } function renderServerMenuOptions(menu, servers) { if (!$rootScope.$server) { menu.append(new MenuItem({ label: 'Disabled...', enabled: false })) return } servers.forEach((server) => { menu.append(new MenuItem({ label: server.getNameAtAddress(), id: server.id, click: () => $rootScope.$broadcast('connect:server', server), checked: server.id === $rootScope.$server.id, type: 'radio' })) }) } }]); 'use strict'; angular.module('torrentApp') .service('notificationService', ["$rootScope", "electron", function($rootScope, electron) { var disableNotifications = false; this.disableAll = function() { disableNotifications = true; } this.enableAll = function() { disableNotifications = false; } this.alert = function(title, message) { sendNotification(title, message, "negative"); } this.warning = function(title, message) { sendNotification(title, message, "warning"); } this.ok = function(title, message) { sendNotification(title, message, "positive"); } function sendNotification(title, message, type) { if (disableNotifications) return; var notification = { title: title, message: message, type: type } $rootScope.$emit('notification', notification); } this.alertAuth = function(message, status){ if (status === -1){ this.alert("Connection problem", "The connection to the server timed out!") } else if (status === 401){ this.alert("Connection problem", "You entered an incorrent username/password") } else { this.alert("Connection problem", "The connection could not be established") } } this.torrentComplete = function(torrent) { var torrentNotification = new Notification('Torrent Completed!', { body: torrent.decodedName, icon: 'img/electorrent-icon.png' }) torrentNotification.onclick = () => { console.info('Notification clicked') } } // Listen for incomming notifications from main process electron.ipc.on('notify', function(event, data){ sendNotification(data.title, data.message, data.type || 'warning'); }) }]); 'use strict'; angular.module('torrentApp').factory('Server', ['AbstractTorrent', '$btclients', function(Torrent, $btclients) { /** * Constructor, with class name */ function Server(ip, port, user, password, client) { if (arguments.length === 1) { this.fromJson(arguments[0]) } else { this.id = generateGUID() this.ip = ip this.port = port this.user = user this.password = password this.client = client this.lastused = -1 this.columns = defaultColumns() } } Server.prototype.fromJson = function (data) { this.id = data.id this.ip = data.ip this.port = data.port this.user = data.user this.password = data.password this.client = data.client this.default = data.default this.lastused = data.lastused this.columns = parseColumns(data.columns) }; Server.prototype.json = function () { return { id: this.id, ip: this.ip, port: this.port, user: this.user, password: this.password, client: this.client, default: this.default, lastused: this.lastused || -1, columns: this.columns.filter((column) => column.enabled).map((column) => column.name) } }; Server.prototype.getName = function () { return $btclients[this.client].name }; Server.prototype.getIcon = function () { return $btclients[this.client].icon }; Server.prototype.getNameAtAddress = function () { return this.getName() + " @ " + this.ip }; Server.prototype.updateLastUsed = function () { this.lastused = new Date().getTime() }; function zipsort(obj, sor) { return function(a,b) { let i = sor.indexOf(a.name) let j = sor.indexOf(b.name) if (i === j) { return 0 } else if (i === -1) { return 1 } else if (j === -1) { return -1 } else { return i - j } } } function parseColumns(data) { if (!data || data.length === 0) return defaultColumns() let columns = [] angular.copy(Torrent.COLUMNS, columns) columns.sort(zipsort(columns, data)) columns.forEach((column) => { column.enabled = data.some((entry) => (entry === column.name)) }) return columns } function defaultColumns() { let columns = [] angular.copy(Torrent.COLUMNS, columns) columns.forEach((columns) => columns.enabled = true) return columns } function generateGUID() { return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); } function s4() { return Math.floor((1 + Math.random()) * 0x10000) .toString(16) .substring(1); } /** * Return the constructor function */ return Server; }]); 'use strict'; angular.module('torrentApp').factory('Column', ['$filter', function($filter) { /** * Constructor, with class name */ function Column(name, type, attribute, filtername) { this.name = name this.type = type this.enabled = false this.attribute = attribute this.filter = filtername && $filter(filtername) } Column.prototype.value = function (torrent) { if (this.filter) { return this.filter(torrent[this.attribute]) } else { return torrent[this.attribute] } }; /** * Return the constructor function */ return Column; }]); 'use strict'; angular.module('torrentApp').factory('AbstractTorrent', ['Column', function(Column) { const statusRegex = /[^a-zA-Z(): ]/g; var decodeName = function(name) { if(!name) return undefined return name.replace(/[\._]/g, ' ').replace(/(\[[^\]]*\])(.*)$/, '$2 $1').trim(); }; var cleanName = function(name) { if(!name) return undefined return name.toLowerCase().replace(/s?([0-9]{1,2})[x|e|-]([0-9]{1,2})/, '').replace( /(bdrip|brrip|cam|dttrip|dvdrip|dvdscr|dvd|fs|hdtv|hdtvrip|hq|pdtv|satrip|dvbrip|r5|r6|ts|tc|tvrip|vhsrip|vhsscr|ws|aac|ac3|dd|dsp|dts|lc|ld|md|mp3|xvid|720p|1080p|fs|internal|limited|proper|stv|subbed|tma|tnz|silent|tls|gbm|fsh|rev|trl|upz|unrated|webrip|ws|mkv|avi|mov|mp4|mp3|iso|x264|x265|h264|h265)/g, '').trim(); }; /** hash (string), status* (integer), name (string), size (integer in bytes), percent progress (integer in per mils), downloaded (integer in bytes), upload-speeded (integer in bytes), ratio (integer in per mils), upload-speed speed (integer in bytes per second), download speed (integer in bytes per second), eta (integer in seconds), label (string), peers connected (integer), peers in swarm (integer), seeds connected (integer), seeds in swarm (integer), availability (integer in 1/65535ths), torrent queue order (integer), remaining (integer in bytes) */ /** * Constructor, with class name */ function AbstractTorrent({ hash, name, size, percent, downloaded, uploaded, ratio, uploadSpeed, downloadSpeed, eta, label, peersConnected, peersInSwarm, seedsConnected, seedsInSwarm, torrentQueueOrder, statusMessage, dateAdded, dateCompleted, savePath }) { this.selected = false; this.isStarred = false; this.hash = hash; this.name = name; this.size = size; this.percent = percent; this.downloaded = downloaded; this.uploaded = uploaded; this.ratio = ratio; this.uploadSpeed = uploadSpeed; this.downloadSpeed = downloadSpeed; this.eta = eta; this.label = label; this.peersConnected = peersConnected; this.peersInSwarm = peersInSwarm; this.seedsConnected = seedsConnected; this.seedsInSwarm = seedsInSwarm; this.torrentQueueOrder = torrentQueueOrder; this.statusMessage = statusMessage; this.dateAdded = dateAdded; this.dateCompleted = dateCompleted; this.savePath = savePath; this.decodedName = decodeName(this.name); this.cleanedName = cleanName(this.decodedName); } AbstractTorrent.prototype.update = function(other) { for(var k in other) { if(other.hasOwnProperty(k) && k !== 'selected') { if(other[k] !== undefined) { this[k] = other[k]; } } } }; AbstractTorrent.prototype.getMagnetURI = function(longUri) { var i = 0; var link = 'magnet:?xt=urn:btih:' + this.hash; if(longUri) { link += '&dn=' + encodeURIComponent(this.name); link += '&xl=' + encodeURIComponent(this.size); if(this.props && this.props.trackers) { var trackers = this.props.trackers.split('\r\n'); for(i = 0; i < trackers.length; i++) { if(trackers[i].length > 0) { link += '&tr=' + encodeURIComponent(trackers[i]); } } } } return link; }; AbstractTorrent.prototype.isStatusError = function() { throw new Error('isStatusError not implemented'); }; AbstractTorrent.prototype.isStatusPaused = function() { throw new Error('isStatusPaused not implemented'); }; AbstractTorrent.prototype.isStatusQueued = function() { throw new Error('isStatusQueued not implemented'); }; AbstractTorrent.prototype.isStatusCompleted = function() { throw new Error('isStatusCompleted not implemented'); }; AbstractTorrent.prototype.isStatusDownloading = func