mirror of
https://github.com/StuffAnThings/qbit_manage.git
synced 2024-09-20 07:16:04 +08:00
BREAKING CHANGE: Adds #155
Adds empty_after_x_days for orphaned data #155
This commit is contained in:
parent
c33ae74adf
commit
20833cedcf
|
@ -14,7 +14,7 @@ commands:
|
|||
tag_tracker_error: False
|
||||
rem_orphaned: False
|
||||
tag_nohardlinks: False
|
||||
skip_recycle: False
|
||||
skip_cleanup: False
|
||||
|
||||
qbt:
|
||||
# qBittorrent parameters
|
||||
|
@ -185,6 +185,10 @@ recyclebin:
|
|||
|
||||
orphaned:
|
||||
# Orphaned files are those in the root_dir download directory that are not referenced by any active torrents.
|
||||
# Will automatically remove all files and folders in orphaned data after x days. (Checks every script run)
|
||||
# If this variable is not defined it, the orphaned data will never be emptied.
|
||||
# WARNING: Setting this variable to 0 will delete all files immediately upon script run!
|
||||
empty_after_x_days: 60
|
||||
# File patterns that will not be considered orphaned files. Handy for generated files that aren't part of the torrent but belong with the torrent's files
|
||||
exclude_patterns:
|
||||
- "**/.DS_Store"
|
||||
|
@ -228,7 +232,7 @@ webhooks:
|
|||
tag_tracker_error: notifiarr
|
||||
rem_orphaned: notifiarr
|
||||
tag_nohardlinks: notifiarr
|
||||
empty_recyclebin: notifiarr
|
||||
cleanup_dirs: notifiarr
|
||||
|
||||
bhd:
|
||||
# BHD Integration used for checking unregistered torrents
|
||||
|
|
|
@ -49,7 +49,7 @@ class Config:
|
|||
'tag_tracker_error',
|
||||
'rem_orphaned',
|
||||
'tag_nohardlinks',
|
||||
'skip_recycle',
|
||||
'skip_cleanup'
|
||||
]:
|
||||
if v not in self.commands:
|
||||
self.commands[v] = False
|
||||
|
@ -62,7 +62,7 @@ class Config:
|
|||
logger.debug(f" --tag-tracker-error (QBT_TAG_TRACKER_ERROR): {self.commands['tag_tracker_error']}")
|
||||
logger.debug(f" --rem-orphaned (QBT_REM_ORPHANED): {self.commands['rem_orphaned']}")
|
||||
logger.debug(f" --tag-nohardlinks (QBT_TAG_NOHARDLINKS): {self.commands['tag_nohardlinks']}")
|
||||
logger.debug(f" --skip-recycle (QBT_SKIP_RECYCLE): {self.commands['skip_recycle']}")
|
||||
logger.debug(f" --skip-cleanup (QBT_SKIP_CLEANUP): {self.commands['skip_cleanup']}")
|
||||
logger.debug(f" --dry-run (QBT_DRY_RUN): {self.commands['dry_run']}")
|
||||
else:
|
||||
self.commands = args
|
||||
|
@ -99,7 +99,7 @@ class Config:
|
|||
hooks("rem_unregistered")
|
||||
hooks("rem_orphaned")
|
||||
hooks("tag_nohardlinks")
|
||||
hooks("empty_recyclebin")
|
||||
hooks("cleanup_dirs")
|
||||
self.data["webhooks"] = temp
|
||||
if "bhd" in self.data: self.data["bhd"] = self.data.pop("bhd")
|
||||
|
||||
|
@ -121,7 +121,7 @@ class Config:
|
|||
'tag_tracker_error': None,
|
||||
'rem_orphaned': None,
|
||||
'tag_nohardlinks': None,
|
||||
'empty_recyclebin': None
|
||||
'cleanup_dirs': None,
|
||||
}
|
||||
|
||||
self.webhooks = {
|
||||
|
@ -265,7 +265,10 @@ class Config:
|
|||
|
||||
# Add Orphaned
|
||||
self.orphaned = {}
|
||||
self.orphaned['empty_after_x_days'] = self.util.check_for_attribute(self.data, "empty_after_x_days", parent="orphaned", var_type="int", default_is_none=True)
|
||||
self.orphaned['exclude_patterns'] = self.util.check_for_attribute(self.data, "exclude_patterns", parent="orphaned", var_type="list", default_is_none=True, do_print=False)
|
||||
exclude_orphaned = f"**{os.sep}{os.path.basename(self.orphaned_dir.rstrip(os.sep))}{os.sep}*"
|
||||
self.orphaned['exclude_patterns'].append(exclude_orphaned) if exclude_orphaned not in self.orphaned['exclude_patterns'] else self.orphaned['exclude_patterns']
|
||||
if self.recyclebin['enabled']:
|
||||
exclude_recycle = f"**{os.sep}{os.path.basename(self.recycle_dir.rstrip(os.sep))}{os.sep}*"
|
||||
self.orphaned['exclude_patterns'].append(exclude_recycle) if exclude_recycle not in self.orphaned['exclude_patterns'] else self.orphaned['exclude_patterns']
|
||||
|
@ -367,66 +370,80 @@ class Config:
|
|||
logger.warning(e)
|
||||
return category
|
||||
|
||||
# Empty the recycle bin
|
||||
def empty_recycle(self):
|
||||
# Empty old files from recycle bin or orphaned
|
||||
def cleanup_dirs(self, location):
|
||||
dry_run = self.commands['dry_run']
|
||||
loglevel = 'DRYRUN' if dry_run else 'INFO'
|
||||
num_del = 0
|
||||
files = []
|
||||
size_bytes = 0
|
||||
if not self.commands["skip_recycle"]:
|
||||
if self.recyclebin['enabled'] and self.recyclebin['empty_after_x_days']:
|
||||
if self.recyclebin['split_by_category']:
|
||||
skip = self.commands["skip_cleanup"]
|
||||
if location == "Recycle Bin":
|
||||
enabled = self.recyclebin['enabled']
|
||||
empty_after_x_days = self.recyclebin['empty_after_x_days']
|
||||
function = "cleanup_dirs"
|
||||
location_path = self.recycle_dir
|
||||
|
||||
elif location == "Orphaned Data":
|
||||
enabled = self.commands["rem_orphaned"]
|
||||
empty_after_x_days = self.orphaned['empty_after_x_days']
|
||||
function = "cleanup_dirs"
|
||||
location_path = self.orphaned_dir
|
||||
|
||||
if not skip:
|
||||
if enabled and empty_after_x_days:
|
||||
if location == "Recycle Bin" and self.recyclebin['split_by_category']:
|
||||
if "cat" in self.data and self.data["cat"] is not None:
|
||||
save_path = list(self.data["cat"].values())
|
||||
cleaned_save_path = [os.path.join(s.replace(self.root_dir, self.remote_dir), os.path.basename(self.recycle_dir.rstrip(os.sep))) for s in save_path]
|
||||
recycle_path = [self.recycle_dir]
|
||||
cleaned_save_path = [os.path.join(s.replace(self.root_dir, self.remote_dir), os.path.basename(location_path.rstrip(os.sep))) for s in save_path]
|
||||
location_path_list = [location_path]
|
||||
for dir in cleaned_save_path:
|
||||
if os.path.exists(dir): recycle_path.append(dir)
|
||||
if os.path.exists(dir): location_path_list.append(dir)
|
||||
else:
|
||||
e = (f'No categories defined. Checking Recycle Bin directory {self.recycle_dir}.')
|
||||
self.notify(e, 'Empty Recycle Bin', False)
|
||||
e = (f'No categories defined. Checking {location} directory {location_path}.')
|
||||
self.notify(e, f'Empty {location}', False)
|
||||
logger.warning(e)
|
||||
recycle_path = [self.recycle_dir]
|
||||
location_path_list = [location_path]
|
||||
else:
|
||||
recycle_path = [self.recycle_dir]
|
||||
recycle_files = [os.path.join(path, name) for r_path in recycle_path for path, subdirs, files in os.walk(r_path) for name in files]
|
||||
recycle_files = sorted(recycle_files)
|
||||
if recycle_files:
|
||||
location_path_list = [location_path]
|
||||
location_files = [os.path.join(path, name) for r_path in location_path_list for path, subdirs, files in os.walk(r_path) for name in files]
|
||||
location_files = sorted(location_files)
|
||||
if location_files:
|
||||
body = []
|
||||
logger.separator(f"Emptying Recycle Bin (Files > {self.recyclebin['empty_after_x_days']} days)", space=True, border=True)
|
||||
logger.separator(f"Emptying {location} (Files > {empty_after_x_days} days)", space=True, border=True)
|
||||
prevfolder = ''
|
||||
for file in recycle_files:
|
||||
folder = re.search(f".*{os.path.basename(self.recycle_dir.rstrip(os.sep))}", file).group(0)
|
||||
for file in location_files:
|
||||
folder = re.search(f".*{os.path.basename(location_path.rstrip(os.sep))}", file).group(0)
|
||||
if folder != prevfolder: body += logger.separator(f"Searching: {folder}", space=False, border=False)
|
||||
fileStats = os.stat(file)
|
||||
filename = os.path.basename(file)
|
||||
last_modified = fileStats[stat.ST_MTIME] # in seconds (last modified time)
|
||||
now = time.time() # in seconds
|
||||
days = (now - last_modified) / (60 * 60 * 24)
|
||||
if (self.recyclebin['empty_after_x_days'] <= days):
|
||||
if (empty_after_x_days <= days):
|
||||
num_del += 1
|
||||
body += logger.print_line(f"{'Did not delete' if dry_run else 'Deleted'} {filename} from {folder} (Last modified {round(days)} days ago).", loglevel)
|
||||
files += [str(filename)]
|
||||
size_bytes += os.path.getsize(file)
|
||||
if not dry_run: os.remove(file)
|
||||
prevfolder = re.search(f".*{os.path.basename(self.recycle_dir.rstrip(os.sep))}", file).group(0)
|
||||
prevfolder = re.search(f".*{os.path.basename(location_path.rstrip(os.sep))}", file).group(0)
|
||||
if num_del > 0:
|
||||
if not dry_run:
|
||||
for path in recycle_path:
|
||||
for path in location_path_list:
|
||||
util.remove_empty_directories(path, "**/*")
|
||||
body += logger.print_line(f"{'Did not delete' if dry_run else 'Deleted'} {num_del} files ({util.human_readable_size(size_bytes)}) from the Recycle Bin.", loglevel)
|
||||
body += logger.print_line(f"{'Did not delete' if dry_run else 'Deleted'} {num_del} files ({util.human_readable_size(size_bytes)}) from the {location}.", loglevel)
|
||||
attr = {
|
||||
"function": "empty_recyclebin",
|
||||
"title": f"Emptying Recycle Bin (Files > {self.recyclebin['empty_after_x_days']} days)",
|
||||
"function": function,
|
||||
"location": location,
|
||||
"title": f"Emptying {location} (Files > {empty_after_x_days} days)",
|
||||
"body": "\n".join(body),
|
||||
"files": files,
|
||||
"empty_after_x_days": self.recyclebin['empty_after_x_days'],
|
||||
"empty_after_x_days": empty_after_x_days,
|
||||
"size_in_bytes": size_bytes
|
||||
}
|
||||
self.send_notifications(attr)
|
||||
else:
|
||||
logger.debug(f'No files found in "{(",".join(recycle_path))}"')
|
||||
logger.debug(f'No files found in "{(",".join(location_path_list))}"')
|
||||
return num_del
|
||||
|
||||
def send_notifications(self, attr):
|
||||
|
|
|
@ -802,16 +802,16 @@ class Qbt:
|
|||
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 self.config.trace_mode:
|
||||
logger.separator("Torrent Files", space=False, border=False, loglevel='DEBUG')
|
||||
logger.print_line("\n".join(torrent_files), 'DEBUG')
|
||||
logger.separator("Root Files", space=False, border=False, loglevel='DEBUG')
|
||||
logger.print_line("\n".join(root_files), 'DEBUG')
|
||||
logger.separator("Excluded Orphan Files", space=False, border=False, loglevel='DEBUG')
|
||||
logger.print_line("\n".join(excluded_orphan_files), 'DEBUG')
|
||||
logger.separator("Orphaned Files", space=False, border=False, loglevel='DEBUG')
|
||||
logger.print_line("\n".join(orphaned_files), 'DEBUG')
|
||||
logger.separator("Deleting Orphaned Files", space=False, border=False, loglevel='DEBUG')
|
||||
# if self.config.trace_mode:
|
||||
# logger.separator("Torrent Files", space=False, border=False, loglevel='DEBUG')
|
||||
# logger.print_line("\n".join(torrent_files), 'DEBUG')
|
||||
# logger.separator("Root Files", space=False, border=False, loglevel='DEBUG')
|
||||
# logger.print_line("\n".join(root_files), 'DEBUG')
|
||||
# logger.separator("Excluded Orphan Files", space=False, border=False, loglevel='DEBUG')
|
||||
# logger.print_line("\n".join(excluded_orphan_files), 'DEBUG')
|
||||
# logger.separator("Orphaned Files", space=False, border=False, loglevel='DEBUG')
|
||||
# logger.print_line("\n".join(orphaned_files), 'DEBUG')
|
||||
# logger.separator("Deleting Orphaned Files", space=False, border=False, loglevel='DEBUG')
|
||||
|
||||
if orphaned_files:
|
||||
os.makedirs(orphaned_path, exist_ok=True)
|
||||
|
@ -836,7 +836,7 @@ class Qbt:
|
|||
for file in alive_it(orphaned_files):
|
||||
src = file.replace(root_path, remote_path)
|
||||
dest = os.path.join(orphaned_path, file.replace(root_path, ''))
|
||||
util.move_files(src, dest)
|
||||
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, "**/*")
|
||||
|
|
|
@ -108,7 +108,8 @@ class Webhooks:
|
|||
"orphaned_files_found": stats["orphaned"],
|
||||
"torrents_tagged_no_hardlinks": stats["tagged_noHL"],
|
||||
"torrents_untagged_no_hardlinks": stats["untagged_noHL"],
|
||||
"files_deleted_from_recyclebin": stats["recycle_emptied"]
|
||||
"files_deleted_from_recyclebin": stats["recycle_emptied"],
|
||||
"files_deleted_from_orphaned": stats["orphaned_emptied"]
|
||||
})
|
||||
|
||||
def error_hooks(self, text, function_error=None, critical=True):
|
||||
|
|
|
@ -39,7 +39,7 @@ parser.add_argument('-tnhl', '--tag-nohardlinks', dest='tag_nohardlinks', action
|
|||
This is useful for those that use Sonarr/Radarr which hard link your media files with the torrents for seeding. \
|
||||
When files get upgraded they no longer become linked with your media therefore will be tagged with a new tag noHL. \
|
||||
You can then safely delete/remove these torrents to free up any extra space that is not being used by your media folder.')
|
||||
parser.add_argument('-sr', '--skip-recycle', dest='skip_recycle', action="store_true", default=False, help='Use this to skip emptying the Reycle Bin folder.')
|
||||
parser.add_argument('-sc', '--skip-cleanup', dest='skip_cleanup', action="store_true", default=False, help='Use this to skip cleaning up Reycle Bin/Orphaned directory.')
|
||||
parser.add_argument('-dr', '--dry-run', dest='dry_run', action="store_true", default=False,
|
||||
help='If you would like to see what is gonna happen but not actually move/delete or tag/categorize anything.')
|
||||
parser.add_argument('-ll', '--log-level', dest='log_level', action="store", default='INFO', type=str, help='Change your log level.')
|
||||
|
@ -85,7 +85,7 @@ rem_unregistered = get_arg("QBT_REM_UNREGISTERED", args.rem_unregistered, arg_bo
|
|||
tag_tracker_error = get_arg("QBT_TAG_TRACKER_ERROR", args.tag_tracker_error, arg_bool=True)
|
||||
rem_orphaned = get_arg("QBT_REM_ORPHANED", args.rem_orphaned, arg_bool=True)
|
||||
tag_nohardlinks = get_arg("QBT_TAG_NOHARDLINKS", args.tag_nohardlinks, arg_bool=True)
|
||||
skip_recycle = get_arg("QBT_SKIP_RECYCLE", args.skip_recycle, arg_bool=True)
|
||||
skip_cleanup = get_arg("QBT_SKIP_CLEANUP", args.skip_cleanup, arg_bool=True)
|
||||
dry_run = get_arg("QBT_DRY_RUN", args.dry_run, arg_bool=True)
|
||||
log_level = get_arg("QBT_LOG_LEVEL", args.log_level)
|
||||
divider = get_arg("QBT_DIVIDER", args.divider)
|
||||
|
@ -129,7 +129,7 @@ for v in [
|
|||
'tag_tracker_error',
|
||||
'rem_orphaned',
|
||||
'tag_nohardlinks',
|
||||
'skip_recycle',
|
||||
'skip_cleanup',
|
||||
'dry_run',
|
||||
'log_level',
|
||||
'divider',
|
||||
|
@ -217,6 +217,7 @@ def start():
|
|||
"rechecked": 0,
|
||||
"orphaned": 0,
|
||||
"recycle_emptied": 0,
|
||||
"orphaned_emptied": 0,
|
||||
"tagged": 0,
|
||||
"categorized": 0,
|
||||
"rem_unreg": 0,
|
||||
|
@ -289,9 +290,13 @@ def start():
|
|||
stats["orphaned"] += num_orphaned
|
||||
|
||||
# Empty RecycleBin
|
||||
recycle_emptied = cfg.empty_recycle()
|
||||
recycle_emptied = cfg.cleanup_dirs("Recycle Bin")
|
||||
stats["recycle_emptied"] += recycle_emptied
|
||||
|
||||
# Empty Orphaned Directory
|
||||
orphaned_emptied = cfg.cleanup_dirs("Orphaned Data")
|
||||
stats["orphaned_emptied"] += orphaned_emptied
|
||||
|
||||
if stats["categorized"] > 0: stats_summary.append(f"Total Torrents Categorized: {stats['categorized']}")
|
||||
if stats["tagged"] > 0: stats_summary.append(f"Total Torrents Tagged: {stats['tagged']}")
|
||||
if stats["rem_unreg"] > 0: stats_summary.append(f"Total Unregistered Torrents Removed: {stats['rem_unreg']}")
|
||||
|
@ -306,6 +311,7 @@ def start():
|
|||
if stats["tagged_noHL"] > 0: stats_summary.append(f"Total noHL Torrents Tagged: {stats['tagged_noHL']}")
|
||||
if stats["untagged_noHL"] > 0: stats_summary.append(f"Total noHL Torrents untagged: {stats['untagged_noHL']}")
|
||||
if stats["recycle_emptied"] > 0: stats_summary.append(f"Total Files Deleted from Recycle Bin: {stats['recycle_emptied']}")
|
||||
if stats["orphaned_emptied"] > 0: stats_summary.append(f"Total Files Deleted from Orphaned Data: {stats['orphaned_emptied']}")
|
||||
|
||||
FinishedRun()
|
||||
if cfg:
|
||||
|
@ -376,7 +382,7 @@ if __name__ == '__main__':
|
|||
logger.debug(f" --tag-tracker-error (QBT_TAG_TRACKER_ERROR): {tag_tracker_error}")
|
||||
logger.debug(f" --rem-orphaned (QBT_REM_ORPHANED): {rem_orphaned}")
|
||||
logger.debug(f" --tag-nohardlinks (QBT_TAG_NOHARDLINKS): {tag_nohardlinks}")
|
||||
logger.debug(f" --skip-recycle (QBT_SKIP_RECYCLE): {skip_recycle}")
|
||||
logger.debug(f" --skip-cleanup (QBT_SKIP_CLEANUP): {skip_cleanup}")
|
||||
logger.debug(f" --dry-run (QBT_DRY_RUN): {dry_run}")
|
||||
logger.debug(f" --log-level (QBT_LOG_LEVEL): {log_level}")
|
||||
logger.debug(f" --divider (QBT_DIVIDER): {divider}")
|
||||
|
|
Loading…
Reference in a new issue