mirror of
				https://github.com/KevinMidboe/delugeClient.git
				synced 2025-10-29 12:00:13 +00:00 
			
		
		
		
	Compare commits
	
		
			43 Commits
		
	
	
		
			socket
			...
			snyk-fix-b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 7cbadf9e27 | ||
| 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 | |||
| 57b1b4e1f2 | |||
| fada382a32 | |||
| 8d943cb1ad | |||
| bd516f34d8 | |||
| cfe14dd1ba | |||
| 5e39ecb8bc | 
							
								
								
									
										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 | ||||
|  | ||||
| ... | ||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1,5 @@ | ||||
| deluge_cli.log | ||||
|  | ||||
| # Byte-compiled / optimized / DLL files | ||||
| __pycache__/ | ||||
| *.py[cod] | ||||
|   | ||||
							
								
								
									
										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) | ||||
							
								
								
									
										119
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										119
									
								
								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> | ||||
|  | ||||
| <p align="center"> | ||||
|   <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> | ||||
| | Tested version | PyPi package | License | | ||||
| |:--------|:------|:------| | ||||
| | [](https://www.python.org/downloads/release/python-3100/) | [](https://pypi.org/project/delugeClient_kevin/) |[](LICENSE) | ||||
|  | ||||
|   <a href="https://opensource.org/licenses/MIT"> | ||||
|     <img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt=""> | ||||
|   </a> | ||||
| </p> | ||||
| | Drone CI | Known vulnerabilities | | ||||
| |:--------|:------| | ||||
| | [](https://drone.schleppe.cloud/KevinMidboe/delugeClient) | [](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> | ||||
|  | ||||
| @@ -28,6 +25,51 @@ | ||||
| ## <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     │ | ||||
| │ --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 | ||||
| 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: | ||||
|  | ||||
|  | ||||
| @@ -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: | ||||
|  | ||||
| ``` | ||||
|  $ 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: | ||||
|  | ||||
| ``` | ||||
| @@ -96,51 +136,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 | ||||
|   | ||||
							
								
								
									
										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 @@ | ||||
|  | ||||
							
								
								
									
										306
									
								
								deluge_cli.py
									
									
									
									
									
								
							
							
						
						
									
										306
									
								
								deluge_cli.py
									
									
									
									
									
								
							| @@ -1,306 +0,0 @@ | ||||
| #!/usr/bin/env python3.6 | ||||
|  | ||||
|  | ||||
| """Custom delugeRPC client | ||||
| Usage: | ||||
|    deluge_cli add MAGNET [DIR] [--debug | --warning | --error] | ||||
|    deluge_cli search NAME | ||||
|    deluge_cli get  TORRENT | ||||
|    deluge_cli ls [--downloading | --seeding | --paused] | ||||
|    deluge_cli toggle TORRENT | ||||
|    deluge_cli progress | ||||
|    deluge_cli rm NAME [--debug | --warning | --error] | ||||
|    deluge_cli (-h | --help) | ||||
|    deluge_cli --version | ||||
|  | ||||
| Arguments: | ||||
|    MAGNET        Magnet link to add | ||||
|    DIR           Directory to save to | ||||
|    TORRENT       A selected torrent | ||||
|  | ||||
| Options: | ||||
|    -h --help     Show this screen | ||||
|    --version     Show version | ||||
|    --debug       Print all debug log | ||||
|    --warning     Print only logged warnings | ||||
|    --error       Print error messages (Error/Warning) | ||||
| """ | ||||
|  | ||||
| import argparse | ||||
| import os | ||||
| import sys | ||||
| import re | ||||
| import signal | ||||
| import socket | ||||
| import logging | ||||
| import logging.config | ||||
| import configparser | ||||
|  | ||||
| from distutils.util import strtobool | ||||
| from pprint import pprint | ||||
|  | ||||
| from deluge_client import DelugeRPCClient | ||||
| from sshtunnel import SSHTunnelForwarder | ||||
| from docopt import docopt | ||||
| from .utils import ColorizeFilter, convert | ||||
|  | ||||
| BASE_DIR = os.path.dirname(os.path.abspath(__file__)) | ||||
|  | ||||
| logger = logging.getLogger('deluge_cli') | ||||
| logger.setLevel(logging.DEBUG) | ||||
|  | ||||
| fh = logging.FileHandler(os.path.join(BASE_DIR, 'deluge_cli.log')) | ||||
| fh.setLevel(logging.DEBUG) | ||||
| ch = logging.StreamHandler() | ||||
| ch.setLevel(logging.ERROR) | ||||
|  | ||||
| formatter = logging.Formatter('%(asctime)s %(levelname)8s %(name)s | %(message)s') | ||||
| fh.setFormatter(formatter) | ||||
| logger.addHandler(fh) | ||||
| logger.addHandler(ch) | ||||
|  | ||||
| logger.addFilter(ColorizeFilter())    | ||||
|  | ||||
|  | ||||
| def getConfig(): | ||||
|    """ | ||||
|    Read path and get configuartion file with site settings | ||||
|    :return: config settings read from 'config.ini' | ||||
|    :rtype: configparser.ConfigParser | ||||
|    """ | ||||
|    config = configparser.ConfigParser() | ||||
|    config_dir = os.path.join(BASE_DIR, 'config.ini') | ||||
|    config.read(config_dir) | ||||
|  | ||||
|    config_values = list(dict(config.items('Deluge')).values()) | ||||
|    config_values.extend(list(dict(config.items('ssh')).values())) | ||||
|  | ||||
|    if any(value.startswith('YOUR') for value in config_values): | ||||
|       raise ValueError('Please set variables in config.ini file.') | ||||
|  | ||||
|    return config | ||||
|  | ||||
| def split_words(string): | ||||
|    logger.debug('Splitting input: {} (type: {}) with split_words'.format(string, type(string))) | ||||
|    return re.findall(r"[\w\d']+", string.lower()) | ||||
|  | ||||
| class Deluge(object): | ||||
|    """docstring for ClassName""" | ||||
|    def __init__(self): | ||||
|       config = getConfig() | ||||
|       self.host = config['Deluge']['HOST'] | ||||
|       self.port = int(config['Deluge']['PORT']) | ||||
|       self.user = config['Deluge']['USER'] | ||||
|       self.password = config['Deluge']['PASSWORD'] | ||||
|  | ||||
|       self.ssh_host = config['ssh']['HOST'] | ||||
|       self.ssh_user = config['ssh']['USER'] | ||||
|       self.ssh_pkey = config['ssh']['PKEY'] | ||||
|     | ||||
|       self._connect() | ||||
|  | ||||
|    def parseResponse(self, response): | ||||
|       torrents = [] | ||||
|       for key in response: | ||||
|          torrent = response[key] | ||||
|          torrents.append(Torrent.fromDeluge(torrent)) | ||||
|       return torrents | ||||
|  | ||||
|    def _connect(self): | ||||
|       logger.info('Checking if script on same server as deluge RPC') | ||||
|       if (socket.gethostbyname(socket.gethostname()) != self.host): | ||||
|          self.tunnel = SSHTunnelForwarder(self.ssh_host, ssh_username=self.ssh_user, ssh_pkey=self.ssh_pkey,  | ||||
|             local_bind_address=('localhost', self.port), remote_bind_address=('localhost', self.port)) | ||||
|          self.tunnel.start() | ||||
|  | ||||
|       self.client = DelugeRPCClient(self.host, self.port, self.user, self.password) | ||||
|       self.client.connect() | ||||
|  | ||||
|    def add(self, url): | ||||
|       if (url.startswith('magnet')): | ||||
|          return self.client.call('core.add_torrent_magnet', url, {}) | ||||
|  | ||||
|    def get_all(self, _filter=None): | ||||
|       if (type(_filter) is list and len(_filter)): | ||||
|          if ('seeding' in _filter): | ||||
|             response = self.client.call('core.get_torrents_status', {'state': 'Seeding'}, []) | ||||
|          elif ('downloading' in _filter): | ||||
|             response = self.client.call('core.get_torrents_status', {'state': 'Downloading'}, []) | ||||
|          elif ('paused' in _filter): | ||||
|             response = self.client.call('core.get_torrents_status', {'paused': 'true'}, []) | ||||
|       else: | ||||
|          response = self.client.call('core.get_torrents_status', {}, []) | ||||
|  | ||||
|       return self.parseResponse(response) | ||||
|  | ||||
|    def search(self, query): | ||||
|       q_list = split_words(query) | ||||
|       return [ t for t in self.get_all() if (set(q_list) <= set(split_words(t.name))) ] | ||||
|  | ||||
|    def get(self, id): | ||||
|       response = self.client.call('core.get_torrent_status', id, {}) | ||||
|       return Torrent.fromDeluge(response) | ||||
|  | ||||
|    def togglePaused(self, id): | ||||
|       torrent = self.get(id) | ||||
|       if (torrent.paused): | ||||
|          response = self.client.call('core.resume_torrent', [id]) | ||||
|       else: | ||||
|          response = self.client.call('core.pause_torrent', [id]) | ||||
|        | ||||
|       print('Response:', response) | ||||
|  | ||||
|    def remove(self, name): | ||||
|       matches = list(filter(lambda t: t.name == name, self.get_all())) | ||||
|       logger.info('Matches for {}: {}'.format(name, matches)) | ||||
|        | ||||
|       if (len(matches) > 1): | ||||
|          raise ValueError('Multiple files found matching key. Unable to remove.') | ||||
|       elif (len(matches) == 1): | ||||
|          torrent = matches[0] | ||||
|          response = self.client.call('core.remove_torrent', torrent.key, False) | ||||
|          logger.info('Response: {}'.format(str(response))) | ||||
|  | ||||
|          if (response == False): | ||||
|             raise AttributeError('Unable to remove torrent.') | ||||
|          return response | ||||
|       else: | ||||
|          logger.error('ERROR. No torrent found with that name.') | ||||
|  | ||||
|    def filterOnValue(self, torrents, value): | ||||
|       filteredTorrents = [] | ||||
|       for t in torrents: | ||||
|          value_template = {'key': None, 'name': None, value: None} | ||||
|          value_template['key'] = t.key | ||||
|          value_template['name'] = t.name | ||||
|          value_template[value] = getattr(t, value) | ||||
|           | ||||
|          filteredTorrents.append(value_template) | ||||
|       return filteredTorrents | ||||
|  | ||||
|    def progress(self): | ||||
|       attributes = ['progress', 'eta', 'state', 'finished'] | ||||
|       all_torrents = self.get_all() | ||||
|  | ||||
|       torrents = [] | ||||
|       for i, attribute in enumerate(attributes): | ||||
|          if i < 1: | ||||
|             torrents = self.filterOnValue(all_torrents, attribute) | ||||
|             continue | ||||
|          torrents = [dict(e, **v) for e,v in zip(torrents, self.filterOnValue(all_torrents, attribute))] | ||||
|  | ||||
|       return torrents | ||||
|  | ||||
|    def __del__(self): | ||||
|       if hasattr(self, 'tunnel'): | ||||
|          logger.info('Closing ssh tunnel') | ||||
|          self.tunnel.stop() | ||||
|  | ||||
| class Torrent(object): | ||||
|    def __init__(self, key, name, progress, eta, save_path, state, paused, finished, files): | ||||
|       super(Torrent, self).__init__() | ||||
|       self.key = key | ||||
|       self.name = name | ||||
|       self.progress = "{0:.2f}".format(float(progress)) | ||||
|       self.eta = eta | ||||
|       self.save_path = save_path | ||||
|       self.state = state | ||||
|       self.paused = paused | ||||
|       self.finished = finished | ||||
|       self.files = list(files) | ||||
|  | ||||
|    def isFolder(self): | ||||
|       return len(self.files) > 1 | ||||
|  | ||||
|    def toBool(self, value): | ||||
|       return True if strtobool(value) else False | ||||
|  | ||||
|    @classmethod | ||||
|    def fromDeluge(cls, d): | ||||
|       # Receive a dict with byte values, convert all elements to string values | ||||
|       d = convert(d) | ||||
|       toBool = lambda val: True if strtobool(val) else False | ||||
|       return cls(d['hash'], d['name'], d['progress'], d['eta'], d['save_path'], d['state'],  | ||||
|                  toBool(d['paused']), toBool(d['is_finished']), d['files']) | ||||
|  | ||||
|    def toJSON(self): | ||||
|       return {'key': self.key, 'name': self.name, 'progress': self.progress, 'eta': self.eta, | ||||
|               'save_path': self.save_path, 'state': self.state, 'paused': self.paused, | ||||
|               'finished': self.finished, 'files': self.files, 'is_folder': self.isFolder()} | ||||
|  | ||||
|    def __str__(self): | ||||
|       return "Name: {}, Progress: {}%, ETA: {}, State: {}, Paused: {}".format( | ||||
|          self.name, self.progress, self.eta, self.state, self.paused) | ||||
|  | ||||
| def signal_handler(signal, frame): | ||||
|    """ | ||||
|    Handle exit by Keyboardinterrupt | ||||
|    """ | ||||
|    logger.info('\nGood bye!') | ||||
|    sys.exit(0) | ||||
|  | ||||
| def main(): | ||||
|    """ | ||||
|    Main function, parse the input | ||||
|    """ | ||||
|    signal.signal(signal.SIGINT, signal_handler) | ||||
|  | ||||
|    arguments = docopt(__doc__, version='1') | ||||
|  | ||||
|    # Set logging level for streamHandler | ||||
|    if arguments['--debug']: | ||||
|       ch.setLevel(logging.DEBUG) | ||||
|    elif arguments['--warning']: | ||||
|       ch.setLevel(logging.WARNING) | ||||
|    elif arguments['--error']: | ||||
|       ch.setLevel(logging.ERROR) | ||||
|  | ||||
|    logger.info('Deluge client') | ||||
|    logger.debug(arguments) | ||||
|  | ||||
|    # Get config settings | ||||
|    deluge = Deluge() | ||||
|  | ||||
|    _id = arguments['TORRENT'] | ||||
|    query = arguments['NAME'] | ||||
|    magnet = arguments['MAGNET'] | ||||
|    name = arguments['NAME'] | ||||
|    _filter = [ a[2:] for a in ['--downloading', '--seeding', '--paused'] if arguments[a] ] | ||||
|    print(_id, query, _filter) | ||||
|  | ||||
|    if arguments['add']: | ||||
|       logger.info('Add cmd selected with link {}'.format(magnet)) | ||||
|       response = deluge.add(magnet) | ||||
|       print('Add response: ', response) | ||||
|  | ||||
|    elif arguments['search']: | ||||
|       logger.info('Search cmd selected for query: {}'.format(query)) | ||||
|       response = deluge.search(query) | ||||
|       [ pprint(t.toJSON()) for t in response ] | ||||
|  | ||||
|    elif arguments['progress']: | ||||
|       logger.info('Progress cmd selected.') | ||||
|       pprint(deluge.progress()) | ||||
|       exit(0) | ||||
|       [ pprint(t.toJSON()) for t in deluge.progress() ] | ||||
|  | ||||
|    elif arguments['get']: | ||||
|       logger.info('Get cmd selected for id: {}'.format(_id)) | ||||
|       response = deluge.get(_id) | ||||
|       pprint(response.toJSON()) | ||||
|  | ||||
|    elif arguments['ls']: | ||||
|       logger.info('List cmd selected') | ||||
|       [ pprint(t.toJSON()) for t in deluge.get_all(_filter=_filter) ] | ||||
|  | ||||
|    elif arguments['toggle']: | ||||
|       logger.info('Toggling id: {}'.format(_id)) | ||||
|       deluge.togglePaused(_id) | ||||
|  | ||||
|    elif arguments['rm']: | ||||
|       logger.info('Remove by name: {}'.format(name)) | ||||
|       deluge.remove(name) | ||||
|  | ||||
| if __name__ == '__main__': | ||||
|    main() | ||||
|  | ||||
							
								
								
									
										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,15 +1,6 @@ | ||||
| asn1crypto==0.24.0 | ||||
| bcrypt==3.1.4 | ||||
| cffi==1.11.5 | ||||
| colored==1.3.5 | ||||
| cryptography==2.3 | ||||
| deluge-client==1.6.0 | ||||
| docopt==0.6.2 | ||||
| idna==2.7 | ||||
| paramiko==2.4.1 | ||||
| pyasn1==0.4.4 | ||||
| pycparser==2.18 | ||||
| PyNaCl==1.2.1 | ||||
| six==1.11.0 | ||||
| sshtunnel==0.1.4 | ||||
| websockets==6.0 | ||||
| colored==1.4.4 | ||||
| deluge-client==1.9.0 | ||||
| requests==2.28.1 | ||||
| sshtunnel==0.4.0 | ||||
| typer==0.7.0 | ||||
| zipp>=3.19.1 # not directly required, pinned by Snyk to avoid a vulnerability | ||||
|   | ||||
							
								
								
									
										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('ascii') | ||||
|     if isinstance(data, dict):   return dict(map(convert, data.items())) | ||||
|     if isinstance(data, tuple):  return map(convert, data) | ||||
|     json_data = json.dumps(data) | ||||
|     return json_data | ||||
		Reference in New Issue
	
	Block a user