mirror of
				https://github.com/StuffAnThings/qbit_manage.git
				synced 2025-10-31 00:17:27 +08:00 
			
		
		
		
	
						commit
						00ce5c6c30
					
				
					 9 changed files with 91 additions and 23 deletions
				
			
		|  | @ -1,7 +1,7 @@ | ||||||
| --- | --- | ||||||
| repos: | repos: | ||||||
|   - repo: https://github.com/pre-commit/pre-commit-hooks |   - repo: https://github.com/pre-commit/pre-commit-hooks | ||||||
|     rev: v4.6.0 |     rev: v5.0.0 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: trailing-whitespace |       - id: trailing-whitespace | ||||||
|       - id: end-of-file-fixer |       - id: end-of-file-fixer | ||||||
|  | @ -38,12 +38,12 @@ repos: | ||||||
|         name: isort (python) |         name: isort (python) | ||||||
|         args: [--force-single-line-imports, --profile, black] |         args: [--force-single-line-imports, --profile, black] | ||||||
|   - repo: https://github.com/asottile/pyupgrade |   - repo: https://github.com/asottile/pyupgrade | ||||||
|     rev: v3.17.0 |     rev: v3.18.0 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: pyupgrade |       - id: pyupgrade | ||||||
|         args: [--py3-plus] |         args: [--py3-plus] | ||||||
|   - repo: https://github.com/psf/black |   - repo: https://github.com/psf/black | ||||||
|     rev: 24.8.0 |     rev: 24.10.0 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: black |       - id: black | ||||||
|         language_version: python3 |         language_version: python3 | ||||||
|  | @ -60,4 +60,4 @@ repos: | ||||||
|         entry: ./scripts/pre-commit/increase_version.sh |         entry: ./scripts/pre-commit/increase_version.sh | ||||||
|         language: script |         language: script | ||||||
|         pass_filenames: false |         pass_filenames: false | ||||||
|         stages: [commit] |         stages: [pre-commit] | ||||||
|  |  | ||||||
							
								
								
									
										14
									
								
								CHANGELOG
									
										
									
									
									
								
							
							
						
						
									
										14
									
								
								CHANGELOG
									
										
									
									
									
								
							|  | @ -1,11 +1,11 @@ | ||||||
|  | # Requirements Updated | ||||||
|  | humanize==4.11.0 | ||||||
|  | 
 | ||||||
| # New Updates | # New Updates | ||||||
| - Adds new config option `disable_qbt_default_share_limits` to allow qbit_manage to handle share limits and disable qbittorrent's default share limits | - Adds new script to remove cross-seed tag (`scripts/remove_cross-seed_tag.py`) | ||||||
| - Adds new config option `max_orphaned_files_to_delete` to set default safeguards against mass deletion when running remove orphaned. |  | ||||||
| - Adds new environment variables `QBT_LOG_SIZE` and `QBT_LOG_COUNT` to customize log retention (Closes #656) |  | ||||||
| 
 | 
 | ||||||
| # Bug Fixes | # Bug Fixes | ||||||
| - Truncates Recyclebin JSON filename when its too long. (Closes #604) | - List orphaned files when reaches max threshold. (Closes #672) | ||||||
| - Uses Qbittorrent's torrent export to save .torrent files for qbittorrent version > 4.5.0 (Closes #650) | - Removing empty directories now ignores exclude patterns (Closes #624) | ||||||
| - Include orphaned files and recycle bin in the list of folders to ignore when looking for noHL (Closes #660) |  | ||||||
| 
 | 
 | ||||||
| **Full Changelog**: https://github.com/StuffAnThings/qbit_manage/compare/v4.1.10...v4.1.11 | **Full Changelog**: https://github.com/StuffAnThings/qbit_manage/compare/v4.1.11...v4.1.12 | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| { | { | ||||||
|     "master": { |     "master": { | ||||||
|         "qbit": "v4.6.6", |         "qbit": "v5.0.0", | ||||||
|         "qbitapi": "2024.8.65" |         "qbitapi": "2024.9.67" | ||||||
|     }, |     }, | ||||||
|     "develop": { |     "develop": { | ||||||
|         "qbit": "v5.0.0", |         "qbit": "v5.0.0", | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								VERSION
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								VERSION
									
										
									
									
									
								
							|  | @ -1 +1 @@ | ||||||
| 4.1.11 | 4.1.12 | ||||||
|  |  | ||||||
|  | @ -29,7 +29,8 @@ class RemoveOrphaned: | ||||||
|         logger.separator("Checking for Orphaned Files", space=False, border=False) |         logger.separator("Checking for Orphaned Files", space=False, border=False) | ||||||
|         torrent_files = [] |         torrent_files = [] | ||||||
|         orphaned_files = [] |         orphaned_files = [] | ||||||
|         excluded_orphan_files = [] |         excluded_orphan_files = set() | ||||||
|  |         exclude_patterns = [] | ||||||
| 
 | 
 | ||||||
|         root_files = self.executor.submit(util.get_root_files, self.root_dir, self.remote_dir, self.orphaned_dir) |         root_files = self.executor.submit(util.get_root_files, self.root_dir, self.remote_dir, self.orphaned_dir) | ||||||
| 
 | 
 | ||||||
|  | @ -54,11 +55,13 @@ class RemoveOrphaned: | ||||||
|                 exclude_pattern.replace(self.remote_dir, self.root_dir) |                 exclude_pattern.replace(self.remote_dir, self.root_dir) | ||||||
|                 for exclude_pattern in self.config.orphaned["exclude_patterns"] |                 for exclude_pattern in self.config.orphaned["exclude_patterns"] | ||||||
|             ] |             ] | ||||||
|             excluded_orphan_files = [ |  | ||||||
|                 file for file in orphaned_files for exclude_pattern in exclude_patterns if fnmatch(file, exclude_pattern) |  | ||||||
|             ] |  | ||||||
| 
 | 
 | ||||||
|         orphaned_files = set(orphaned_files) - set(excluded_orphan_files) |             for file in orphaned_files: | ||||||
|  |                 for exclude_pattern in exclude_patterns: | ||||||
|  |                     if fnmatch(file, exclude_pattern): | ||||||
|  |                         excluded_orphan_files.add(file) | ||||||
|  | 
 | ||||||
|  |         orphaned_files = orphaned_files - excluded_orphan_files | ||||||
| 
 | 
 | ||||||
|         # Check the threshold before deleting orphaned files |         # Check the threshold before deleting orphaned files | ||||||
|         max_orphaned_files_to_delete = self.config.orphaned.get("max_orphaned_files_to_delete") |         max_orphaned_files_to_delete = self.config.orphaned.get("max_orphaned_files_to_delete") | ||||||
|  | @ -69,6 +72,7 @@ class RemoveOrphaned: | ||||||
|                 "Aborting deletion to avoid accidental data loss." |                 "Aborting deletion to avoid accidental data loss." | ||||||
|             ) |             ) | ||||||
|             self.config.notify(e, "Remove Orphaned", False) |             self.config.notify(e, "Remove Orphaned", False) | ||||||
|  |             logger.debug(f"Orphaned files detected: {orphaned_files}") | ||||||
|             logger.warning(e) |             logger.warning(e) | ||||||
|             return |             return | ||||||
|         elif orphaned_files: |         elif orphaned_files: | ||||||
|  | @ -104,7 +108,9 @@ class RemoveOrphaned: | ||||||
|                 orphaned_parent_path = set(self.executor.map(self.handle_orphaned_files, orphaned_files)) |                 orphaned_parent_path = set(self.executor.map(self.handle_orphaned_files, orphaned_files)) | ||||||
|                 logger.print_line("Removing newly empty directories", self.config.loglevel) |                 logger.print_line("Removing newly empty directories", self.config.loglevel) | ||||||
|                 self.executor.map( |                 self.executor.map( | ||||||
|                     lambda directory: util.remove_empty_directories(directory, self.qbt.get_category_save_paths()), |                     lambda directory: util.remove_empty_directories( | ||||||
|  |                         directory, self.qbt.get_category_save_paths(), exclude_patterns | ||||||
|  |                     ), | ||||||
|                     orphaned_parent_path, |                     orphaned_parent_path, | ||||||
|                 ) |                 ) | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -6,6 +6,7 @@ import os | ||||||
| import shutil | import shutil | ||||||
| import signal | import signal | ||||||
| import time | import time | ||||||
|  | from fnmatch import fnmatch | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| 
 | 
 | ||||||
| import requests | import requests | ||||||
|  | @ -486,7 +487,7 @@ def copy_files(src, dest): | ||||||
|         logger.error(ex) |         logger.error(ex) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def remove_empty_directories(pathlib_root_dir, excluded_paths=None): | def remove_empty_directories(pathlib_root_dir, excluded_paths=None, exclude_patterns=[]): | ||||||
|     """Remove empty directories recursively, optimized version.""" |     """Remove empty directories recursively, optimized version.""" | ||||||
|     pathlib_root_dir = Path(pathlib_root_dir) |     pathlib_root_dir = Path(pathlib_root_dir) | ||||||
|     if excluded_paths is not None: |     if excluded_paths is not None: | ||||||
|  | @ -499,6 +500,14 @@ def remove_empty_directories(pathlib_root_dir, excluded_paths=None): | ||||||
|         if excluded_paths and root_path in excluded_paths: |         if excluded_paths and root_path in excluded_paths: | ||||||
|             continue |             continue | ||||||
| 
 | 
 | ||||||
|  |         exclude_pattern_match = False | ||||||
|  |         for exclude_pattern in exclude_patterns: | ||||||
|  |             if fnmatch(os.path.join(root, ""), exclude_pattern): | ||||||
|  |                 exclude_pattern_match = True | ||||||
|  |                 break | ||||||
|  |         if exclude_pattern_match: | ||||||
|  |             continue | ||||||
|  | 
 | ||||||
|         # Attempt to remove the directory if it's empty |         # Attempt to remove the directory if it's empty | ||||||
|         try: |         try: | ||||||
|             os.rmdir(root) |             os.rmdir(root) | ||||||
|  |  | ||||||
|  | @ -1,2 +1,2 @@ | ||||||
| flake8==7.1.1 | flake8==7.1.1 | ||||||
| pre-commit==3.8.0 | pre-commit==4.0.1 | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| bencodepy==0.9.5 | bencodepy==0.9.5 | ||||||
| croniter==3.0.3 | croniter==3.0.3 | ||||||
| GitPython==3.1.43 | GitPython==3.1.43 | ||||||
| humanize==4.10.0 | humanize==4.11.0 | ||||||
| pytimeparse2==1.7.1 | pytimeparse2==1.7.1 | ||||||
| qbittorrent-api==2024.9.67 | qbittorrent-api==2024.9.67 | ||||||
| requests==2.32.3 | requests==2.32.3 | ||||||
|  |  | ||||||
							
								
								
									
										53
									
								
								scripts/remove_cross-seed_tag.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								scripts/remove_cross-seed_tag.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,53 @@ | ||||||
|  | #!/usr/bin/env python3 | ||||||
|  | import os | ||||||
|  | 
 | ||||||
|  | # USES ENVIRONMENTAL VARIABLES, IF NONE ARE PRESENT WILL FALLBACK TO THE SECOND STRING | ||||||
|  | QBIT_HOST = os.getenv("QBT_HOST", "http://localhost:8080") | ||||||
|  | QBIT_USERNAME = os.getenv("QBT_USERNAME", "admin") | ||||||
|  | QBIT_PASSWORD = os.getenv("QBT_PASSWORD", "YOURPASSWORD") | ||||||
|  | 
 | ||||||
|  | CRED = "\033[91m" | ||||||
|  | CGREEN = "\33[32m" | ||||||
|  | CEND = "\033[0m" | ||||||
|  | 
 | ||||||
|  | CROSS_SEED_TAG = "cross-seed" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def split(separator, data): | ||||||
|  |     if data is None: | ||||||
|  |         return None | ||||||
|  |     else: | ||||||
|  |         return [item.strip() for item in str(data).split(separator)] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | try: | ||||||
|  |     from qbittorrentapi import APIConnectionError | ||||||
|  |     from qbittorrentapi import Client | ||||||
|  |     from qbittorrentapi import LoginFailed | ||||||
|  | except ModuleNotFoundError: | ||||||
|  |     print('Error: qbittorrent-api not installed. Please install using the command "pip install qbittorrent-api"') | ||||||
|  |     exit(1) | ||||||
|  | 
 | ||||||
|  | try: | ||||||
|  |     qbt_client = Client(host=QBIT_HOST, username=QBIT_USERNAME, password=QBIT_PASSWORD) | ||||||
|  | except LoginFailed: | ||||||
|  |     raise "Qbittorrent Error: Failed to login. Invalid username/password." | ||||||
|  | except APIConnectionError: | ||||||
|  |     raise "Qbittorrent Error: Unable to connect to the client." | ||||||
|  | except Exception: | ||||||
|  |     raise "Qbittorrent Error: Unable to connect to the client." | ||||||
|  | print("qBittorrent:", qbt_client.app_version()) | ||||||
|  | print("qBittorrent Web API:", qbt_client.app_web_api_version()) | ||||||
|  | print() | ||||||
|  | 
 | ||||||
|  | torrents_list = qbt_client.torrents.info(sort="added_on", reverse=True) | ||||||
|  | 
 | ||||||
|  | print("Total torrents:", len(torrents_list)) | ||||||
|  | print() | ||||||
|  | 
 | ||||||
|  | for torrent in torrents_list: | ||||||
|  |     torrent_tags = split(",", torrent.tags) | ||||||
|  | 
 | ||||||
|  |     if CROSS_SEED_TAG in torrent_tags: | ||||||
|  |         print(CGREEN, "remove cross-seed tag:", torrent.name, CEND) | ||||||
|  |         torrent.remove_tags(tags=CROSS_SEED_TAG) | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue