mirror of
				https://github.com/KevinMidboe/delugeClient.git
				synced 2025-10-29 12:00:13 +00:00 
			
		
		
		
	Compare commits
	
		
			36 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| adf8f3f1ef | |||
| 8f2327c065 | |||
| 9bfa3c4c59 | |||
| 078d422498 | |||
| a6fb543b3f | |||
| 519b51c47c | |||
| 6fd63ff348 | |||
| 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 | 
							
								
								
									
										90
									
								
								.drone.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								.drone.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | |||||||
|  | --- | ||||||
|  | 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 verify | ||||||
|  |     image: python:3.10 | ||||||
|  |     commands: | ||||||
|  |       - make dist | ||||||
|  |       - pip3 install twine | ||||||
|  |       - twine check dist/* | ||||||
|  |  | ||||||
|  |   - name: PyPi test publish | ||||||
|  |     image: python:3.10 | ||||||
|  |     environment: | ||||||
|  |       TWINE_USERNAME: | ||||||
|  |         from_secret: TWINE_USERNAME | ||||||
|  |       TWINE_PASSWORD: | ||||||
|  |         from_secret: TWINE_TEST_PASSWORD | ||||||
|  |     commands: | ||||||
|  |       - make dist | ||||||
|  |       - pip3 install twine | ||||||
|  |       - twine upload --repository-url https://test.pypi.org/legacy/ dist/* | ||||||
|  |  | ||||||
|  |   - name: PyPi publish | ||||||
|  |     image: python:3.10 | ||||||
|  |     environment: | ||||||
|  |       TWINE_USERNAME: | ||||||
|  |         from_secret: TWINE_USERNAME | ||||||
|  |       TWINE_PASSWORD: | ||||||
|  |         from_secret: TWINE_PASSWORD | ||||||
|  |     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: 60604a21f35e11d078d5d381bbea8e25b903175c018ba9e6f4a4379285e89883 | ||||||
|  |  | ||||||
|  | ... | ||||||
							
								
								
									
										22
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								Makefile
									
									
									
									
									
										Normal 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) | ||||||
							
								
								
									
										123
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										123
									
								
								README.md
									
									
									
									
									
								
							| @@ -4,23 +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> | <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 | License | | ||||||
|   <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/) |[](LICENSE) | ||||||
|   </a> |  | ||||||
|    | | Drone CI | Known vulnerabilities | | ||||||
|   <a href="https://opensource.org/licenses/MIT"> | |:--------|:------| | ||||||
|     <img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt=""> | | [](https://drone.schleppe.cloud/KevinMidboe/delugeClient) | [](https://snyk.io/test/github/kevinmidboe/delugeClient?targetFile=requirements.txt) | ||||||
|   </a> |  | ||||||
| </p> |  | ||||||
|  |  | ||||||
| <p align="center"> | <p align="center"> | ||||||
|   <a href="#abstract">Abstract</a> • |   <a href="#abstract">Abstract</a> • | ||||||
|   <a href="#setup_virtualenv">Setup virtualenv</a> • |   <a href="#install">Install</a> • | ||||||
|   <a href="#configure">Configure</a> • |  | ||||||
|   <a href="#installation">Install dependencies</a> • |  | ||||||
|   <a href="#usage">Usage</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> |   <a href="#contributing">Contributing</a> | ||||||
| </p> | </p> | ||||||
|  |  | ||||||
| @@ -28,6 +25,51 @@ | |||||||
| ## <a name="abstract"></a> Abstract | ## <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.  | 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     │ | ||||||
|  | │ --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                                │ | ||||||
|  | ╰─────────────────────────────────────────────────────────────────────────╯ | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ### Running from source | ||||||
|  | Run from source for fun or during development using module flag: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | python3 -m delugeClient --help | ||||||
|  | ``` | ||||||
|  |  | ||||||
| ## <a name="setup_virtualenv"></a> Setup Virtual Environment | ## <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. | 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. | ||||||
| @@ -41,7 +83,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: | After you have downloaded this project go to it in your terminal by going to the folder you downloaded and typing the following: | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -52,14 +94,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: | 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: | 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: | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
| @@ -96,54 +136,9 @@ Then you need to change the HOST and PORT to reflect the address for your deluge | |||||||
|  $ cat /home/USER/.config/deluge/auth |  $ 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 | ## <a name="contributing"></a> Contributing | ||||||
| - Fork it! | - Fork it! | ||||||
| - 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 |  | ||||||
							
								
								
									
										24
									
								
								delugeClient/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								delugeClient/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | #!/usr/bin/env python3.10 | ||||||
|  | # -*- encoding: utf-8 -*- | ||||||
|  |  | ||||||
|  | from sys import path | ||||||
|  | from os.path import dirname, join, abspath | ||||||
|  |  | ||||||
|  | SCRIPT_DIR = dirname(abspath(__file__)) | ||||||
|  | path.append(dirname(SCRIPT_DIR)) | ||||||
|  |  | ||||||
|  | import logging | ||||||
|  | from delugeClient.utils import BASE_DIR | ||||||
|  | from delugeClient.deluge import Deluge | ||||||
|  |  | ||||||
|  | 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) | ||||||
							
								
								
									
										176
									
								
								delugeClient/__main__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								delugeClient/__main__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,176 @@ | |||||||
|  | #!/usr/bin/env python3.10 | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | import signal | ||||||
|  | import logging | ||||||
|  |  | ||||||
|  | import typer | ||||||
|  | from pprint import pprint | ||||||
|  |  | ||||||
|  | from delugeClient.deluge import Deluge | ||||||
|  | from delugeClient.utils import ColorizeFilter, BASE_DIR, validHash, convertFilesize | ||||||
|  | from delugeClient.__version__ import __version__ | ||||||
|  | from delugeClient.__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() | ||||||
							
								
								
									
										4
									
								
								delugeClient/__version__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								delugeClient/__version__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | __version__ = '0.3.5' | ||||||
|  |  | ||||||
|  | if __name__ == '__main__': | ||||||
|  |   print(__version__) | ||||||
							
								
								
									
										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 delugeClient.utils import getConfig, BASE_DIR | ||||||
|  | from delugeClient.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 delugeClient.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.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 @@ | |||||||
|  |  | ||||||
							
								
								
									
										309
									
								
								deluge_cli.py
									
									
									
									
									
								
							
							
						
						
									
										309
									
								
								deluge_cli.py
									
									
									
									
									
								
							| @@ -1,309 +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 [--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 |  | ||||||
|    --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, 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.client.call('core.remove_torrent', torrent.key, destroy) |  | ||||||
|          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']: |  | ||||||
|       destroy = arguments['--destroy'] |  | ||||||
|       logger.info('Remove by name: {}.'.format(name)) |  | ||||||
|       if destroy: |  | ||||||
|          logger.info('Destroy set, removing files') |  | ||||||
|       deluge.remove(name, destroy) |  | ||||||
|  |  | ||||||
| if __name__ == '__main__': |  | ||||||
|    main() |  | ||||||
|  |  | ||||||
							
								
								
									
										19
									
								
								publish_version?.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								publish_version?.sh
									
									
									
									
									
										Normal 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 | ||||||
							
								
								
									
										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,14 +1,5 @@ | |||||||
| asn1crypto==0.24.0 | colored==1.4.4 | ||||||
| bcrypt==3.1.4 | deluge-client==1.9.0 | ||||||
| cffi==1.11.5 | requests==2.28.1 | ||||||
| colored==1.3.5 | sshtunnel==0.4.0 | ||||||
| cryptography==2.5 | typer==0.7.0 | ||||||
| deluge-client==1.6.0 |  | ||||||
| docopt==0.6.2 |  | ||||||
| idna==2.7 |  | ||||||
| pyasn1==0.4.4 |  | ||||||
| pycparser==2.18 |  | ||||||
| PyNaCl==1.2.1 |  | ||||||
| six==1.11.0 |  | ||||||
| sshtunnel==0.1.4 |  | ||||||
| websockets==6.0 |  | ||||||
|   | |||||||
							
								
								
									
										43
									
								
								setup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								setup.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | |||||||
|  | #!/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', | ||||||
|  |     'License :: OSI Approved :: MIT License', | ||||||
|  |     '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('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 |  | ||||||
		Reference in New Issue
	
	Block a user