#!/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) --destroy When removing choose to remove file on disk """ 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, destroyFiles=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, destroyFiles) 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: {}. Destroy files: {}'.format(name, destroy)) deluge.remove(name, destroy) if __name__ == '__main__': main()