diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 33a5ba1..d12bc9a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -38,12 +38,12 @@ repos: name: isort (python) args: [--force-single-line-imports, --profile, black] - repo: https://github.com/asottile/pyupgrade - rev: v3.17.0 + rev: v3.18.0 hooks: - id: pyupgrade args: [--py3-plus] - repo: https://github.com/psf/black - rev: 24.8.0 + rev: 24.10.0 hooks: - id: black language_version: python3 @@ -60,4 +60,4 @@ repos: entry: ./scripts/pre-commit/increase_version.sh language: script pass_filenames: false - stages: [commit] + stages: [pre-commit] diff --git a/CHANGELOG b/CHANGELOG index 92685d9..6346067 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,11 +1,11 @@ +# Requirements Updated +humanize==4.11.0 + # 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 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) +- Adds new script to remove cross-seed tag (`scripts/remove_cross-seed_tag.py`) # Bug Fixes -- Truncates Recyclebin JSON filename when its too long. (Closes #604) -- Uses Qbittorrent's torrent export to save .torrent files for qbittorrent version > 4.5.0 (Closes #650) -- Include orphaned files and recycle bin in the list of folders to ignore when looking for noHL (Closes #660) +- List orphaned files when reaches max threshold. (Closes #672) +- Removing empty directories now ignores exclude patterns (Closes #624) -**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 diff --git a/SUPPORTED_VERSIONS.json b/SUPPORTED_VERSIONS.json index 8cf4fdd..8bcf3e5 100644 --- a/SUPPORTED_VERSIONS.json +++ b/SUPPORTED_VERSIONS.json @@ -1,7 +1,7 @@ { "master": { - "qbit": "v4.6.6", - "qbitapi": "2024.8.65" + "qbit": "v5.0.0", + "qbitapi": "2024.9.67" }, "develop": { "qbit": "v5.0.0", diff --git a/VERSION b/VERSION index 152e452..b05079e 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.1.11 +4.1.12 diff --git a/modules/core/remove_orphaned.py b/modules/core/remove_orphaned.py index f25022d..b4fe7fb 100644 --- a/modules/core/remove_orphaned.py +++ b/modules/core/remove_orphaned.py @@ -29,7 +29,8 @@ class RemoveOrphaned: logger.separator("Checking for Orphaned Files", space=False, border=False) torrent_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) @@ -54,11 +55,13 @@ class RemoveOrphaned: exclude_pattern.replace(self.remote_dir, self.root_dir) 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 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." ) self.config.notify(e, "Remove Orphaned", False) + logger.debug(f"Orphaned files detected: {orphaned_files}") logger.warning(e) return elif orphaned_files: @@ -104,7 +108,9 @@ class RemoveOrphaned: orphaned_parent_path = set(self.executor.map(self.handle_orphaned_files, orphaned_files)) logger.print_line("Removing newly empty directories", self.config.loglevel) 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, ) diff --git a/modules/util.py b/modules/util.py index d028e6d..b32b084 100755 --- a/modules/util.py +++ b/modules/util.py @@ -6,6 +6,7 @@ import os import shutil import signal import time +from fnmatch import fnmatch from pathlib import Path import requests @@ -486,7 +487,7 @@ def copy_files(src, dest): 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.""" pathlib_root_dir = Path(pathlib_root_dir) 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: 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 try: os.rmdir(root) diff --git a/requirements-dev.txt b/requirements-dev.txt index 325e4be..6072bcb 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,2 +1,2 @@ flake8==7.1.1 -pre-commit==3.8.0 +pre-commit==4.0.1 diff --git a/requirements.txt b/requirements.txt index b6a645e..d8661da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ bencodepy==0.9.5 croniter==3.0.3 GitPython==3.1.43 -humanize==4.10.0 +humanize==4.11.0 pytimeparse2==1.7.1 qbittorrent-api==2024.9.67 requests==2.32.3 diff --git a/scripts/remove_cross-seed_tag.py b/scripts/remove_cross-seed_tag.py new file mode 100644 index 0000000..bd4daef --- /dev/null +++ b/scripts/remove_cross-seed_tag.py @@ -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)