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 | ||||
|  | ||||
| with open("README.md", "r", encoding="utf-8") as fh: | ||||
|   long_description = fh.read() | ||||
|  | ||||
| setuptools.setup( | ||||
| setup( | ||||
|   name="delugeClient", | ||||
|   version=delugeClient.__version__, | ||||
|   author="KevinMidboe", | ||||
| @@ -27,10 +31,12 @@ setuptools.setup( | ||||
|   ], | ||||
|   entry_points={ | ||||
|     'console_scripts': [ | ||||
|       'delugeClient = delugeClient.__main__:main', | ||||
|       'delugeclient = delugeClient.__main__:main', | ||||
|    ], | ||||
|   }, | ||||
|   package_dir={"": "delugeClient"}, | ||||
|   packages=setuptools.find_packages(where="delugeClient"), | ||||
|   packages=find_packages(), | ||||
|   package_data={ | ||||
|     'delugeClient': ['default_config.ini'], | ||||
|   }, | ||||
|   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