mirror of
				https://github.com/KevinMidboe/delugeClient.git
				synced 2025-10-29 12:00:13 +00:00 
			
		
		
		
	Restructured project files.
Moved all packages files to delugeClient folder. Split contents of deluge_cli to __main__, utils, deluge & torrent. Config default changed from config.ini --> delugeClient/default_config.ini. Setup.py updated with new entry.
This commit is contained in:
		
							
								
								
									
										10
									
								
								config.ini
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								config.ini
									
									
									
									
									
								
							| @@ -1,10 +0,0 @@ | |||||||
| [Deluge] |  | ||||||
| HOST = YOUR_DELUGE_HOST |  | ||||||
| PORT = YOUR_DELUGE_PORT |  | ||||||
| USER = YOUR_DELUGE_USER |  | ||||||
| PASSWORD = YOUR_DELUGE_PASSWORD |  | ||||||
|  |  | ||||||
| [ssh] |  | ||||||
| HOST = YOUR_DELUGE_SERVER_IP |  | ||||||
| USER = YOUR_SSH_USER |  | ||||||
| PKEY = YOUR_SSH_PRIVATE_KEY_DIRECTORY |  | ||||||
							
								
								
									
										20
									
								
								delugeClient/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								delugeClient/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | from sys import path | ||||||
|  | from os.path import dirname | ||||||
|  |  | ||||||
|  | path.append(dirname(__file__)) | ||||||
|  |  | ||||||
|  | __version__=0.1 | ||||||
|  |  | ||||||
|  | import logging | ||||||
|  | logger = logging.getLogger('deluge_cli') | ||||||
|  | logger.setLevel(logging.DEBUG) | ||||||
|  |  | ||||||
|  | fh = logging.FileHandler(os.path.join(BASE_DIR, 'deluge_cli.log')) | ||||||
|  | fh.setLevel(logging.DEBUG) | ||||||
|  | ch = logging.StreamHandler() | ||||||
|  | ch.setLevel(logging.ERROR) | ||||||
|  |  | ||||||
|  | formatter = logging.Formatter('%(asctime)s %(levelname)8s %(name)s | %(message)s') | ||||||
|  | fh.setFormatter(formatter) | ||||||
|  | logger.addHandler(fh) | ||||||
|  | logger.addHandler(ch) | ||||||
							
								
								
									
										148
									
								
								delugeClient/__main__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								delugeClient/__main__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,148 @@ | |||||||
|  | #!/usr/bin/env python3.6 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | """Custom delugeRPC client | ||||||
|  | Usage: | ||||||
|  |    deluge_cli add MAGNET [DIR] [--json | --debug | --warning | --error] | ||||||
|  |    deluge_cli search NAME [--json] | ||||||
|  |    deluge_cli get  TORRENT [--json | --debug | --warning | --error] | ||||||
|  |    deluge_cli ls [--downloading | --seeding | --paused | --json] | ||||||
|  |    deluge_cli toggle TORRENT | ||||||
|  |    deluge_cli progress [--json] | ||||||
|  |    deluge_cli rm NAME [--destroy] [--debug | --warning | --error] | ||||||
|  |    deluge_cli (-h | --help) | ||||||
|  |    deluge_cli --version | ||||||
|  |  | ||||||
|  | Arguments: | ||||||
|  |    MAGNET        Magnet link to add | ||||||
|  |    DIR           Directory to save to | ||||||
|  |    TORRENT       A selected torrent | ||||||
|  |  | ||||||
|  | Options: | ||||||
|  |    -h --help     Show this screen | ||||||
|  |    --version     Show version | ||||||
|  |    --print       Print response from commands | ||||||
|  |    --json        Print response as JSON | ||||||
|  |    --debug       Print all debug log | ||||||
|  |    --warning     Print only logged warnings | ||||||
|  |    --error       Print error messages (Error/Warning) | ||||||
|  | """ | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | import signal | ||||||
|  | import logging | ||||||
|  |  | ||||||
|  | from docopt import docopt | ||||||
|  | from pprint import pprint | ||||||
|  |  | ||||||
|  | from deluge import Deluge | ||||||
|  | from utils import ColorizeFilter, BASE_DIR | ||||||
|  | from __init__ import __version__ | ||||||
|  |  | ||||||
|  | logger = logging.getLogger('deluge_cli') | ||||||
|  | logger.setLevel(logging.DEBUG) | ||||||
|  |  | ||||||
|  | fh = logging.FileHandler(os.path.join(BASE_DIR, 'deluge_cli.log')) | ||||||
|  | fh.setLevel(logging.DEBUG) | ||||||
|  | ch = logging.StreamHandler() | ||||||
|  | ch.setLevel(logging.ERROR) | ||||||
|  |  | ||||||
|  | formatter = logging.Formatter('%(asctime)s %(levelname)8s %(name)s | %(message)s') | ||||||
|  | fh.setFormatter(formatter) | ||||||
|  | logger.addHandler(fh) | ||||||
|  | logger.addHandler(ch) | ||||||
|  |  | ||||||
|  | logger.addFilter(ColorizeFilter())    | ||||||
|  |  | ||||||
|  | def signal_handler(signal, frame): | ||||||
|  |   """ | ||||||
|  |   Handle exit by Keyboardinterrupt | ||||||
|  |   """ | ||||||
|  |   logger.info('\nGood bye!') | ||||||
|  |   sys.exit(0) | ||||||
|  |  | ||||||
|  | def main(): | ||||||
|  |   """ | ||||||
|  |   Main function, parse the input | ||||||
|  |   """ | ||||||
|  |   signal.signal(signal.SIGINT, signal_handler) | ||||||
|  |  | ||||||
|  |   arguments = docopt(__doc__, version=__version__) | ||||||
|  |  | ||||||
|  |   # Set logging level for streamHandler | ||||||
|  |   if arguments['--debug']: | ||||||
|  |     ch.setLevel(logging.DEBUG) | ||||||
|  |   elif arguments['--warning']: | ||||||
|  |     ch.setLevel(logging.WARNING) | ||||||
|  |   elif arguments['--error']: | ||||||
|  |     ch.setLevel(logging.ERROR) | ||||||
|  |  | ||||||
|  |   logger.info('Deluge client') | ||||||
|  |   logger.debug(arguments) | ||||||
|  |  | ||||||
|  |   # Get config settings | ||||||
|  |   deluge = Deluge() | ||||||
|  |  | ||||||
|  |   _id = arguments['TORRENT'] | ||||||
|  |   query = arguments['NAME'] | ||||||
|  |   magnet = arguments['MAGNET'] | ||||||
|  |   name = arguments['NAME'] | ||||||
|  |   _filter = [ a[2:] for a in ['--downloading', '--seeding', '--paused'] if arguments[a] ] | ||||||
|  |  | ||||||
|  |   response = None | ||||||
|  |  | ||||||
|  |   if arguments['add']: | ||||||
|  |     logger.info('Add cmd selected with link {}'.format(magnet)) | ||||||
|  |     response = deluge.add(magnet) | ||||||
|  |  | ||||||
|  |     if response is not None: | ||||||
|  |       logger.info('Successfully added torrent.\nResponse from deluge: {}'.format(response)) | ||||||
|  |     else: | ||||||
|  |       logger.warning('Add response returned empty: {}'.format(response)) | ||||||
|  |  | ||||||
|  |   elif arguments['search']: | ||||||
|  |     logger.info('Search cmd selected for query: {}'.format(query)) | ||||||
|  |     response = deluge.search(query) | ||||||
|  |     if response is not None or response != '[]': | ||||||
|  |       logger.info('Search found {} torrents'.format(len(response))) | ||||||
|  |     else: | ||||||
|  |       logger.info('Empty response for search query.') | ||||||
|  |  | ||||||
|  |   elif arguments['progress']: | ||||||
|  |     logger.info('Progress cmd selected.') | ||||||
|  |     response = deluge.progress() | ||||||
|  |  | ||||||
|  |   elif arguments['get']: | ||||||
|  |     logger.info('Get cmd selected for id: {}'.format(_id)) | ||||||
|  |     response = deluge.get(_id) | ||||||
|  |  | ||||||
|  |   elif arguments['ls']: | ||||||
|  |     logger.info('List cmd selected') | ||||||
|  |     response = deluge.get_all(_filter=_filter) | ||||||
|  |  | ||||||
|  |   elif arguments['toggle']: | ||||||
|  |     logger.info('Toggling id: {}'.format(_id)) | ||||||
|  |     deluge.togglePaused(_id) | ||||||
|  |  | ||||||
|  |   elif arguments['rm']: | ||||||
|  |     destroy = arguments['--destroy'] | ||||||
|  |     logger.info('Remove by name: {}.'.format(name)) | ||||||
|  |  | ||||||
|  |     if destroy: | ||||||
|  |       logger.info('Destroy set, removing files') | ||||||
|  |       deluge.remove(name, destroy) | ||||||
|  |  | ||||||
|  |   try: | ||||||
|  |     if arguments['--json']: | ||||||
|  |       if len(response) > 1: | ||||||
|  |         print('[{}]'.format(','.join([t.toJSON() for t in response]))) | ||||||
|  |       else: | ||||||
|  |         print(response[0].toJSON()) | ||||||
|  |   except KeyError as error: | ||||||
|  |     logger.error('Unexpected error while trying to print') | ||||||
|  |     raise error | ||||||
|  |  | ||||||
|  |   return response | ||||||
|  |  | ||||||
|  | if __name__ == '__main__': | ||||||
|  |   main() | ||||||
							
								
								
									
										11
									
								
								delugeClient/default_config.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								delugeClient/default_config.ini
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | [deluge] | ||||||
|  | host= | ||||||
|  | port=58846 | ||||||
|  | user= | ||||||
|  | password= | ||||||
|  |  | ||||||
|  | [ssh] | ||||||
|  | host= | ||||||
|  | user= | ||||||
|  | password= | ||||||
|  | pkey= | ||||||
							
								
								
									
										167
									
								
								delugeClient/deluge.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								delugeClient/deluge.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,167 @@ | |||||||
|  | #!/usr/bin/env python3.6 | ||||||
|  |  | ||||||
|  | import os | ||||||
|  | import re | ||||||
|  | import sys | ||||||
|  | import logging | ||||||
|  | import requests | ||||||
|  | import logging.config | ||||||
|  |  | ||||||
|  | from deluge_client import DelugeRPCClient | ||||||
|  | from sshtunnel import SSHTunnelForwarder | ||||||
|  | from delugeUtils import getConfig, BASE_DIR | ||||||
|  |  | ||||||
|  | from torrent import Torrent | ||||||
|  |  | ||||||
|  | logger = logging.getLogger('deluge_cli') | ||||||
|  |  | ||||||
|  | def split_words(string): | ||||||
|  |    logger.debug('Splitting input: {} (type: {}) with split_words'.format(string, type(string))) | ||||||
|  |    return re.findall(r"[\w\d']+", string.lower()) | ||||||
|  |  | ||||||
|  | class Deluge(object): | ||||||
|  |    """docstring for ClassName""" | ||||||
|  |    def __init__(self): | ||||||
|  |       config = getConfig() | ||||||
|  |       self.host = config['deluge']['host'] | ||||||
|  |       self.port = int(config['deluge']['port']) | ||||||
|  |       self.user = config['deluge']['user'] | ||||||
|  |       self.password = config['deluge']['password'] | ||||||
|  |  | ||||||
|  |       self.ssh_host = config['ssh']['host'] | ||||||
|  |       self.ssh_user = config['ssh']['user'] | ||||||
|  |       self.ssh_pkey = config['ssh']['pkey'] | ||||||
|  |       self.ssh_password = config['ssh']['password'] | ||||||
|  |  | ||||||
|  |       self._connect() | ||||||
|  |  | ||||||
|  |    def freeSpace(self): | ||||||
|  |       return self.client.call('core.get_free_space') | ||||||
|  |  | ||||||
|  |    def parseResponse(self, response): | ||||||
|  |       torrents = [] | ||||||
|  |       for key in response: | ||||||
|  |          torrent = response[key] | ||||||
|  |          torrents.append(Torrent.fromDeluge(torrent)) | ||||||
|  |       return torrents | ||||||
|  |  | ||||||
|  |    def _connect(self): | ||||||
|  |       logger.info('Checking if script on same server as deluge RPC') | ||||||
|  |       if self.host != 'localhost' and self.host is not None: | ||||||
|  |          try: | ||||||
|  |             if self.password: | ||||||
|  |               self.tunnel = SSHTunnelForwarder(self.ssh_host, ssh_username=self.ssh_user, ssh_password=self.ssh_password,  | ||||||
|  |                  local_bind_address=('localhost', self.port), remote_bind_address=('localhost', self.port)) | ||||||
|  |             elif self.pkey is not None: | ||||||
|  |               self.tunnel = SSHTunnelForwarder(self.ssh_host, ssh_username=self.ssh_user, ssh_pkey=self.ssh_pkey,  | ||||||
|  |                  local_bind_address=('localhost', self.port), remote_bind_address=('localhost', self.port)) | ||||||
|  |          except ValueError as error: | ||||||
|  |             logger.error("Either password or private key path must be set in config.") | ||||||
|  |             raise error | ||||||
|  |  | ||||||
|  |          self.tunnel.start() | ||||||
|  |  | ||||||
|  |       self.client = DelugeRPCClient(self.host, self.port, self.user, self.password) | ||||||
|  |       self.client.connect() | ||||||
|  |  | ||||||
|  |    def add(self, url): | ||||||
|  |       logger.info('Adding magnet with url: {}.'.format(url)) | ||||||
|  |       if (url.startswith('magnet')): | ||||||
|  |          return self.client.call('core.add_torrent_magnet', url, {}) | ||||||
|  |       elif url.startswith('http'): | ||||||
|  |          magnet = self.getMagnetFromFile(url) | ||||||
|  |          return self.client.call('core.add_torrent_magnet', magnet, {}) | ||||||
|  |  | ||||||
|  |    def get_all(self, _filter=None): | ||||||
|  |       if (type(_filter) is list and len(_filter)): | ||||||
|  |          if ('seeding' in _filter): | ||||||
|  |             response = self.client.call('core.get_torrents_status', {'state': 'Seeding'}, []) | ||||||
|  |          elif ('downloading' in _filter): | ||||||
|  |             response = self.client.call('core.get_torrents_status', {'state': 'Downloading'}, []) | ||||||
|  |          elif ('paused' in _filter): | ||||||
|  |             response = self.client.call('core.get_torrents_status', {'paused': 'true'}, []) | ||||||
|  |       else: | ||||||
|  |          response = self.client.call('core.get_torrents_status', {}, []) | ||||||
|  |  | ||||||
|  |       return self.parseResponse(response) | ||||||
|  |  | ||||||
|  |    def search(self, query): | ||||||
|  |       allTorrents = self.get_all() | ||||||
|  |       torrentNamesMatchingQuery = [] | ||||||
|  |       if len(allTorrents): | ||||||
|  |          for torrent in allTorrents: | ||||||
|  |             if query in torrent.name: | ||||||
|  |                torrentNamesMatchingQuery.append(torrent) | ||||||
|  |  | ||||||
|  |          allTorrents = torrentNamesMatchingQuery | ||||||
|  |  | ||||||
|  |       return allTorrents | ||||||
|  |  | ||||||
|  |       q_list = split_words(query) | ||||||
|  |       return [ t for t in self.get_all() if (set(q_list) <= set(split_words(t.name))) ] | ||||||
|  |  | ||||||
|  |    def get(self, id): | ||||||
|  |       response = self.client.call('core.get_torrent_status', id, {}) | ||||||
|  |       return Torrent.fromDeluge(response) | ||||||
|  |  | ||||||
|  |    def togglePaused(self, id): | ||||||
|  |       torrent = self.get(id) | ||||||
|  |       if (torrent.paused): | ||||||
|  |          response = self.client.call('core.resume_torrent', [id]) | ||||||
|  |       else: | ||||||
|  |          response = self.client.call('core.pause_torrent', [id]) | ||||||
|  |       return response | ||||||
|  |  | ||||||
|  |    def remove(self, name, destroy=False): | ||||||
|  |       matches = list(filter(lambda t: t.name == name, self.get_all())) | ||||||
|  |       logger.info('Matches for {}: {}'.format(name, matches)) | ||||||
|  |        | ||||||
|  |       if (len(matches) > 1): | ||||||
|  |          raise ValueError('Multiple files found matching key. Unable to remove.') | ||||||
|  |       elif (len(matches) == 1): | ||||||
|  |          torrent = matches[0] | ||||||
|  |          response = self.client.call('core.remove_torrent', torrent.key, destroy) | ||||||
|  |          logger.info('Response: {}'.format(str(response))) | ||||||
|  |  | ||||||
|  |          if (response == False): | ||||||
|  |             raise AttributeError('Unable to remove torrent.') | ||||||
|  |          return response | ||||||
|  |       else: | ||||||
|  |          logger.error('ERROR. No torrent found with that name.') | ||||||
|  |  | ||||||
|  |    def filterOnValue(self, torrents, value): | ||||||
|  |       filteredTorrents = [] | ||||||
|  |       for t in torrents: | ||||||
|  |          value_template = {'key': None, 'name': None, value: None} | ||||||
|  |          value_template['key'] = t.key | ||||||
|  |          value_template['name'] = t.name | ||||||
|  |          value_template[value] = getattr(t, value) | ||||||
|  |           | ||||||
|  |          filteredTorrents.append(value_template) | ||||||
|  |       return filteredTorrents | ||||||
|  |  | ||||||
|  |    def progress(self): | ||||||
|  |       attributes = ['progress', 'eta', 'state', 'finished'] | ||||||
|  |       all_torrents = self.get_all() | ||||||
|  |  | ||||||
|  |       torrents = [] | ||||||
|  |       for i, attribute in enumerate(attributes): | ||||||
|  |          if i < 1: | ||||||
|  |             torrents = self.filterOnValue(all_torrents, attribute) | ||||||
|  |             continue | ||||||
|  |          torrents = [dict(e, **v) for e,v in zip(torrents, self.filterOnValue(all_torrents, attribute))] | ||||||
|  |  | ||||||
|  |       return torrents | ||||||
|  |  | ||||||
|  |    def __del__(self): | ||||||
|  |       if hasattr(self, 'tunnel'): | ||||||
|  |          logger.info('Closing ssh tunnel') | ||||||
|  |          self.tunnel.stop() | ||||||
|  |  | ||||||
|  |    def getMagnetFromFile(self, url): | ||||||
|  |       logger.info('File url found, fetching magnet.') | ||||||
|  |       r = requests.get(url, allow_redirects=False) | ||||||
|  |       magnet = r.headers['Location'] | ||||||
|  |       logger.info('Found magnet: {}.'.format(magnet)) | ||||||
|  |       return magnet | ||||||
|  |  | ||||||
							
								
								
									
										81
									
								
								delugeClient/delugeUtils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								delugeClient/delugeUtils.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | |||||||
|  | #!/usr/bin/env python3 | ||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | # @Author: kevinmidboe | ||||||
|  | # @Date:   2018-04-17 19:55:38 | ||||||
|  | # @Last Modified by:   KevinMidboe | ||||||
|  | # @Last Modified time: 2018-05-04 00:04:25 | ||||||
|  |  | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | import json | ||||||
|  | import shutil | ||||||
|  | import logging | ||||||
|  | import colored | ||||||
|  | import configparser | ||||||
|  | from pprint import pprint | ||||||
|  |  | ||||||
|  | from colored import stylize | ||||||
|  |  | ||||||
|  | __all__ = ('ColorizeFilter', ) | ||||||
|  |  | ||||||
|  | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | ||||||
|  | logger = logging.getLogger('deluge_cli') | ||||||
|  |  | ||||||
|  | def checkConfigExists(): | ||||||
|  |   user_config_dir = os.path.expanduser("~") + "/.config/delugeClient" | ||||||
|  |   config_dir = os.path.join(user_config_dir, 'config.ini') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def getConfig(): | ||||||
|  |   """ | ||||||
|  |   Read path and get configuartion file with site settings | ||||||
|  |   :return: config settings read from 'config.ini' | ||||||
|  |   :rtype: configparser.ConfigParser | ||||||
|  |   """ | ||||||
|  |   config = configparser.ConfigParser() | ||||||
|  |   user_config_dir = os.path.expanduser("~") + "/.config/delugeClient" | ||||||
|  |  | ||||||
|  |   config_dir = os.path.join(user_config_dir, 'config.ini') | ||||||
|  |   if not os.path.isfile(config_dir): | ||||||
|  |     defaultConfig = os.path.join(BASE_DIR, 'default_config.ini') | ||||||
|  |     logger.error('Missing config! Moved default.config.ini to {}.\nOpen this file and set all varaibles!'.format(config_dir)) | ||||||
|  |     os.makedirs(user_config_dir, exist_ok=True) | ||||||
|  |     shutil.copyfile(defaultConfig, config_dir) | ||||||
|  |  | ||||||
|  |   config.read(config_dir) | ||||||
|  |  | ||||||
|  |   requiredParameters = [('deluge host', config['deluge']['host']), ('deluge port', config['deluge']['port']), | ||||||
|  |     ('deluge user', config['deluge']['user']), ('deluge password', config['deluge']['password']), | ||||||
|  |     ('ssh password', config['ssh']['user'])] | ||||||
|  |   for key, value in requiredParameters: | ||||||
|  |     if value == '': | ||||||
|  |       logger.error('Missing value for variable: "{}" in config: \ | ||||||
|  | "$HOME/.config/delugeClient/config.ini".'.format(key)) | ||||||
|  |       exit(1) | ||||||
|  |  | ||||||
|  |   return config | ||||||
|  |  | ||||||
|  | class ColorizeFilter(logging.Filter): | ||||||
|  |   """ | ||||||
|  |   Class for setting specific colors to levels of severity for log output | ||||||
|  |   """ | ||||||
|  |   color_by_level = { | ||||||
|  |     10: 'chartreuse_3b', | ||||||
|  |     20: 'white', | ||||||
|  |     30: 'orange_1', | ||||||
|  |     40: 'red' | ||||||
|  |   } | ||||||
|  |     | ||||||
|  |   def filter(self, record): | ||||||
|  |     record.raw_msg = record.msg | ||||||
|  |     color = self.color_by_level.get(record.levelno) | ||||||
|  |     if color: | ||||||
|  |       record.msg = stylize(record.msg, colored.fg(color)) | ||||||
|  |     return True | ||||||
|  |  | ||||||
|  | def convert(data): | ||||||
|  |   if isinstance(data, bytes):  return data.decode('utf-8') | ||||||
|  |   if isinstance(data, dict):   return dict(map(convert, data.items())) | ||||||
|  |   if isinstance(data, tuple):  return map(convert, data) | ||||||
|  |   json_data = json.dumps(data) | ||||||
|  |   return json_data | ||||||
							
								
								
									
										48
									
								
								delugeClient/torrent.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								delugeClient/torrent.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | |||||||
|  | import json | ||||||
|  | import logging | ||||||
|  | from distutils.util import strtobool | ||||||
|  |  | ||||||
|  | from utils import convert | ||||||
|  |  | ||||||
|  | logger = logging.getLogger('deluge_cli') | ||||||
|  |  | ||||||
|  | class Torrent(object): | ||||||
|  |   def __init__(self, key, name, progress, eta, save_path, state, paused, finished, files): | ||||||
|  |     super(Torrent, self).__init__() | ||||||
|  |     self.key = key | ||||||
|  |     self.name = name | ||||||
|  |     self.progress = "{0:.2f}".format(float(progress)) | ||||||
|  |     self.eta = eta | ||||||
|  |     self.save_path = save_path | ||||||
|  |     self.state = state | ||||||
|  |     self.paused = paused | ||||||
|  |     self.finished = finished | ||||||
|  |     self.files = list(files) | ||||||
|  |  | ||||||
|  |   def isFolder(self): | ||||||
|  |     return len(self.files) > 1 | ||||||
|  |  | ||||||
|  |   def toBool(self, value): | ||||||
|  |     return True if strtobool(value) else False | ||||||
|  |  | ||||||
|  |   @classmethod | ||||||
|  |   def fromDeluge(cls, d): | ||||||
|  |     # Receive a dict with byte values, convert all elements to string values | ||||||
|  |     d = convert(d) | ||||||
|  |     toBool = lambda val: True if strtobool(val) else False | ||||||
|  |     return cls(d['hash'], d['name'], d['progress'], d['eta'], d['save_path'], d['state'],  | ||||||
|  |       toBool(d['paused']), toBool(d['is_finished']), d['files']) | ||||||
|  |  | ||||||
|  |   def toJSON(self, files=False): | ||||||
|  |     torrentDict = {'key': self.key, 'name': self.name, 'progress': self.progress, 'eta': self.eta, | ||||||
|  |       'save_path': self.save_path, 'state': self.state, 'paused': self.paused, | ||||||
|  |       'finished': self.finished, 'files': self.files, 'is_folder': self.isFolder()} | ||||||
|  |  | ||||||
|  |     if (files is False): | ||||||
|  |       del torrentDict['files'] | ||||||
|  |  | ||||||
|  |     return json.dumps(torrentDict) | ||||||
|  |  | ||||||
|  |   def __str__(self): | ||||||
|  |     return "Name: {}, Progress: {}%, ETA: {}, State: {}, Paused: {}".format( | ||||||
|  |       self.name, self.progress, self.eta, self.state, self.paused) | ||||||
							
								
								
									
										309
									
								
								deluge_cli.py
									
									
									
									
									
								
							
							
						
						
									
										309
									
								
								deluge_cli.py
									
									
									
									
									
								
							| @@ -1,309 +0,0 @@ | |||||||
| #!/usr/bin/env python3.6 |  | ||||||
|  |  | ||||||
|  |  | ||||||
| """Custom delugeRPC client |  | ||||||
| Usage: |  | ||||||
|    deluge_cli add MAGNET [DIR] [--debug | --warning | --error] |  | ||||||
|    deluge_cli search NAME |  | ||||||
|    deluge_cli get  TORRENT |  | ||||||
|    deluge_cli ls [--downloading | --seeding | --paused] |  | ||||||
|    deluge_cli toggle TORRENT |  | ||||||
|    deluge_cli progress |  | ||||||
|    deluge_cli rm NAME [--destroy] [--debug | --warning | --error] |  | ||||||
|    deluge_cli (-h | --help) |  | ||||||
|    deluge_cli --version |  | ||||||
|  |  | ||||||
| Arguments: |  | ||||||
|    MAGNET        Magnet link to add |  | ||||||
|    DIR           Directory to save to |  | ||||||
|    TORRENT       A selected torrent |  | ||||||
|  |  | ||||||
| Options: |  | ||||||
|    -h --help     Show this screen |  | ||||||
|    --version     Show version |  | ||||||
|    --debug       Print all debug log |  | ||||||
|    --warning     Print only logged warnings |  | ||||||
|    --error       Print error messages (Error/Warning) |  | ||||||
| """ |  | ||||||
|  |  | ||||||
| import argparse |  | ||||||
| import os |  | ||||||
| import sys |  | ||||||
| import re |  | ||||||
| import signal |  | ||||||
| import socket |  | ||||||
| import logging |  | ||||||
| import logging.config |  | ||||||
| import configparser |  | ||||||
|  |  | ||||||
| from distutils.util import strtobool |  | ||||||
| from pprint import pprint |  | ||||||
|  |  | ||||||
| from deluge_client import DelugeRPCClient |  | ||||||
| from sshtunnel import SSHTunnelForwarder |  | ||||||
| from docopt import docopt |  | ||||||
| from utils import ColorizeFilter, convert |  | ||||||
|  |  | ||||||
| BASE_DIR = os.path.dirname(os.path.abspath(__file__)) |  | ||||||
|  |  | ||||||
| logger = logging.getLogger('deluge_cli') |  | ||||||
| logger.setLevel(logging.DEBUG) |  | ||||||
|  |  | ||||||
| fh = logging.FileHandler(os.path.join(BASE_DIR, 'deluge_cli.log')) |  | ||||||
| fh.setLevel(logging.DEBUG) |  | ||||||
| ch = logging.StreamHandler() |  | ||||||
| ch.setLevel(logging.ERROR) |  | ||||||
|  |  | ||||||
| formatter = logging.Formatter('%(asctime)s %(levelname)8s %(name)s | %(message)s') |  | ||||||
| fh.setFormatter(formatter) |  | ||||||
| logger.addHandler(fh) |  | ||||||
| logger.addHandler(ch) |  | ||||||
|  |  | ||||||
| logger.addFilter(ColorizeFilter())    |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def getConfig(): |  | ||||||
|    """ |  | ||||||
|    Read path and get configuartion file with site settings |  | ||||||
|    :return: config settings read from 'config.ini' |  | ||||||
|    :rtype: configparser.ConfigParser |  | ||||||
|    """ |  | ||||||
|    config = configparser.ConfigParser() |  | ||||||
|    config_dir = os.path.join(BASE_DIR, 'config.ini') |  | ||||||
|    config.read(config_dir) |  | ||||||
|  |  | ||||||
|    config_values = list(dict(config.items('Deluge')).values()) |  | ||||||
|    config_values.extend(list(dict(config.items('ssh')).values())) |  | ||||||
|  |  | ||||||
|    if any(value.startswith('YOUR') for value in config_values): |  | ||||||
|       raise ValueError('Please set variables in config.ini file.') |  | ||||||
|  |  | ||||||
|    return config |  | ||||||
|  |  | ||||||
| def split_words(string): |  | ||||||
|    logger.debug('Splitting input: {} (type: {}) with split_words'.format(string, type(string))) |  | ||||||
|    return re.findall(r"[\w\d']+", string.lower()) |  | ||||||
|  |  | ||||||
| class Deluge(object): |  | ||||||
|    """docstring for ClassName""" |  | ||||||
|    def __init__(self): |  | ||||||
|       config = getConfig() |  | ||||||
|       self.host = config['Deluge']['HOST'] |  | ||||||
|       self.port = int(config['Deluge']['PORT']) |  | ||||||
|       self.user = config['Deluge']['USER'] |  | ||||||
|       self.password = config['Deluge']['PASSWORD'] |  | ||||||
|  |  | ||||||
|       self.ssh_host = config['ssh']['HOST'] |  | ||||||
|       self.ssh_user = config['ssh']['USER'] |  | ||||||
|       self.ssh_pkey = config['ssh']['PKEY'] |  | ||||||
|     |  | ||||||
|       self._connect() |  | ||||||
|  |  | ||||||
|    def parseResponse(self, response): |  | ||||||
|       torrents = [] |  | ||||||
|       for key in response: |  | ||||||
|          torrent = response[key] |  | ||||||
|          torrents.append(Torrent.fromDeluge(torrent)) |  | ||||||
|       return torrents |  | ||||||
|  |  | ||||||
|    def _connect(self): |  | ||||||
|       logger.info('Checking if script on same server as deluge RPC') |  | ||||||
|       if (socket.gethostbyname(socket.gethostname()) != self.host): |  | ||||||
|          self.tunnel = SSHTunnelForwarder(self.ssh_host, ssh_username=self.ssh_user, ssh_pkey=self.ssh_pkey,  |  | ||||||
|             local_bind_address=('localhost', self.port), remote_bind_address=('localhost', self.port)) |  | ||||||
|          self.tunnel.start() |  | ||||||
|  |  | ||||||
|       self.client = DelugeRPCClient(self.host, self.port, self.user, self.password) |  | ||||||
|       self.client.connect() |  | ||||||
|  |  | ||||||
|    def add(self, url): |  | ||||||
|       if (url.startswith('magnet')): |  | ||||||
|          return self.client.call('core.add_torrent_magnet', url, {}) |  | ||||||
|  |  | ||||||
|    def get_all(self, _filter=None): |  | ||||||
|       if (type(_filter) is list and len(_filter)): |  | ||||||
|          if ('seeding' in _filter): |  | ||||||
|             response = self.client.call('core.get_torrents_status', {'state': 'Seeding'}, []) |  | ||||||
|          elif ('downloading' in _filter): |  | ||||||
|             response = self.client.call('core.get_torrents_status', {'state': 'Downloading'}, []) |  | ||||||
|          elif ('paused' in _filter): |  | ||||||
|             response = self.client.call('core.get_torrents_status', {'paused': 'true'}, []) |  | ||||||
|       else: |  | ||||||
|          response = self.client.call('core.get_torrents_status', {}, []) |  | ||||||
|  |  | ||||||
|       return self.parseResponse(response) |  | ||||||
|  |  | ||||||
|    def search(self, query): |  | ||||||
|       q_list = split_words(query) |  | ||||||
|       return [ t for t in self.get_all() if (set(q_list) <= set(split_words(t.name))) ] |  | ||||||
|  |  | ||||||
|    def get(self, id): |  | ||||||
|       response = self.client.call('core.get_torrent_status', id, {}) |  | ||||||
|       return Torrent.fromDeluge(response) |  | ||||||
|  |  | ||||||
|    def togglePaused(self, id): |  | ||||||
|       torrent = self.get(id) |  | ||||||
|       if (torrent.paused): |  | ||||||
|          response = self.client.call('core.resume_torrent', [id]) |  | ||||||
|       else: |  | ||||||
|          response = self.client.call('core.pause_torrent', [id]) |  | ||||||
|        |  | ||||||
|       print('Response:', response) |  | ||||||
|  |  | ||||||
|    def remove(self, name, destroy=False): |  | ||||||
|       matches = list(filter(lambda t: t.name == name, self.get_all())) |  | ||||||
|       logger.info('Matches for {}: {}'.format(name, matches)) |  | ||||||
|        |  | ||||||
|       if (len(matches) > 1): |  | ||||||
|          raise ValueError('Multiple files found matching key. Unable to remove.') |  | ||||||
|       elif (len(matches) == 1): |  | ||||||
|          torrent = matches[0] |  | ||||||
|          response = self.client.call('core.remove_torrent', torrent.key, destroy) |  | ||||||
|          logger.info('Response: {}'.format(str(response))) |  | ||||||
|  |  | ||||||
|          if (response == False): |  | ||||||
|             raise AttributeError('Unable to remove torrent.') |  | ||||||
|          return response |  | ||||||
|       else: |  | ||||||
|          logger.error('ERROR. No torrent found with that name.') |  | ||||||
|  |  | ||||||
|    def filterOnValue(self, torrents, value): |  | ||||||
|       filteredTorrents = [] |  | ||||||
|       for t in torrents: |  | ||||||
|          value_template = {'key': None, 'name': None, value: None} |  | ||||||
|          value_template['key'] = t.key |  | ||||||
|          value_template['name'] = t.name |  | ||||||
|          value_template[value] = getattr(t, value) |  | ||||||
|           |  | ||||||
|          filteredTorrents.append(value_template) |  | ||||||
|       return filteredTorrents |  | ||||||
|  |  | ||||||
|    def progress(self): |  | ||||||
|       attributes = ['progress', 'eta', 'state', 'finished'] |  | ||||||
|       all_torrents = self.get_all() |  | ||||||
|  |  | ||||||
|       torrents = [] |  | ||||||
|       for i, attribute in enumerate(attributes): |  | ||||||
|          if i < 1: |  | ||||||
|             torrents = self.filterOnValue(all_torrents, attribute) |  | ||||||
|             continue |  | ||||||
|          torrents = [dict(e, **v) for e,v in zip(torrents, self.filterOnValue(all_torrents, attribute))] |  | ||||||
|  |  | ||||||
|       return torrents |  | ||||||
|  |  | ||||||
|    def __del__(self): |  | ||||||
|       if hasattr(self, 'tunnel'): |  | ||||||
|          logger.info('Closing ssh tunnel') |  | ||||||
|          self.tunnel.stop() |  | ||||||
|  |  | ||||||
| class Torrent(object): |  | ||||||
|    def __init__(self, key, name, progress, eta, save_path, state, paused, finished, files): |  | ||||||
|       super(Torrent, self).__init__() |  | ||||||
|       self.key = key |  | ||||||
|       self.name = name |  | ||||||
|       self.progress = "{0:.2f}".format(float(progress)) |  | ||||||
|       self.eta = eta |  | ||||||
|       self.save_path = save_path |  | ||||||
|       self.state = state |  | ||||||
|       self.paused = paused |  | ||||||
|       self.finished = finished |  | ||||||
|       self.files = list(files) |  | ||||||
|  |  | ||||||
|    def isFolder(self): |  | ||||||
|       return len(self.files) > 1 |  | ||||||
|  |  | ||||||
|    def toBool(self, value): |  | ||||||
|       return True if strtobool(value) else False |  | ||||||
|  |  | ||||||
|    @classmethod |  | ||||||
|    def fromDeluge(cls, d): |  | ||||||
|       # Receive a dict with byte values, convert all elements to string values |  | ||||||
|       d = convert(d) |  | ||||||
|       toBool = lambda val: True if strtobool(val) else False |  | ||||||
|       return cls(d['hash'], d['name'], d['progress'], d['eta'], d['save_path'], d['state'],  |  | ||||||
|                  toBool(d['paused']), toBool(d['is_finished']), d['files']) |  | ||||||
|  |  | ||||||
|    def toJSON(self): |  | ||||||
|       return {'key': self.key, 'name': self.name, 'progress': self.progress, 'eta': self.eta, |  | ||||||
|               'save_path': self.save_path, 'state': self.state, 'paused': self.paused, |  | ||||||
|               'finished': self.finished, 'files': self.files, 'is_folder': self.isFolder()} |  | ||||||
|  |  | ||||||
|    def __str__(self): |  | ||||||
|       return "Name: {}, Progress: {}%, ETA: {}, State: {}, Paused: {}".format( |  | ||||||
|          self.name, self.progress, self.eta, self.state, self.paused) |  | ||||||
|  |  | ||||||
| def signal_handler(signal, frame): |  | ||||||
|    """ |  | ||||||
|    Handle exit by Keyboardinterrupt |  | ||||||
|    """ |  | ||||||
|    logger.info('\nGood bye!') |  | ||||||
|    sys.exit(0) |  | ||||||
|  |  | ||||||
| def main(): |  | ||||||
|    """ |  | ||||||
|    Main function, parse the input |  | ||||||
|    """ |  | ||||||
|    signal.signal(signal.SIGINT, signal_handler) |  | ||||||
|  |  | ||||||
|    arguments = docopt(__doc__, version='1') |  | ||||||
|  |  | ||||||
|    # Set logging level for streamHandler |  | ||||||
|    if arguments['--debug']: |  | ||||||
|       ch.setLevel(logging.DEBUG) |  | ||||||
|    elif arguments['--warning']: |  | ||||||
|       ch.setLevel(logging.WARNING) |  | ||||||
|    elif arguments['--error']: |  | ||||||
|       ch.setLevel(logging.ERROR) |  | ||||||
|  |  | ||||||
|    logger.info('Deluge client') |  | ||||||
|    logger.debug(arguments) |  | ||||||
|  |  | ||||||
|    # Get config settings |  | ||||||
|    deluge = Deluge() |  | ||||||
|  |  | ||||||
|    _id = arguments['TORRENT'] |  | ||||||
|    query = arguments['NAME'] |  | ||||||
|    magnet = arguments['MAGNET'] |  | ||||||
|    name = arguments['NAME'] |  | ||||||
|    _filter = [ a[2:] for a in ['--downloading', '--seeding', '--paused'] if arguments[a] ] |  | ||||||
|    print(_id, query, _filter) |  | ||||||
|  |  | ||||||
|    if arguments['add']: |  | ||||||
|       logger.info('Add cmd selected with link {}'.format(magnet)) |  | ||||||
|       response = deluge.add(magnet) |  | ||||||
|       print('Add response: ', response) |  | ||||||
|  |  | ||||||
|    elif arguments['search']: |  | ||||||
|       logger.info('Search cmd selected for query: {}'.format(query)) |  | ||||||
|       response = deluge.search(query) |  | ||||||
|       [ pprint(t.toJSON()) for t in response ] |  | ||||||
|  |  | ||||||
|    elif arguments['progress']: |  | ||||||
|       logger.info('Progress cmd selected.') |  | ||||||
|       pprint(deluge.progress()) |  | ||||||
|       exit(0) |  | ||||||
|       [ pprint(t.toJSON()) for t in deluge.progress() ] |  | ||||||
|  |  | ||||||
|    elif arguments['get']: |  | ||||||
|       logger.info('Get cmd selected for id: {}'.format(_id)) |  | ||||||
|       response = deluge.get(_id) |  | ||||||
|       pprint(response.toJSON()) |  | ||||||
|  |  | ||||||
|    elif arguments['ls']: |  | ||||||
|       logger.info('List cmd selected') |  | ||||||
|       [ pprint(t.toJSON()) for t in deluge.get_all(_filter=_filter) ] |  | ||||||
|  |  | ||||||
|    elif arguments['toggle']: |  | ||||||
|       logger.info('Toggling id: {}'.format(_id)) |  | ||||||
|       deluge.togglePaused(_id) |  | ||||||
|  |  | ||||||
|    elif arguments['rm']: |  | ||||||
|       destroy = arguments['--destroy'] |  | ||||||
|       logger.info('Remove by name: {}.'.format(name)) |  | ||||||
|       if destroy: |  | ||||||
|          logger.info('Destroy set, removing files') |  | ||||||
|       deluge.remove(name, destroy) |  | ||||||
|  |  | ||||||
| if __name__ == '__main__': |  | ||||||
|    main() |  | ||||||
|  |  | ||||||
							
								
								
									
										16
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								setup.py
									
									
									
									
									
								
							| @@ -1,10 +1,14 @@ | |||||||
| import setuptools | from setuptools import setup, find_packages | ||||||
|  | from sys import path | ||||||
|  | from os.path import dirname | ||||||
|  |  | ||||||
|  | path.append(dirname(__file__)) | ||||||
| import delugeClient | import delugeClient | ||||||
|  |  | ||||||
| with open("README.md", "r", encoding="utf-8") as fh: | with open("README.md", "r", encoding="utf-8") as fh: | ||||||
|   long_description = fh.read() |   long_description = fh.read() | ||||||
|  |  | ||||||
| setuptools.setup( | setup( | ||||||
|   name="delugeClient", |   name="delugeClient", | ||||||
|   version=delugeClient.__version__, |   version=delugeClient.__version__, | ||||||
|   author="KevinMidboe", |   author="KevinMidboe", | ||||||
| @@ -27,10 +31,12 @@ setuptools.setup( | |||||||
|   ], |   ], | ||||||
|   entry_points={ |   entry_points={ | ||||||
|     'console_scripts': [ |     'console_scripts': [ | ||||||
|       'delugeClient = delugeClient.__main__:main', |       'delugeclient = delugeClient.__main__:main', | ||||||
|    ], |    ], | ||||||
|   }, |   }, | ||||||
|   package_dir={"": "delugeClient"}, |   packages=find_packages(), | ||||||
|   packages=setuptools.find_packages(where="delugeClient"), |   package_data={ | ||||||
|  |     'delugeClient': ['default_config.ini'], | ||||||
|  |   }, | ||||||
|   python_requires=">=3.6", |   python_requires=">=3.6", | ||||||
| ) | ) | ||||||
							
								
								
									
										42
									
								
								utils.py
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								utils.py
									
									
									
									
									
								
							| @@ -1,42 +0,0 @@ | |||||||
| #!/usr/bin/env python3 |  | ||||||
| # -*- coding: utf-8 -*- |  | ||||||
| # @Author: kevinmidboe |  | ||||||
| # @Date:   2018-04-17 19:55:38 |  | ||||||
| # @Last Modified by:   KevinMidboe |  | ||||||
| # @Last Modified time: 2018-05-04 00:04:25 |  | ||||||
|  |  | ||||||
| import logging |  | ||||||
| import colored |  | ||||||
| import json |  | ||||||
| from pprint import pprint |  | ||||||
|  |  | ||||||
| from colored import stylize |  | ||||||
|  |  | ||||||
| __all__ = ('ColorizeFilter', ) |  | ||||||
|  |  | ||||||
| class ColorizeFilter(logging.Filter): |  | ||||||
|    """ |  | ||||||
|    Class for setting specific colors to levels of severity for log output |  | ||||||
|    """ |  | ||||||
|    color_by_level = { |  | ||||||
|       10: 'chartreuse_3b', |  | ||||||
|       20: 'white', |  | ||||||
|       30: 'orange_1', |  | ||||||
|       40: 'red' |  | ||||||
|    } |  | ||||||
|     |  | ||||||
|    logger = logging.getLogger('deluge_cli') |  | ||||||
|  |  | ||||||
|    def filter(self, record): |  | ||||||
|       record.raw_msg = record.msg |  | ||||||
|       color = self.color_by_level.get(record.levelno) |  | ||||||
|       if color: |  | ||||||
|          record.msg = stylize(record.msg, colored.fg(color)) |  | ||||||
|       return True |  | ||||||
|  |  | ||||||
| def convert(data): |  | ||||||
|     if isinstance(data, bytes):  return data.decode('utf-8') |  | ||||||
|     if isinstance(data, dict):   return dict(map(convert, data.items())) |  | ||||||
|     if isinstance(data, tuple):  return map(convert, data) |  | ||||||
|     json_data = json.dumps(data) |  | ||||||
|     return json_data |  | ||||||
		Reference in New Issue
	
	Block a user