diff --git a/modules/core/remove_orphaned.py b/modules/core/remove_orphaned.py new file mode 100644 index 0000000..2d14f6c --- /dev/null +++ b/modules/core/remove_orphaned.py @@ -0,0 +1,102 @@ +import os +from fnmatch import fnmatch + +from modules import util + +logger = util.logger + + +class RemoveOrphaned: + def __init__(self, qbit_manager): + self.qbt = qbit_manager + self.config = qbit_manager.config + self.client = qbit_manager.client + self.stats = 0 + + self.remote_dir = qbit_manager.config.remote_dir + self.root_dir = qbit_manager.config.root_dir + self.orphaned_dir = qbit_manager.config.orphaned_dir + + self.rem_orphaned() + + def rem_orphaned(self): + """Remove orphaned files from remote directory""" + self.stats = 0 + logger.separator("Checking for Orphaned Files", space=False, border=False) + torrent_files = [] + root_files = [] + orphaned_files = [] + excluded_orphan_files = [] + orphaned_parent_path = set() + + if self.remote_dir != self.root_dir: + root_files = [ + os.path.join(path.replace(self.remote_dir, self.root_dir), name) + for path, subdirs, files in os.walk(self.remote_dir) + for name in files + if self.orphaned_dir.replace(self.remote_dir, self.root_dir) not in path + ] + else: + root_files = [ + os.path.join(path, name) + for path, subdirs, files in os.walk(self.root_dir) + for name in files + if self.orphaned_dir.replace(self.root_dir, self.remote_dir) not in path + ] + + # Get an updated list of torrents + torrent_list = self.qbt.get_torrents({"sort": "added_on"}) + for torrent in torrent_list: + for file in torrent.files: + fullpath = os.path.join(torrent.save_path, file.name) + # Replace fullpath with \\ if qbm is running in docker (linux) but qbt is on windows + fullpath = fullpath.replace(r"/", "\\") if ":\\" in fullpath else fullpath + torrent_files.append(fullpath) + + orphaned_files = set(root_files) - set(torrent_files) + orphaned_files = sorted(orphaned_files) + + if self.config.orphaned["exclude_patterns"]: + exclude_patterns = 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.replace(self.remote_dir, self.root_dir)) + ] + + orphaned_files = set(orphaned_files) - set(excluded_orphan_files) + + if orphaned_files: + os.makedirs(self.orphaned_dir, exist_ok=True) + body = [] + num_orphaned = len(orphaned_files) + logger.print_line(f"{num_orphaned} Orphaned files found", self.config.loglevel) + body += logger.print_line("\n".join(orphaned_files), self.config.loglevel) + body += logger.print_line( + f"{'Did not move' if self.config.dry_run else 'Moved'} {num_orphaned} Orphaned files " + f"to {self.orphaned_dir.replace(self.remote_dir,self.root_dir)}", + self.config.loglevel, + ) + + attr = { + "function": "rem_orphaned", + "title": f"Removing {num_orphaned} Orphaned Files", + "body": "\n".join(body), + "orphaned_files": list(orphaned_files), + "orphaned_directory": self.orphaned_dir.replace(self.remote_dir, self.root_dir), + "total_orphaned_files": num_orphaned, + } + self.config.send_notifications(attr) + # Delete empty directories after moving orphan files + logger.info("Cleaning up any empty directories...") + if not self.config.dry_run: + for file in orphaned_files: + src = file.replace(self.root_dir, self.remote_dir) + dest = os.path.join(self.orphaned_dir, file.replace(self.root_dir, "")) + util.move_files(src, dest, True) + orphaned_parent_path.add(os.path.dirname(file).replace(self.root_dir, self.remote_dir)) + for parent_path in orphaned_parent_path: + util.remove_empty_directories(parent_path, "**/*") + else: + logger.print_line("No Orphaned Files found.", self.config.loglevel) diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index b2b6f98..3effec6 100755 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -2,7 +2,6 @@ import os import sys from datetime import timedelta -from fnmatch import fnmatch from qbittorrentapi import APIConnectionError from qbittorrentapi import Client @@ -564,92 +563,6 @@ class Qbt: logger.warning(e) return category - def rem_orphaned(self): - """Remove orphaned files from remote directory""" - orphaned = 0 - if self.config.commands["rem_orphaned"]: - logger.separator("Checking for Orphaned Files", space=False, border=False) - torrent_files = [] - root_files = [] - orphaned_files = [] - excluded_orphan_files = [] - orphaned_parent_path = set() - remote_path = self.config.remote_dir - root_path = self.config.root_dir - orphaned_path = self.config.orphaned_dir - if remote_path != root_path: - root_files = [ - os.path.join(path.replace(remote_path, root_path), name) - for path, subdirs, files in os.walk(remote_path) - for name in files - if orphaned_path.replace(remote_path, root_path) not in path - ] - else: - root_files = [ - os.path.join(path, name) - for path, subdirs, files in os.walk(root_path) - for name in files - if orphaned_path.replace(root_path, remote_path) not in path - ] - - # Get an updated list of torrents - torrent_list = self.get_torrents({"sort": "added_on"}) - for torrent in torrent_list: - for file in torrent.files: - fullpath = os.path.join(torrent.save_path, file.name) - # Replace fullpath with \\ if qbm is running in docker (linux) but qbt is on windows - fullpath = fullpath.replace(r"/", "\\") if ":\\" in fullpath else fullpath - torrent_files.append(fullpath) - - orphaned_files = set(root_files) - set(torrent_files) - orphaned_files = sorted(orphaned_files) - - if self.config.orphaned["exclude_patterns"]: - exclude_patterns = 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.replace(remote_path, root_path)) - ] - - orphaned_files = set(orphaned_files) - set(excluded_orphan_files) - - if orphaned_files: - os.makedirs(orphaned_path, exist_ok=True) - body = [] - num_orphaned = len(orphaned_files) - logger.print_line(f"{num_orphaned} Orphaned files found", self.config.loglevel) - body += logger.print_line("\n".join(orphaned_files), self.config.loglevel) - body += logger.print_line( - f"{'Did not move' if self.config.dry_run else 'Moved'} {num_orphaned} Orphaned files " - f"to {orphaned_path.replace(remote_path,root_path)}", - self.config.loglevel, - ) - - attr = { - "function": "rem_orphaned", - "title": f"Removing {num_orphaned} Orphaned Files", - "body": "\n".join(body), - "orphaned_files": list(orphaned_files), - "orphaned_directory": orphaned_path.replace(remote_path, root_path), - "total_orphaned_files": num_orphaned, - } - self.config.send_notifications(attr) - # Delete empty directories after moving orphan files - logger.info("Cleaning up any empty directories...") - if not self.config.dry_run: - for file in orphaned_files: - src = file.replace(root_path, remote_path) - dest = os.path.join(orphaned_path, file.replace(root_path, "")) - util.move_files(src, dest, True) - orphaned_parent_path.add(os.path.dirname(file).replace(root_path, remote_path)) - for parent_path in orphaned_parent_path: - util.remove_empty_directories(parent_path, "**/*") - else: - logger.print_line("No Orphaned Files found.", self.config.loglevel) - return orphaned - def tor_delete_recycle(self, torrent, info): """Move torrent to recycle bin""" if self.config.recyclebin["enabled"]: diff --git a/qbit_manage.py b/qbit_manage.py index 5349535..0ca6f61 100755 --- a/qbit_manage.py +++ b/qbit_manage.py @@ -293,6 +293,7 @@ from modules.core.remove_unregistered import RemoveUnregistered # noqa from modules.core.cross_seed import CrossSeed # noqa from modules.core.recheck import ReCheck # noqa from modules.core.tag_nohardlinks import TagNoHardLinks # noqa +from modules.core.remove_orphaned import RemoveOrphaned # noqa def my_except_hook(exctype, value, tbi): @@ -428,16 +429,14 @@ def start(): stats["deleted_contents"] += no_hardlinks.stats_deleted_contents # Remove Orphaned Files - num_orphaned = cfg.qbt.rem_orphaned() - stats["orphaned"] += num_orphaned + if cfg.commands["rem_orphaned"]: + stats["orphaned"] += RemoveOrphaned(qbit_manager).stats # Empty RecycleBin - recycle_emptied = cfg.cleanup_dirs("Recycle Bin") - stats["recycle_emptied"] += recycle_emptied + stats["recycle_emptied"] += cfg.cleanup_dirs("Recycle Bin") # Empty Orphaned Directory - orphaned_emptied = cfg.cleanup_dirs("Orphaned Data") - stats["orphaned_emptied"] += orphaned_emptied + stats["orphaned_emptied"] += cfg.cleanup_dirs("Orphaned Data") if stats["categorized"] > 0: stats_summary.append(f"Total Torrents Categorized: {stats['categorized']}")