15 Commits

Author SHA1 Message Date
8597615e68 Replaced docopt with typer. Bumps version to 0.3.0
Switched out the cli package to typer.
Removed progress command since it did mostly what ls did.
Priting Torrents pads output to be a bit more readable.
Make sure to disconnect from deluge & ssh before script exits
2022-09-28 21:14:26 +02:00
9f959dd171 Terminate with exit code. Also structured func resp & print to caller of main. 2022-09-28 19:17:27 +02:00
869fe579ad Renamed cli params for clearity and consistency 2022-09-28 19:12:53 +02:00
724af16f45 Moved logging setup to init and only define streamhandler from main. 2022-09-28 19:10:01 +02:00
201f944fdc Updated incorrect markdown table formatting syntax
Also bumped package version to 0.2.3
2022-09-26 00:36:52 +02:00
9273666fed Merge pull request #7 from KevinMidboe/feat/drone-ci
Feat: Drone ci
2022-09-26 00:35:03 +02:00
0cc33c98c1 Bumped to version 0.2.2 2022-09-26 00:33:50 +02:00
5ffb97824f Updated readme w/ drone ci badge 2022-09-26 00:33:19 +02:00
365cfd0911 Simple drone integration that just tries to build package 2022-09-26 00:24:37 +02:00
61d1734954 Merge pull request #6 from KevinMidboe/fix/setuptools-build
Fix: Setuptools build
2022-09-26 00:23:01 +02:00
c48b4aa68b Bumped to version 0.2.1 2022-09-26 00:19:25 +02:00
32cb0e51a7 Created make file w/ build, dist, install & upload 2022-09-26 00:18:19 +02:00
e03247bcc6 Re-ordered setup & logging actions/lines 2022-09-26 00:17:51 +02:00
1b2620b6f2 Renamed file delugeUtils -> utils 2022-09-26 00:17:06 +02:00
b7eb06e266 Moved __version__ to separate file 2022-09-26 00:16:16 +02:00
11 changed files with 217 additions and 181 deletions

14
.drone.yml Normal file
View File

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

17
Makefile Normal file
View File

@@ -0,0 +1,17 @@
.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,18 +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> <h4 align="center"> A easy to use Deluge CLI that can connect to Deluge RPC (even over ssh) written entirely in python.</h4>
<p align="center"> | Tested version | PyPi package | Drone CI |
<a href="https://pypi.org/project/delugeClient-kevin/"> |:--------|:------|:------|
<img src="https://img.shields.io/pypi/v/delugeClient-kevin" /> | [![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)
</a>
<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%;"> | Known vulnerabilities | License |
</a> |:--------|:------|
| [![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)
<a href="https://opensource.org/licenses/MIT">
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="">
</a>
</p>
<p align="center"> <p align="center">
<a href="#abstract">Abstract</a> • <a href="#abstract">Abstract</a> •

View File

@@ -1,22 +1,22 @@
import os #!/usr/bin/env python3.6
# -*- encoding: utf-8 -*-
from sys import path from sys import path
from os.path import dirname, join
path.append(os.path.dirname(__file__)) path.append(dirname(__file__))
__version__=0.1
import logging import logging
from delugeUtils import BASE_DIR from utils import BASE_DIR, ColorizeFilter
logger = logging.getLogger('deluge_cli') logger = logging.getLogger('deluge_cli')
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
fh = logging.FileHandler(os.path.join(BASE_DIR, 'deluge_cli.log')) fh = logging.FileHandler(join(BASE_DIR, 'deluge_cli.log'))
fh.setLevel(logging.DEBUG) fh.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
ch.setLevel(logging.ERROR)
formatter = logging.Formatter('%(asctime)s %(levelname)8s %(name)s | %(message)s') formatter = logging.Formatter('%(asctime)s %(levelname)8s %(name)s | %(message)s')
fh.setFormatter(formatter) fh.setFormatter(formatter)
logger.addHandler(fh) logger.addHandler(fh)
logger.addHandler(ch) logger.addFilter(ColorizeFilter())

View File

@@ -1,148 +1,137 @@
#!/usr/bin/env python3.6 #!/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 os
import sys import sys
import signal import signal
import logging import logging
from docopt import docopt import typer
from pprint import pprint from pprint import pprint
from deluge import Deluge from deluge import Deluge
from utils import ColorizeFilter, BASE_DIR from utils import ColorizeFilter, BASE_DIR
from __init__ import __version__ from __version__ import __version__
logger = logging.getLogger('deluge_cli') 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 = logging.StreamHandler()
ch.setLevel(logging.ERROR) 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.addHandler(ch)
logger.addFilter(ColorizeFilter()) logger.addFilter(ColorizeFilter())
app = typer.Typer()
deluge = Deluge()
def signal_handler(signal, frame): def signal_handler(signal, frame):
""" """
Handle exit by Keyboardinterrupt Handle exit by Keyboardinterrupt
""" """
del deluge
logger.info('\nGood bye!') logger.info('\nGood bye!')
sys.exit(0) sys.exit(0)
def main(): def handleKeyboardInterrupt():
"""
Main function, parse the input
"""
signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGINT, signal_handler)
arguments = docopt(__doc__, version=__version__) def printResponse(response, json=False):
# 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: try:
if arguments['--json']: if json:
if len(response) > 1: if isinstance(response, list):
print('[{}]'.format(','.join([t.toJSON() for t in response]))) print('[{}]'.format(','.join([t.toJSON() for t in response])))
else: else:
print(response[0].toJSON()) print(response.toJSON())
elif isinstance(response, list):
for el in response:
print(el)
elif response:
print(response)
except KeyError as error: except KeyError as error:
logger.error('Unexpected error while trying to print') logger.error('Unexpected error while trying to print')
raise error raise error
return response @app.command()
def add(magnet: str):
'''
Add magnet torrent
'''
logger.debug('Add command selected')
logger.debug(magnet)
response = deluge.add(magnet)
printResponse(response)
@app.command()
def ls(json: bool = typer.Option(False, help="Print as json")):
'''
List all torrents
'''
logger.debug('List command selected')
response = deluge.get_all()
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.debug('Get command selected for id {}'.format(id))
response = deluge.get(id)
printResponse(response, json)
@app.command()
def toggle(id: str):
'''
Toggle torrent download state
'''
logger.debug('Toggle command selected for id {}'.format(id))
response = deluge.toggle(id)
printResponse(response)
@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))
response = deluge.search(query)
printResponse(response, json)
@app.command()
def remove(id: str, destroy: bool = typer.Option(False, help="Remove torrent data")):
'''
Remove torrent by id or hash
'''
logger.debug('Remove command selected for id: {} with destroy: {}'.format(id, destroy))
response = deluge.remove(id, destroy)
printResponse(response)
@app.command()
def version():
'''
Print package version
'''
print(__version__)
@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)
if 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)
def main():
app()
del deluge
if __name__ == '__main__': if __name__ == '__main__':
main() handleKeyboardInterrupt()
main()

View File

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

View File

@@ -9,7 +9,7 @@ import logging.config
from deluge_client import DelugeRPCClient from deluge_client import DelugeRPCClient
from sshtunnel import SSHTunnelForwarder from sshtunnel import SSHTunnelForwarder
from delugeUtils import getConfig, BASE_DIR from utils import getConfig, BASE_DIR
from torrent import Torrent from torrent import Torrent
@@ -19,6 +19,14 @@ def split_words(string):
logger.debug('Splitting input: {} (type: {}) with split_words'.format(string, type(string))) logger.debug('Splitting input: {} (type: {}) with split_words'.format(string, type(string)))
return re.findall(r"[\w\d']+", string.lower()) 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): class Deluge(object):
"""docstring for ClassName""" """docstring for ClassName"""
def __init__(self): def __init__(self):
@@ -46,7 +54,7 @@ class Deluge(object):
return torrents return torrents
def _connect(self): def _connect(self):
logger.info('Checking if script on same server as deluge RPC') logger.debug('Checking if script on same server as deluge RPC')
if self.host != 'localhost' and self.host is not None: if self.host != 'localhost' and self.host is not None:
try: try:
if self.password: if self.password:
@@ -66,11 +74,14 @@ class Deluge(object):
def add(self, url): def add(self, url):
logger.info('Adding magnet with url: {}.'.format(url)) logger.info('Adding magnet with url: {}.'.format(url))
response = None
if (url.startswith('magnet')): if (url.startswith('magnet')):
return self.client.call('core.add_torrent_magnet', url, {}) response = self.client.call('core.add_torrent_magnet', url, {})
elif url.startswith('http'): elif url.startswith('http'):
magnet = self.getMagnetFromFile(url) magnet = self.getMagnetFromFile(url)
return self.client.call('core.add_torrent_magnet', magnet, {}) response = self.client.call('core.add_torrent_magnet', magnet, {})
return responseToString(response)
def get_all(self, _filter=None): def get_all(self, _filter=None):
if (type(_filter) is list and len(_filter)): if (type(_filter) is list and len(_filter)):
@@ -90,7 +101,7 @@ class Deluge(object):
torrentNamesMatchingQuery = [] torrentNamesMatchingQuery = []
if len(allTorrents): if len(allTorrents):
for torrent in allTorrents: for torrent in allTorrents:
if query in torrent.name: if query in torrent.name.lower():
torrentNamesMatchingQuery.append(torrent) torrentNamesMatchingQuery.append(torrent)
allTorrents = torrentNamesMatchingQuery allTorrents = torrentNamesMatchingQuery
@@ -102,33 +113,47 @@ class Deluge(object):
def get(self, id): def get(self, id):
response = self.client.call('core.get_torrent_status', id, {}) response = self.client.call('core.get_torrent_status', id, {})
if response == {}:
logger.warning('No torrent with id: {}'.format(id))
return None
return Torrent.fromDeluge(response) return Torrent.fromDeluge(response)
def togglePaused(self, id): def toggle(self, id):
torrent = self.get(id) torrent = self.get(id)
if (torrent.paused): if (torrent.paused):
response = self.client.call('core.resume_torrent', [id]) response = self.client.call('core.resume_torrent', [id])
else: else:
response = self.client.call('core.pause_torrent', [id]) response = self.client.call('core.pause_torrent', [id])
return response
def remove(self, name, destroy=False): return responseToString(response)
def removeByName(self, name, destroy=False):
matches = list(filter(lambda t: t.name == name, self.get_all())) matches = list(filter(lambda t: t.name == name, self.get_all()))
logger.info('Matches for {}: {}'.format(name, matches)) logger.info('Matches for {}: {}'.format(name, matches))
if (len(matches) > 1): if len(matches) > 1:
raise ValueError('Multiple files found matching key. Unable to remove.') raise ValueError('Multiple files found matching key. Unable to remove.')
elif (len(matches) == 1): elif len(matches) == 1:
torrent = matches[0] torrent = matches[0]
response = self.client.call('core.remove_torrent', torrent.key, destroy) response = self.remove(torrent.key, destroy)
logger.info('Response: {}'.format(str(response))) logger.info('Response: {}'.format(str(response)))
if (response == False): if response == False:
raise AttributeError('Unable to remove torrent.') raise AttributeError('Unable to remove torrent.')
return response return responseToString(response)
else: else:
logger.error('ERROR. No torrent found with that name.') 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)))
if response == False:
raise AttributeError('Unable to remove torrent.')
return responseToString(response)
def filterOnValue(self, torrents, value): def filterOnValue(self, torrents, value):
filteredTorrents = [] filteredTorrents = []
for t in torrents: for t in torrents:
@@ -140,23 +165,12 @@ class Deluge(object):
filteredTorrents.append(value_template) filteredTorrents.append(value_template)
return filteredTorrents 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): def __del__(self):
if hasattr(self, 'tunnel'): self.client.disconnect()
logger.info('Closing ssh tunnel')
self.tunnel.stop() if hasattr(self, 'tunnel') and self.tunnel.is_active:
logger.debug('Closing ssh tunnel')
self.tunnel.stop(True)
def getMagnetFromFile(self, url): def getMagnetFromFile(self, url):
logger.info('File url found, fetching magnet.') logger.info('File url found, fetching magnet.')

View File

@@ -44,5 +44,5 @@ class Torrent(object):
return json.dumps(torrentDict) return json.dumps(torrentDict)
def __str__(self): def __str__(self):
return "Name: {}, Progress: {}%, ETA: {}, State: {}, Paused: {}".format( return "{} Progress: {}% ETA: {} State: {} Paused: {}".format(
self.name, self.progress, self.eta, self.state, self.paused) self.name[:59].ljust(60), self.progress.rjust(5), self.eta.rjust(11), self.state.ljust(12), self.paused)

View File

@@ -50,7 +50,7 @@ def getConfig():
for key, value in requiredParameters: for key, value in requiredParameters:
if value == '': if value == '':
logger.error('Missing value for variable: "{}" in config: \ logger.error('Missing value for variable: "{}" in config: \
"$HOME/.config/delugeClient/config.ini".'.format(key)) "{}.'.format(key, user_config_dir))
exit(1) exit(1)
return config return config

View File

@@ -3,4 +3,4 @@ deluge-client==1.9.0
docopt==0.6.2 docopt==0.6.2
requests==2.25.1 requests==2.25.1
sshtunnel==0.4.0 sshtunnel==0.4.0
websockets==10.0 websockets==9.1

View File

@@ -1,25 +1,34 @@
#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
from setuptools import setup, find_packages from setuptools import setup, find_packages
from sys import path
import delugeClient from os.path import dirname
with open("README.md", "r", encoding="utf-8") as fh: with open("README.md", "r", encoding="utf-8") as fh:
long_description = fh.read() long_description = fh.read()
exec(open('delugeClient/__version__.py').read())
setup( setup(
name="delugeClient-kevin", name="delugeClient-kevin",
version=delugeClient.__version__, version=__version__,
packages=find_packages(),
package_data={
'delugeClient': ['default_config.ini'],
},
python_requires=">=3.6",
author="KevinMidboe", author="KevinMidboe",
description="Deluge client with custom functions written in python", description="Deluge client with custom functions written in python",
long_description=long_description, long_description=long_description,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
url="https://github.com/kevinmidboe/delugeClient", url="https://github.com/kevinmidboe/delugeClient",
install_requires=[ install_requires=[
'colored==1.4.2', 'colored',
'deluge-client==1.9.0', 'deluge-client',
'docopt==0.6.2', 'requests',
'requests==2.25.1', 'sshtunnel',
'sshtunnel==0.4.0', 'typer',
'websockets==9.1' 'websockets'
], ],
classifiers=[ classifiers=[
'Programming Language :: Python', 'Programming Language :: Python',
@@ -30,10 +39,5 @@ setup(
'console_scripts': [ 'console_scripts': [
'delugeclient = delugeClient.__main__:main', 'delugeclient = delugeClient.__main__:main',
], ],
}, }
packages=find_packages(),
package_data={
'delugeClient': ['default_config.ini'],
},
python_requires=">=3.6",
) )