mirror of
https://github.com/KevinMidboe/delugeClient.git
synced 2025-10-29 12:00:13 +00:00
Compare commits
35 Commits
socket
...
74f6f2b06f
| Author | SHA1 | Date | |
|---|---|---|---|
| 74f6f2b06f | |||
| 7ef58745e1 | |||
| b076b1274b | |||
| 97d86253c8 | |||
| d546027df7 | |||
| 2420d9e8c4 | |||
| 2bbf175c2a | |||
| 8597615e68 | |||
| 9f959dd171 | |||
| 869fe579ad | |||
| 724af16f45 | |||
| 201f944fdc | |||
| 9273666fed | |||
| 0cc33c98c1 | |||
| 5ffb97824f | |||
| 365cfd0911 | |||
| 61d1734954 | |||
| c48b4aa68b | |||
| 32cb0e51a7 | |||
| e03247bcc6 | |||
| 1b2620b6f2 | |||
| b7eb06e266 | |||
| c09d35670e | |||
| 39661ea3db | |||
| df8fdbd4fa | |||
| 177d73c516 | |||
| 518b5b0e78 | |||
| 9d34802957 | |||
| e36ba428a7 | |||
| 57b1b4e1f2 | |||
| fada382a32 | |||
| 8d943cb1ad | |||
| bd516f34d8 | |||
| cfe14dd1ba | |||
| 5e39ecb8bc |
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.10
|
||||||
|
commands:
|
||||||
|
- make build
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
|
deluge_cli.log
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|||||||
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)
|
||||||
24
README.md
24
README.md
@@ -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>
|
<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://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%;">
|
| [](https://www.python.org/downloads/release/python-3100/) | [](https://pypi.org/project/delugeClient_kevin/) | [](https://drone.schleppe.cloud/KevinMidboe/delugeClient)
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="https://opensource.org/licenses/MIT">
|
| Known vulnerabilities | License |
|
||||||
<img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="">
|
|:--------|:------|
|
||||||
</a>
|
| [](https://snyk.io/test/github/kevinmidboe/delugeClient?targetFile=requirements.txt) |[](LICENSE)
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="#abstract">Abstract</a> •
|
<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:
|
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.
|
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
|
- Create your feature branch: git checkout -b my-new-feature
|
||||||
- Commit your changes: git commit -am 'Add some feature'
|
- Commit your changes: git commit -am 'Add some feature'
|
||||||
- Push to the branch: git push origin my-new-feature
|
- Push to the branch: git push origin my-new-feature
|
||||||
- Submit a pull request
|
- Submit a pull request
|
||||||
|
|||||||
10
config.ini
10
config.ini
@@ -1,10 +0,0 @@
|
|||||||
[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
|
|
||||||
22
delugeClient/__init__.py
Normal file
22
delugeClient/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/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)
|
||||||
177
delugeClient/__main__.py
Normal file
177
delugeClient/__main__.py
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
#!/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()
|
||||||
1
delugeClient/__version__.py
Normal file
1
delugeClient/__version__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__version__ = '0.3.1'
|
||||||
11
delugeClient/default_config.ini
Normal file
11
delugeClient/default_config.ini
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[deluge]
|
||||||
|
host=
|
||||||
|
port=58846
|
||||||
|
user=
|
||||||
|
password=
|
||||||
|
|
||||||
|
[ssh]
|
||||||
|
host=
|
||||||
|
user=
|
||||||
|
password=
|
||||||
|
pkey=
|
||||||
220
delugeClient/deluge.py
Normal file
220
delugeClient/deluge.py
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
48
delugeClient/torrent.py
Normal file
48
delugeClient/torrent.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
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)
|
||||||
98
delugeClient/utils.py
Normal file
98
delugeClient/utils.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
#!/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 +0,0 @@
|
|||||||
|
|
||||||
306
deluge_cli.py
306
deluge_cli.py
@@ -1,306 +0,0 @@
|
|||||||
#!/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 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):
|
|
||||||
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():
|
|
||||||
"""
|
|
||||||
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']:
|
|
||||||
logger.info('Remove by name: {}'.format(name))
|
|
||||||
deluge.remove(name)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
|
|
||||||
7
pyproject.toml
Normal file
7
pyproject.toml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = [
|
||||||
|
"setuptools>=42",
|
||||||
|
"wheel"
|
||||||
|
]
|
||||||
|
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
@@ -1,15 +1,6 @@
|
|||||||
asn1crypto==0.24.0
|
colored==1.4.2
|
||||||
bcrypt==3.1.4
|
deluge-client==1.9.0
|
||||||
cffi==1.11.5
|
|
||||||
colored==1.3.5
|
|
||||||
cryptography==2.3
|
|
||||||
deluge-client==1.6.0
|
|
||||||
docopt==0.6.2
|
docopt==0.6.2
|
||||||
idna==2.7
|
requests==2.25.1
|
||||||
paramiko==2.4.1
|
sshtunnel==0.4.0
|
||||||
pyasn1==0.4.4
|
websockets==9.1
|
||||||
pycparser==2.18
|
|
||||||
PyNaCl==1.2.1
|
|
||||||
six==1.11.0
|
|
||||||
sshtunnel==0.1.4
|
|
||||||
websockets==6.0
|
|
||||||
42
setup.py
Normal file
42
setup.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/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
42
utils.py
@@ -1,42 +0,0 @@
|
|||||||
#!/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
|
|
||||||
Reference in New Issue
Block a user