mirror of
https://github.com/KevinMidboe/delugeClient.git
synced 2026-01-04 16:45:31 +00:00
Compare commits
15 Commits
snyk-fix-7
...
8597615e68
| Author | SHA1 | Date | |
|---|---|---|---|
| 8597615e68 | |||
| 9f959dd171 | |||
| 869fe579ad | |||
| 724af16f45 | |||
| 201f944fdc | |||
| 9273666fed | |||
| 0cc33c98c1 | |||
| 5ffb97824f | |||
| 365cfd0911 | |||
| 61d1734954 | |||
| c48b4aa68b | |||
| 32cb0e51a7 | |||
| e03247bcc6 | |||
| 1b2620b6f2 | |||
| b7eb06e266 |
14
.drone.yml
Normal file
14
.drone.yml
Normal 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
17
Makefile
Normal 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)
|
||||
21
README.md
21
README.md
@@ -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>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://pypi.org/project/delugeClient-kevin/">
|
||||
<img src="https://img.shields.io/pypi/v/delugeClient-kevin" />
|
||||
</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%;">
|
||||
</a>
|
||||
|
||||
<a href="https://opensource.org/licenses/MIT">
|
||||
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="">
|
||||
</a>
|
||||
</p>
|
||||
| Tested version | PyPi package | Drone CI |
|
||||
|:--------|:------|:------|
|
||||
| [](https://www.python.org/downloads/release/python-380/) | [](https://pypi.org/project/delugeClient_kevin/) | [](https://drone.schleppe.cloud/KevinMidboe/delugeClient)
|
||||
|
||||
|
||||
| Known vulnerabilities | License |
|
||||
|:--------|:------|
|
||||
| [](https://snyk.io/test/github/kevinmidboe/delugeClient?targetFile=requirements.txt) |[](LICENSE)
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="#abstract">Abstract</a> •
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import os
|
||||
#!/usr/bin/env python3.6
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
from sys import path
|
||||
from os.path import dirname, join
|
||||
|
||||
path.append(os.path.dirname(__file__))
|
||||
|
||||
__version__=0.1
|
||||
path.append(dirname(__file__))
|
||||
|
||||
import logging
|
||||
from delugeUtils import BASE_DIR
|
||||
from utils import BASE_DIR, ColorizeFilter
|
||||
|
||||
logger = logging.getLogger('deluge_cli')
|
||||
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)
|
||||
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())
|
||||
@@ -1,148 +1,137 @@
|
||||
#!/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
|
||||
import typer
|
||||
from pprint import pprint
|
||||
|
||||
from deluge import Deluge
|
||||
from utils import ColorizeFilter, BASE_DIR
|
||||
from __init__ import __version__
|
||||
from __version__ 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())
|
||||
logger.addFilter(ColorizeFilter())
|
||||
|
||||
app = typer.Typer()
|
||||
deluge = Deluge()
|
||||
|
||||
def signal_handler(signal, frame):
|
||||
"""
|
||||
Handle exit by Keyboardinterrupt
|
||||
"""
|
||||
del deluge
|
||||
|
||||
logger.info('\nGood bye!')
|
||||
sys.exit(0)
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main function, parse the input
|
||||
"""
|
||||
def handleKeyboardInterrupt():
|
||||
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)
|
||||
|
||||
def printResponse(response, json=False):
|
||||
try:
|
||||
if arguments['--json']:
|
||||
if len(response) > 1:
|
||||
if json:
|
||||
if isinstance(response, list):
|
||||
print('[{}]'.format(','.join([t.toJSON() for t in response])))
|
||||
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:
|
||||
logger.error('Unexpected error while trying to print')
|
||||
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__':
|
||||
main()
|
||||
handleKeyboardInterrupt()
|
||||
main()
|
||||
|
||||
1
delugeClient/__version__.py
Normal file
1
delugeClient/__version__.py
Normal file
@@ -0,0 +1 @@
|
||||
__version__ = '0.3.0'
|
||||
@@ -9,7 +9,7 @@ import logging.config
|
||||
|
||||
from deluge_client import DelugeRPCClient
|
||||
from sshtunnel import SSHTunnelForwarder
|
||||
from delugeUtils import getConfig, BASE_DIR
|
||||
from utils import getConfig, BASE_DIR
|
||||
|
||||
from torrent import Torrent
|
||||
|
||||
@@ -19,6 +19,14 @@ 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):
|
||||
@@ -46,7 +54,7 @@ class Deluge(object):
|
||||
return torrents
|
||||
|
||||
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:
|
||||
try:
|
||||
if self.password:
|
||||
@@ -66,11 +74,14 @@ class Deluge(object):
|
||||
|
||||
def add(self, url):
|
||||
logger.info('Adding magnet with url: {}.'.format(url))
|
||||
response = None
|
||||
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'):
|
||||
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):
|
||||
if (type(_filter) is list and len(_filter)):
|
||||
@@ -90,7 +101,7 @@ class Deluge(object):
|
||||
torrentNamesMatchingQuery = []
|
||||
if len(allTorrents):
|
||||
for torrent in allTorrents:
|
||||
if query in torrent.name:
|
||||
if query in torrent.name.lower():
|
||||
torrentNamesMatchingQuery.append(torrent)
|
||||
|
||||
allTorrents = torrentNamesMatchingQuery
|
||||
@@ -102,33 +113,47 @@ class Deluge(object):
|
||||
|
||||
def get(self, 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)
|
||||
|
||||
def togglePaused(self, id):
|
||||
def toggle(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):
|
||||
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):
|
||||
if len(matches) > 1:
|
||||
raise ValueError('Multiple files found matching key. Unable to remove.')
|
||||
elif (len(matches) == 1):
|
||||
elif len(matches) == 1:
|
||||
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)))
|
||||
|
||||
if (response == False):
|
||||
if response == False:
|
||||
raise AttributeError('Unable to remove torrent.')
|
||||
return response
|
||||
return responseToString(response)
|
||||
else:
|
||||
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):
|
||||
filteredTorrents = []
|
||||
for t in torrents:
|
||||
@@ -140,23 +165,12 @@ class Deluge(object):
|
||||
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()
|
||||
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.')
|
||||
|
||||
@@ -44,5 +44,5 @@ class Torrent(object):
|
||||
return json.dumps(torrentDict)
|
||||
|
||||
def __str__(self):
|
||||
return "Name: {}, Progress: {}%, ETA: {}, State: {}, Paused: {}".format(
|
||||
self.name, self.progress, self.eta, self.state, self.paused)
|
||||
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)
|
||||
@@ -50,7 +50,7 @@ def getConfig():
|
||||
for key, value in requiredParameters:
|
||||
if value == '':
|
||||
logger.error('Missing value for variable: "{}" in config: \
|
||||
"$HOME/.config/delugeClient/config.ini".'.format(key))
|
||||
"{}.'.format(key, user_config_dir))
|
||||
exit(1)
|
||||
|
||||
return config
|
||||
@@ -3,4 +3,4 @@ deluge-client==1.9.0
|
||||
docopt==0.6.2
|
||||
requests==2.25.1
|
||||
sshtunnel==0.4.0
|
||||
websockets==10.0
|
||||
websockets==9.1
|
||||
34
setup.py
34
setup.py
@@ -1,25 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- encoding: utf-8 -*-
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
import delugeClient
|
||||
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=delugeClient.__version__,
|
||||
version=__version__,
|
||||
packages=find_packages(),
|
||||
package_data={
|
||||
'delugeClient': ['default_config.ini'],
|
||||
},
|
||||
python_requires=">=3.6",
|
||||
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.2',
|
||||
'deluge-client==1.9.0',
|
||||
'docopt==0.6.2',
|
||||
'requests==2.25.1',
|
||||
'sshtunnel==0.4.0',
|
||||
'websockets==9.1'
|
||||
'colored',
|
||||
'deluge-client',
|
||||
'requests',
|
||||
'sshtunnel',
|
||||
'typer',
|
||||
'websockets'
|
||||
],
|
||||
classifiers=[
|
||||
'Programming Language :: Python',
|
||||
@@ -30,10 +39,5 @@ setup(
|
||||
'console_scripts': [
|
||||
'delugeclient = delugeClient.__main__:main',
|
||||
],
|
||||
},
|
||||
packages=find_packages(),
|
||||
package_data={
|
||||
'delugeClient': ['default_config.ini'],
|
||||
},
|
||||
python_requires=">=3.6",
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user