BREAKING CHANGE: Adds #155

Adds empty_after_x_days for orphaned data #155
This commit is contained in:
Jon Lee 2022-09-25 21:48:57 -04:00
parent c33ae74adf
commit 20833cedcf
No known key found for this signature in database
GPG key ID: 9665BA6CF5DC2671
6 changed files with 78 additions and 50 deletions

View file

@ -1 +1 @@
3.2.7
3.3.0

View file

@ -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

View file

@ -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):

View file

@ -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, "**/*")

View file

@ -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):

View file

@ -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}")