7 Commits

Author SHA1 Message Date
74f6f2b06f Updated pacakge version to 0.3.1 2022-11-24 00:21:33 +01:00
7ef58745e1 Better validation of args and logging of non success responses 2022-11-24 00:21:21 +01:00
b076b1274b Handle errors from deluge-client cracefully with logs 2022-11-24 00:18:26 +01:00
97d86253c8 Include id when printing torrent 2022-11-24 00:17:42 +01:00
d546027df7 Better adding of new log handlers
- __init__.py exports addHandler function for adding new handlers
- when --json flag is set we try disable all ch logs
2022-11-24 00:15:35 +01:00
2420d9e8c4 Define install_requires with minimal versions 2022-11-24 00:12:38 +01:00
2bbf175c2a Updated for python 3.10 2022-11-24 00:11:57 +01:00
9 changed files with 177 additions and 82 deletions

View File

@@ -9,6 +9,6 @@ platform:
steps:
- name: Build package
image: python:3.8
image: python:3.10
commands:
- make build

View File

@@ -6,7 +6,7 @@
| Tested version | PyPi package | Drone CI |
|:--------|:------|:------|
| [![PyVersion](https://img.shields.io/badge/python-3.8-blue.svg)](https://www.python.org/downloads/release/python-380/) | [![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)
| [![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 |
@@ -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.6 env
$ virtualenv -p python3.10 env
```
> 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/)
> 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/)
First we navigate to the folder we downloaded.

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python3.6
#!/usr/bin/env python3.10
# -*- encoding: utf-8 -*-
from sys import path
@@ -7,16 +7,16 @@ from os.path import dirname, join
path.append(dirname(__file__))
import logging
from utils import BASE_DIR, ColorizeFilter
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'))
fh.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s| %(levelname)s | %(message)s')
formatter = logging.Formatter('%(asctime)s %(levelname)8s %(name)s | %(message)s')
fh.setFormatter(formatter)
logger.addHandler(fh)
logger.addFilter(ColorizeFilter())
addHandler(fh)

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python3.6
#!/usr/bin/env python3.10
import os
import sys
@@ -9,27 +9,27 @@ import typer
from pprint import pprint
from deluge import Deluge
from utils import ColorizeFilter, BASE_DIR
from utils import ColorizeFilter, BASE_DIR, validHash, convertFilesize
from __version__ import __version__
from __init__ import addHandler
logger = logging.getLogger('deluge_cli')
ch = logging.StreamHandler()
ch.setLevel(logging.ERROR)
logger.addHandler(ch)
logger.addFilter(ColorizeFilter())
ch.addFilter(ColorizeFilter())
addHandler(ch)
logger = logging.getLogger('deluge_cli')
app = typer.Typer()
deluge = Deluge()
deluge = None
def signal_handler(signal, frame):
"""
Handle exit by Keyboardinterrupt
"""
global deluge
del deluge
logger.info('\nGood bye!')
sys.exit(0)
sys.exit(1)
def handleKeyboardInterrupt():
signal.signal(signal.SIGINT, signal_handler)
@@ -54,22 +54,30 @@ def printResponse(response, json=False):
raise error
@app.command()
def add(magnet: str):
def add(magnet: str, json: bool = typer.Option(False, help="Print as json")):
'''
Add magnet torrent
'''
logger.debug('Add command selected')
logger.info('Add command selected')
logger.debug(magnet)
response = deluge.add(magnet)
printResponse(response)
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.debug('List command selected')
logger.info('List command selected')
response = deluge.get_all()
if response is None:
logger.info('No torrents found')
return
printResponse(response, json)
@app.command()
@@ -77,7 +85,9 @@ def get(id: str, json: bool = typer.Option(False, help="Print as json")):
'''
Get torrent by id or hash
'''
logger.debug('Get command selected for id {}'.format(id))
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)
@@ -86,27 +96,50 @@ def toggle(id: str):
'''
Toggle torrent download state
'''
logger.debug('Toggle command selected for id {}'.format(id))
response = deluge.toggle(id)
printResponse(response)
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.debug('Search command selected with query: {}'.format(query))
logger.info('Search command selected with query: {}'.format(query))
response = deluge.search(query)
printResponse(response, json)
@app.command()
def remove(id: str, destroy: bool = typer.Option(False, help="Remove torrent data")):
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.debug('Remove command selected for id: {} with destroy: {}'.format(id, destroy))
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)
printResponse(response)
@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():
@@ -115,11 +148,14 @@ def 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.WARNING)
ch.setLevel(logging.INFO)
if error == True:
if '--json' in sys.argv:
ch.setLevel(logging.CRITICAL)
elif error == True:
ch.setLevel(logging.ERROR)
elif warning == True:
ch.setLevel(logging.WARNING)
@@ -128,6 +164,10 @@ def defaultOptions(debug: bool = typer.Option(False, '--debug', help='Set log le
elif debug == True:
ch.setLevel(logging.DEBUG)
# Initiate deluge
global deluge
deluge = Deluge()
def main():
app()
del deluge

View File

@@ -1 +1 @@
__version__ = '0.3.0'
__version__ = '0.3.1'

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python3.6
#!/usr/bin/env python3.10
import os
import re
@@ -7,8 +7,8 @@ import logging
import requests
import logging.config
from deluge_client import DelugeRPCClient
from sshtunnel import SSHTunnelForwarder
from deluge_client import DelugeRPCClient, FailedToReconnectException
from sshtunnel import SSHTunnelForwarder, BaseSSHTunnelForwarderError
from utils import getConfig, BASE_DIR
from torrent import Torrent
@@ -41,7 +41,19 @@ class Deluge(object):
self.ssh_pkey = config['ssh']['pkey']
self.ssh_password = config['ssh']['password']
self._connect()
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')
@@ -53,49 +65,68 @@ class Deluge(object):
torrents.append(Torrent.fromDeluge(torrent))
return torrents
def _connect(self):
def establishSSHTunnel(self):
logger.debug('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
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):
logger.info('Adding magnet with url: {}.'.format(url))
response = None
if (url.startswith('magnet')):
response = self.client.call('core.add_torrent_magnet', url, {})
response = self._call('core.add_torrent_magnet', url, {})
elif url.startswith('http'):
magnet = self.getMagnetFromFile(url)
response = self.client.call('core.add_torrent_magnet', magnet, {})
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.client.call('core.get_torrents_status', {'state': 'Seeding'}, [])
response = self._call('core.get_torrents_status', {'state': 'Seeding'}, [])
elif ('downloading' in _filter):
response = self.client.call('core.get_torrents_status', {'state': 'Downloading'}, [])
response = self._call('core.get_torrents_status', {'state': 'Downloading'}, [])
elif ('paused' in _filter):
response = self.client.call('core.get_torrents_status', {'paused': 'true'}, [])
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 = []
@@ -112,7 +143,7 @@ class Deluge(object):
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, {})
response = self._call('core.get_torrent_status', id, {})
if response == {}:
logger.warning('No torrent with id: {}'.format(id))
return None
@@ -121,10 +152,13 @@ class Deluge(object):
def toggle(self, id):
torrent = self.get(id)
if torrent is None:
return
if (torrent.paused):
response = self.client.call('core.resume_torrent', [id])
response = self._call('core.resume_torrent', [id])
else:
response = self.client.call('core.pause_torrent', [id])
response = self._call('core.pause_torrent', [id])
return responseToString(response)
@@ -137,7 +171,7 @@ class Deluge(object):
elif len(matches) == 1:
torrent = matches[0]
response = self.remove(torrent.key, destroy)
logger.info('Response: {}'.format(str(response)))
logger.debug('Response rm: {}'.format(str(response)))
if response == False:
raise AttributeError('Unable to remove torrent.')
@@ -146,13 +180,16 @@ class Deluge(object):
logger.error('ERROR. No torrent found with that name.')
def remove(self, id, destroy=False):
response = self.client.call('core.remove_torrent', id, destroy)
logger.info('Response: {}'.format(str(response)))
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
if response == False:
raise AttributeError('Unable to remove torrent.')
return responseToString(response)
raise error
def filterOnValue(self, torrents, value):
filteredTorrents = []
@@ -166,7 +203,9 @@ class Deluge(object):
return filteredTorrents
def __del__(self):
self.client.disconnect()
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')

View File

@@ -44,5 +44,5 @@ class Torrent(object):
return json.dumps(torrentDict)
def __str__(self):
return "{} Progress: {}% ETA: {} State: {} Paused: {}".format(
self.name[:59].ljust(60), self.progress.rjust(5), self.eta.rjust(11), self.state.ljust(12), self.paused)
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,4 +1,4 @@
#!/usr/bin/env python3
#!/usr/bin/env python3.10
# -*- coding: utf-8 -*-
# @Author: kevinmidboe
# @Date: 2018-04-17 19:55:38
@@ -60,7 +60,7 @@ class ColorizeFilter(logging.Filter):
Class for setting specific colors to levels of severity for log output
"""
color_by_level = {
10: 'chartreuse_3b',
10: 'cyan',
20: 'white',
30: 'orange_1',
40: 'red'
@@ -79,3 +79,20 @@ def convert(data):
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])

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python3
#!/usr/bin/env python3.10
# -*- encoding: utf-8 -*-
from setuptools import setup, find_packages
from sys import path
@@ -16,24 +16,23 @@ setup(
package_data={
'delugeClient': ['default_config.ini'],
},
python_requires=">=3.6",
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',
'deluge-client',
'requests',
'sshtunnel',
'typer',
'websockets'
'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.6',
'Programming Language :: Python :: 3.10',
],
entry_points={
'console_scripts': [