48 Commits

Author SHA1 Message Date
e81f5f618e Sign drone config file
View docs for more info on signing: https://docs.drone.io/signature/
2022-11-26 13:24:23 +01:00
4b5500ec3b Try install built wheel with wildcard 2022-11-26 13:10:38 +01:00
29c9feeaa1 Bumped package version to 0.3.2 2022-11-26 13:05:53 +01:00
c36bacc264 Exclude publish step when PR towards master 2022-11-26 13:03:11 +01:00
48e5b3bf3e Remove test pypi publish ci step 2022-11-26 12:58:18 +01:00
8853ae85d8 Updated readme and license in setup.py 2022-11-26 12:55:43 +01:00
07c6e6fbe1 Only run public package when interacting with master 2022-11-26 12:53:44 +01:00
4d861e1739 Merged with upstream 2022-11-25 01:23:05 +01:00
5acf8e8848 Clearer output text for publish version 2022-11-25 01:21:59 +01:00
103696e01a Clearer output text for publish version 2022-11-25 01:03:59 +01:00
b1018d7f9d Merge branch 'ci/build-and-pypi-publish' of github.com:kevinmidboe/delugeClient into ci/build-and-pypi-publish 2022-11-25 00:56:02 +01:00
7d4f4d0e9b If source is newer than pypi, exit 0 and continue pipeline 2022-11-25 00:38:35 +01:00
2f716e65a3 If source is newer than pypi, exit 0 and continue pipeline 2022-11-25 00:33:22 +01:00
fa59acfd03 Twince check command before upload 2022-11-25 00:01:40 +01:00
120d300b07 Install python dependencies before publish 2022-11-24 23:49:18 +01:00
0faa42a048 Publish depends on build 2022-11-24 23:13:29 +01:00
e2db73bf2a fixed typo in pacakge name 2022-11-24 23:05:28 +01:00
0841fdc03d sync pipenv after install 2022-11-24 22:57:03 +01:00
76c99568a8 try install requirements first 2022-11-24 22:50:04 +01:00
09e496a907 test explicitly defining wheel version 2022-11-24 22:46:54 +01:00
3989523632 add missing pipenv command 2022-11-24 22:43:18 +01:00
30c3e117da test step indentation fking with lint 2022-11-24 22:40:30 +01:00
3fa8c4b18f Add build for amd64 & arm64 arch + start of publish steps 2022-11-24 22:30:03 +01:00
11e9677d1a Split wheel and tarball dist into separate make cmds 2022-11-24 22:29:50 +01:00
519b51c47c Updated python requirements file 2022-11-24 21:32:08 +01:00
6fd63ff348 Delete requirements.txt 2022-11-24 01:16:40 +01:00
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
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
12 changed files with 476 additions and 278 deletions

66
.drone.yml Normal file
View File

@@ -0,0 +1,66 @@
---
kind: pipeline
type: docker
name: Build and test amd64
platform:
os: linux
arch: amd64
steps:
- name: Build source
image: python:3.10
commands:
- make build
- name: Install
image: python:3.10
commands:
- make dist
- pip3 install -r requirements.txt
- pip3 install dist/*.whl
# - pipenv install pytest
# - name: Run tests
# image: python:3.10
# commands:
# pipenv run pytest
---
kind: pipeline
type: docker
name: Publish package to PyPi
platform:
os: linux
arch: amd64
steps:
- name: Newer version to publish?
image: python:3.10
commands:
- pip3 install delugeClient-kevin -q -q
- bash publish_version?.sh
- name: PyPi publish
image: python:3.10
commands:
- make dist
- pip3 install twine
- twine upload dist/*
depends_on:
- Build and test amd64
trigger:
branch:
- master
event:
exclude:
- pull_request
---
kind: signature
hmac: 08793426ddd2274e2de166144dc15cd63fe6a2c0fd47382d28f20ececee84898
...

22
Makefile Normal file
View File

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

117
README.md
View File

@@ -4,26 +4,20 @@
<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 | License |
|:--------|:------|:------|
| [![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/) |[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
| Drone CI | Known vulnerabilities |
|:--------|:------|
| [![Build Status](https://drone.schleppe.cloud/api/badges/KevinMidboe/delugeClient/status.svg)](https://drone.schleppe.cloud/KevinMidboe/delugeClient) | [![Known Vulnerabilities](https://snyk.io/test/github/kevinmidboe/delugeClient/badge.svg?targetFile=requirements.txt)](https://snyk.io/test/github/kevinmidboe/delugeClient?targetFile=requirements.txt)
<p align="center">
<a href="#abstract">Abstract</a>
<a href="#setup_virtualenv">Setup virtualenv</a>
<a href="#configure">Configure</a>
<a href="#installation">Install dependencies</a>
<a href="#install">Install</a>
<a href="#usage">Usage</a>
<a href="#running">Running</a>
<a href="#setup_virtualenv">Setup Virtual Environment</a>
<a href="#configure">Configure</a>
<a href="#contributing">Contributing</a>
</p>
@@ -31,6 +25,44 @@
## <a name="abstract"></a> Abstract
Create a deluge python client for interfacing with deluge for common tasks like listing, adding, removing and setting download directory for torrents.
## <a name="install"></a> Install
Install from source:
```bash
python3 setup.py install
```
Install from pip:
```bash
pip3 install delugeClient-kevin
```
## <a name="usage"></a> Usage
View delugeClient cli options with `delugeClient --help`:
```
Usage: python -m delugeclient [OPTIONS] COMMAND [ARGS]...
╭─ Options ───────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ --debug Set log level to debug │
│ --info Set log level to info │
│ --warning Set log level to warning │
│ --error Set log level to error │
│ --install-completion Install completion for the current shell. │
│ --show-completion Show completion for the current shell, to copy it or customize the installation. │
│ --help Show this message and exit. │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ──────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ add Add magnet torrent │
│ disk Get free disk space │
│ get Get torrent by id or hash │
│ ls List all torrents │
│ remove Remove torrent by id or hash │
│ rm Remove torrent by name │
│ search Search for string segment in torrent name │
│ toggle Toggle torrent download state │
│ version Print package version │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
```
## <a name="setup_virtualenv"></a> Setup Virtual Environment
Virtual environment allows us to create a local environment for the requirements needed. Because pip does not download packages already downloaded to your system, we can use virtualenv to save our packages in the project folder.
@@ -44,7 +76,7 @@ To install virtualenv, simply run:
```
### Usage
### Virtualenv setup
After you have downloaded this project go to it in your terminal by going to the folder you downloaded and typing the following:
@@ -55,14 +87,12 @@ 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.
Then we use the ```virtualenv``` command to create a ```env``` subdirectory in our project. This is where pip will download everything to and where we can add other specific python versions. Then we need to *activate* our virtual environment by doing:
```
@@ -99,51 +129,6 @@ Then you need to change the HOST and PORT to reflect the address for your deluge
$ cat /home/USER/.config/deluge/auth
```
## <a name="install"></a> Install Required Dependencies
Now that we have our virutalenv set up and activated we want to install all the necessary packages listed in `requirements.txt`. To install it's dependencies do the following:
```
$ pip install -r requirements.txt
```
Now we have our neccessary packages installed!
## <a name="usage"></a> Usage
```
Custom delugeRPC client
Usage:
deluge_cli add MAGNET [DIR] [--debug | --warning | --error]
deluge_cli get TORRENT
deluge_cli ls [--downloading | --seeding | --paused]
deluge_cli toggle TORRENT
deluge_cli rm TORRENT [--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)
```
### <a name="running"></a> Running
To interface with deluged :
```
$ ./deluge_cli.py ls
```
## <a name="contributing"></a> Contributing
- Fork it!
- Create your feature branch: git checkout -b my-new-feature

View File

@@ -1,22 +1,22 @@
import os
#!/usr/bin/env python3.10
# -*- 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
def addHandler(handler):
handler.setFormatter(formatter)
logger.addHandler(handler)
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)
fh = logging.FileHandler(join(BASE_DIR, 'deluge_cli.log'))
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.addHandler(ch)
addHandler(fh)

View File

@@ -1,148 +1,177 @@
#!/usr/bin/env python3.6
#!/usr/bin/env python3.10
"""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 utils import ColorizeFilter, BASE_DIR, validHash, convertFilesize
from __version__ import __version__
from __init__ import addHandler
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)
ch.addFilter(ColorizeFilter())
addHandler(ch)
logger = logging.getLogger('deluge_cli')
formatter = logging.Formatter('%(asctime)s %(levelname)8s %(name)s | %(message)s')
fh.setFormatter(formatter)
logger.addHandler(fh)
logger.addHandler(ch)
logger.addFilter(ColorizeFilter())
app = typer.Typer()
deluge = None
def signal_handler(signal, frame):
"""
Handle exit by Keyboardinterrupt
"""
logger.info('\nGood bye!')
sys.exit(0)
global deluge
del deluge
def main():
"""
Main function, parse the input
"""
logger.info('\nGood bye!')
sys.exit(1)
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, 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__':
main()
handleKeyboardInterrupt()
main()

View File

@@ -0,0 +1,4 @@
__version__ = '0.3.2'
if __name__ == '__main__':
print(__version__)

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python3.6
#!/usr/bin/env python3.10
import os
import re
@@ -7,9 +7,9 @@ import logging
import requests
import logging.config
from deluge_client import DelugeRPCClient
from sshtunnel import SSHTunnelForwarder
from delugeUtils import getConfig, BASE_DIR
from deluge_client import DelugeRPCClient, FailedToReconnectException
from sshtunnel import SSHTunnelForwarder, BaseSSHTunnelForwarderError
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):
@@ -33,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')
@@ -45,52 +65,74 @@ class Deluge(object):
torrents.append(Torrent.fromDeluge(torrent))
return torrents
def _connect(self):
logger.info('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
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):
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._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._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 = []
if len(allTorrents):
for torrent in allTorrents:
if query in torrent.name:
if query in torrent.name.lower():
torrentNamesMatchingQuery.append(torrent)
allTorrents = torrentNamesMatchingQuery
@@ -101,34 +143,54 @@ 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
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
if torrent is None:
return
def remove(self, name, destroy=False):
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):
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)
logger.info('Response: {}'.format(str(response)))
response = self.remove(torrent.key, destroy)
logger.debug('Response rm: {}'.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):
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:
@@ -140,23 +202,14 @@ 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()
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.')

View File

@@ -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.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
@@ -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
@@ -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])

19
publish_version?.sh Normal file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/bash
PYPI_VERSION=$(pip3 show delugeClient-kevin | awk '$1 ~ /Version:/ { print $2 }')
SOURCE_VERSION=$(python3 delugeClient/__version__.py)
printf "Source version:\t\t %s\n" $SOURCE_VERSION
printf "Remote PyPi version:\t %s\n" $PYPI_VERSION
function version {
echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }';
}
if [ $(version $SOURCE_VERSION) -gt $(version $PYPI_VERSION) ]; then
echo "Soure is newer than remote, publishing!"
exit 0
else
echo "Source is same or oldre than remote, nothing to do."
exit 1
fi

View File

@@ -1,6 +1,5 @@
colored==1.4.2
colored==1.4.4
deluge-client==1.9.0
docopt==0.6.2
requests==2.25.1
requests==2.28.1
sshtunnel==0.4.0
websockets==9.1
typer==0.7.0

View File

@@ -1,39 +1,43 @@
#!/usr/bin/env python3.10
# -*- 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.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.2',
'deluge-client==1.9.0',
'docopt==0.6.2',
'requests==2.25.1',
'sshtunnel==0.4.0',
'websockets==9.1'
'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',
'License :: OSI Approved :: MIT License',
'Programming Language :: Python :: 3.10',
],
entry_points={
'console_scripts': [
'delugeclient = delugeClient.__main__:main',
],
},
packages=find_packages(),
package_data={
'delugeClient': ['default_config.ini'],
},
python_requires=">=3.6",
}
)