9 Commits

19 changed files with 411 additions and 678 deletions

View File

@@ -1,14 +0,0 @@
---
kind: pipeline
type: docker
name: delugeClient
platform:
os: linux
arch: amd64
steps:
- name: Build package
image: python:3.10
commands:
- make build

2
.gitignore vendored
View File

@@ -1,5 +1,3 @@
deluge_cli.log
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]

View File

@@ -1,17 +0,0 @@
.PHONY: clean
binaries=dist build
install:
python3 setup.py install
build:
python3 setup.py build
dist:
python3 setup.py sdist
upload: clean dist
twine upload dist/*
clean:
rm -rf $(binaries)

View File

@@ -4,15 +4,15 @@
<h4 align="center"> A easy to use Deluge CLI that can connect to Deluge RPC (even over ssh) written entirely in python.</h4>
| Tested version | PyPi package | Drone CI |
|:--------|:------|:------|
| [![PyVersion](https://img.shields.io/badge/python-3.10-blue.svg)](https://www.python.org/downloads/release/python-3100/) | [![PyPI](https://img.shields.io/pypi/v/delugeClient_kevin)](https://pypi.org/project/delugeClient_kevin/) | [![Build Status](https://drone.schleppe.cloud/api/badges/KevinMidboe/delugeClient/status.svg)](https://drone.schleppe.cloud/KevinMidboe/delugeClient)
| Known vulnerabilities | License |
|:--------|:------|
| [![Known Vulnerabilities](https://snyk.io/test/github/kevinmidboe/delugeClient/badge.svg?targetFile=requirements.txt)](https://snyk.io/test/github/kevinmidboe/delugeClient?targetFile=requirements.txt) |[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
<p align="center">
<a href="https://snyk.io/test/github/kevinmidboe/delugeclient?targetFile=requirements.txt">
<img src="https://snyk.io/test/github/kevinmidboe/delugeclient/badge.svg?targetFile=requirements.txt" alt="Known Vulnerabilities" data-canonical-src="https://snyk.io/test/github/kevinmidboe/delugeclient?targetFile=requirements.txt" style="max-width:100%;">
</a>
<a href="https://opensource.org/licenses/MIT">
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="">
</a>
</p>
<p align="center">
<a href="#abstract">Abstract</a>
@@ -52,10 +52,10 @@ After you have downloaded this project go to it in your terminal by going to the
The to setup a virtual environment enter this:
```
$ virtualenv -p python3.10 env
$ virtualenv -p python3.6 env
```
> If you get an error now it might be because you don't have python3.10, please make sure you have python version 3.10 if else you can download it from [here](https://www.python.org/downloads/)
> If you get an error now it might be because you don't have python3.6, please make sure you have python version 3.6 if else you can download it from [here](https://www.python.org/downloads/)
First we navigate to the folder we downloaded.
@@ -146,4 +146,4 @@ To interface with deluged :
- Create your feature branch: git checkout -b my-new-feature
- Commit your changes: git commit -am 'Add some feature'
- Push to the branch: git push origin my-new-feature
- Submit a pull request
- Submit a pull request

10
config.ini Normal file
View File

@@ -0,0 +1,10 @@
[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

View File

@@ -1,22 +0,0 @@
#!/usr/bin/env python3.10
# -*- encoding: utf-8 -*-
from sys import path
from os.path import dirname, join
path.append(dirname(__file__))
import logging
from utils import BASE_DIR
def addHandler(handler):
handler.setFormatter(formatter)
logger.addHandler(handler)
logger = logging.getLogger('deluge_cli')
logger.setLevel(logging.DEBUG)
fh = logging.FileHandler(join(BASE_DIR, 'deluge_cli.log'))
formatter = logging.Formatter('%(asctime)s| %(levelname)s | %(message)s')
addHandler(fh)

View File

@@ -1,177 +0,0 @@
#!/usr/bin/env python3.10
import os
import sys
import signal
import logging
import typer
from pprint import pprint
from deluge import Deluge
from utils import ColorizeFilter, BASE_DIR, validHash, convertFilesize
from __version__ import __version__
from __init__ import addHandler
ch = logging.StreamHandler()
ch.addFilter(ColorizeFilter())
addHandler(ch)
logger = logging.getLogger('deluge_cli')
app = typer.Typer()
deluge = None
def signal_handler(signal, frame):
"""
Handle exit by Keyboardinterrupt
"""
global deluge
del deluge
logger.info('\nGood bye!')
sys.exit(1)
def handleKeyboardInterrupt():
signal.signal(signal.SIGINT, signal_handler)
def printResponse(response, json=False):
try:
if json:
if isinstance(response, list):
print('[{}]'.format(','.join([t.toJSON() for t in response])))
else:
print(response.toJSON())
elif isinstance(response, list):
for el in response:
print(el)
elif response:
print(response)
except KeyError as error:
logger.error('Unexpected error while trying to print')
raise error
@app.command()
def add(magnet: str, json: bool = typer.Option(False, help="Print as json")):
'''
Add magnet torrent
'''
logger.info('Add command selected')
logger.debug(magnet)
response = deluge.add(magnet)
if validHash(response):
torrent = deluge.get(response)
printResponse(torrent, json)
else:
logger.info('Unable to add torrent')
@app.command()
def ls(json: bool = typer.Option(False, help="Print as json")):
'''
List all torrents
'''
logger.info('List command selected')
response = deluge.get_all()
if response is None:
logger.info('No torrents found')
return
printResponse(response, json)
@app.command()
def get(id: str, json: bool = typer.Option(False, help="Print as json")):
'''
Get torrent by id or hash
'''
logger.info('Get command selected for id: {}'.format(id))
if not validHash(id):
return logger.info("Id is not valid")
response = deluge.get(id)
printResponse(response, json)
@app.command()
def toggle(id: str):
'''
Toggle torrent download state
'''
logger.info('Toggle command selected for id: {}'.format(id))
if not validHash(id):
return logger.info("Id is not valid")
deluge.toggle(id)
torrent = deluge.get(id)
printResponse(torrent)
@app.command()
def search(query: str, json: bool = typer.Option(False, help="Print as json")):
'''
Search for string segment in torrent name
'''
logger.info('Search command selected with query: {}'.format(query))
response = deluge.search(query)
printResponse(response, json)
@app.command()
def rm(name: str, destroy: bool = typer.Option(False, help="Remove torrent by name")):
'''
Remove torrent by name
'''
logger.info('Removing torrent with name: {}, destroy flag: {}'.format(name, destroy))
response = deluge.removeByName(name, destroy)
@app.command()
def remove(id: str, destroy: bool = typer.Option(False, help="Remove torrent by id")):
'''
Remove torrent by id or hash
'''
logger.info('Removing torrent with id: {}, destroy flag: {}'.format(id, destroy))
if not validHash(id):
return logger.info("Id is not valid")
response = deluge.remove(id, destroy)
@app.command()
def disk():
'''
Get free disk space
'''
response = deluge.freeSpace()
if response == None or not isinstance(response, int):
logger.error("Unable to get available disk space")
return
print(convertFilesize(response))
@app.command()
def version():
'''
Print package version
'''
print(__version__)
# Runs before any command
@app.callback()
def defaultOptions(debug: bool = typer.Option(False, '--debug', help='Set log level to debug'), info: bool = typer.Option(False, '--info', help='Set log level to info'), warning: bool = typer.Option(False, '--warning', help='Set log level to warning'), error: bool = typer.Option(False, '--error', help='Set log level to error')):
ch.setLevel(logging.INFO)
if '--json' in sys.argv:
ch.setLevel(logging.CRITICAL)
elif error == True:
ch.setLevel(logging.ERROR)
elif warning == True:
ch.setLevel(logging.WARNING)
elif info == True:
ch.setLevel(logging.INFO)
elif debug == True:
ch.setLevel(logging.DEBUG)
# Initiate deluge
global deluge
deluge = Deluge()
def main():
app()
del deluge
if __name__ == '__main__':
handleKeyboardInterrupt()
main()

View File

@@ -1 +0,0 @@
__version__ = '0.3.1'

View File

@@ -1,11 +0,0 @@
[deluge]
host=
port=58846
user=
password=
[ssh]
host=
user=
password=
pkey=

View File

@@ -1,220 +0,0 @@
#!/usr/bin/env python3.10
import os
import re
import sys
import logging
import requests
import logging.config
from deluge_client import DelugeRPCClient, FailedToReconnectException
from sshtunnel import SSHTunnelForwarder, BaseSSHTunnelForwarderError
from utils 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())
def responseToString(response=None):
try:
response = response.decode('utf-8')
except (UnicodeDecodeError, AttributeError):
pass
return response
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']
try:
self._connect()
except FailedToReconnectException:
logger.error("Unable to connect to deluge, make sure it's running")
sys.exit(1)
except ConnectionRefusedError:
logger.error("Unable to connect to deluge, make sure it's running")
sys.exit(1)
except BaseException as error:
logger.error("Unable to connect to deluge, make sure it's running")
if 'nodename nor servname provided' in str(error):
sys.exit(1)
raise error
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 establishSSHTunnel(self):
logger.debug('Checking if script on same server as deluge RPC')
if self.password is not None:
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))
else:
logger.error("Either password or private key path must be set in config.")
return
try:
self.tunnel.start()
except BaseSSHTunnelForwarderError as sshError:
logger.warning("SSH host {} online, check your connection".format(self.ssh_host))
return
def _call(self, command, *args):
try:
return self.client.call(command, *args)
except ConnectionRefusedError as error:
logger.error("Unable to run command, connection to deluge seems to be offline")
except FailedToReconnectException as error:
logger.error("Unable to run command, reconnection to deluge failed")
def _connect(self):
if self.host != 'localhost' and self.host is not None:
self.establishSSHTunnel()
self.client = DelugeRPCClient(self.host, self.port, self.user, self.password)
self.client.connect()
def add(self, url):
response = None
if (url.startswith('magnet')):
response = self._call('core.add_torrent_magnet', url, {})
elif url.startswith('http'):
magnet = self.getMagnetFromFile(url)
response = self._call('core.add_torrent_magnet', magnet, {})
return responseToString(response)
def get_all(self, _filter=None):
response = None
if (type(_filter) is list and len(_filter)):
if ('seeding' in _filter):
response = self._call('core.get_torrents_status', {'state': 'Seeding'}, [])
elif ('downloading' in _filter):
response = self._call('core.get_torrents_status', {'state': 'Downloading'}, [])
elif ('paused' in _filter):
response = self._call('core.get_torrents_status', {'paused': 'true'}, [])
else:
response = self.client.call('core.get_torrents_status', {}, [])
if response == {}:
return None
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.lower():
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._call('core.get_torrent_status', id, {})
if response == {}:
logger.warning('No torrent with id: {}'.format(id))
return None
return Torrent.fromDeluge(response)
def toggle(self, id):
torrent = self.get(id)
if torrent is None:
return
if (torrent.paused):
response = self._call('core.resume_torrent', [id])
else:
response = self._call('core.pause_torrent', [id])
return responseToString(response)
def removeByName(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.remove(torrent.key, destroy)
logger.debug('Response rm: {}'.format(str(response)))
if response == False:
raise AttributeError('Unable to remove torrent.')
return responseToString(response)
else:
logger.error('ERROR. No torrent found with that name.')
def remove(self, id, destroy=False):
try:
response = self.client.call('core.remove_torrent', id, destroy)
logger.debug('Response from remove: {}'.format(str(response)))
return responseToString(response)
except BaseException as error:
if 'torrent_id not in session' in str(error):
logger.info('Unable to remove. No torrent with matching id')
return None
raise error
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 __del__(self):
if hasattr(self, 'client') and self.client.connected:
logger.debug('Disconnected deluge rpc')
self.client.disconnect()
if hasattr(self, 'tunnel') and self.tunnel.is_active:
logger.debug('Closing ssh tunnel')
self.tunnel.stop(True)
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

View File

@@ -1,48 +0,0 @@
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 "{} {} Progress: {}% ETA: {} State: {} Paused: {}".format(
self.key, self.name[:59].ljust(60), self.progress.rjust(5), self.eta.rjust(11), self.state.ljust(12), self.paused)

View File

@@ -1,98 +0,0 @@
#!/usr/bin/env python3.10
# -*- 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: \
"{}.'.format(key, user_config_dir))
exit(1)
return config
class ColorizeFilter(logging.Filter):
"""
Class for setting specific colors to levels of severity for log output
"""
color_by_level = {
10: 'cyan',
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
def validHash(hash: str):
try:
return hash and len(hash) == 40 and int(hash, 16)
except ValueError:
return False
import math
def convertFilesize(size_bytes):
if size_bytes == None or size_bytes == 0:
return "0B"
size_name = ("B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB")
i = int(math.floor(math.log(size_bytes, 1024)))
p = math.pow(1024, i)
s = round(size_bytes / p, 2)
return "%s %s" % (s, size_name[i])

1
deluge_cli.log Normal file
View File

@@ -0,0 +1 @@

319
deluge_cli.py Executable file
View File

@@ -0,0 +1,319 @@
#!/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 [--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 json
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])
return response
def remove(self, name):
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, False)
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(arg=None):
"""
Main function, parse the input
"""
signal.signal(signal.SIGINT, signal_handler)
arguments = docopt(__doc__, argv=arg, 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)
return response
elif arguments['search']:
logger.info('Search cmd selected for query: {}'.format(query))
response = deluge.search(query)
[ pprint(t.toJSON()) for t in response ]
return response
elif arguments['progress']:
logger.info('Progress cmd selected.')
response = deluge.progress()
print(response)
# [ pprint(t.toJSON()) for t in response ]
return response
elif arguments['get']:
logger.info('Get cmd selected for id: {}'.format(_id))
response = deluge.get(_id)
pprint(response.toJSON())
return response
elif arguments['ls']:
logger.info('List cmd selected')
response = deluge.get_all(_filter=_filter)
response = [t.toJSON() for t in response]
# pprint(response)
return json.dumps(response)
elif arguments['toggle']:
logger.info('Toggling id: {}'.format(_id))
response = deluge.togglePaused(_id)
print('toggle response: ', response)
return response
elif arguments['rm']:
logger.info('Remove by name: {}'.format(name))
response = deluge.remove(name)
print('rm response: ', response)
return response
if __name__ == '__main__':
main()

View File

@@ -2,6 +2,9 @@ import asyncio
import datetime
import random
import websockets
import json
import deluge_cli
async def hello(websocket, path):
name = await websocket.recv()
@@ -19,9 +22,16 @@ async def time(websocket, path):
await asyncio.sleep(1)
async def deluge(websocket, path):
while True:
downloading = deluge_cli.main(['progress'])
await websocket.send(json.dumps(downloading))
await asyncio.sleep(1)
serve_hello = websockets.serve(hello, '0.0.0.0', 8765)
serve_time = websockets.serve(time, '0.0.0.0', 5678)
serve_deluge = websockets.serve(deluge, '0.0.0.0', 5678)
asyncio.get_event_loop().run_until_complete(serve_hello)
asyncio.get_event_loop().run_until_complete(serve_time)
asyncio.get_event_loop().run_until_complete(serve_deluge)
asyncio.get_event_loop().run_forever()

View File

@@ -1,7 +0,0 @@
[build-system]
requires = [
"setuptools>=42",
"wheel"
]
build-backend = "setuptools.build_meta"

View File

@@ -1,5 +1,15 @@
colored==1.4.4
deluge-client==1.9.0
requests==2.28.1
sshtunnel==0.4.0
typer==0.7.0
asn1crypto==0.24.0
bcrypt==3.1.4
cffi==1.11.5
colored==1.3.5
cryptography==2.3
deluge-client==1.6.0
docopt==0.6.2
idna==2.7
paramiko==2.4.1
pyasn1==0.4.4
pycparser==2.18
PyNaCl==1.2.1
six==1.11.0
sshtunnel==0.1.4
websockets==6.0

View File

@@ -1,42 +0,0 @@
#!/usr/bin/env python3.10
# -*- encoding: utf-8 -*-
from setuptools import setup, find_packages
from sys import path
from os.path import dirname
with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
exec(open('delugeClient/__version__.py').read())
setup(
name="delugeClient-kevin",
version=__version__,
packages=find_packages(),
package_data={
'delugeClient': ['default_config.ini'],
},
python_requires=">=3.10",
author="KevinMidboe",
description="Deluge client with custom functions written in python",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/kevinmidboe/delugeClient",
install_requires=[
'colored>=1.4.4',
'deluge-client>=1.9.0',
'requests>=2.28.1',
'sshtunnel>=0.4.0',
'typer[all]>=0.7.0'
],
classifiers=[
'Programming Language :: Python',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3.10',
],
entry_points={
'console_scripts': [
'delugeclient = delugeClient.__main__:main',
],
}
)

42
utils.py Normal file
View File

@@ -0,0 +1,42 @@
#!/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('ascii')
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