[FR] Save .torrents in Recycle Bin #73

Save the .torrent and .fastresume files into Recycle Bin before deleting
This commit is contained in:
bobokun 2021-12-31 19:28:45 -05:00
parent 7bba88d9fc
commit 21c1d5a07b
No known key found for this signature in database
GPG key ID: 9665BA6CF5DC2671
4 changed files with 112 additions and 29 deletions

View file

@ -1,5 +1,7 @@
# This is an example configuration file that documents all the options.
# It will need to be modified for your specific use case.
# Please refer to the link below for more details on how to set up the configuration file
# https://github.com/StuffAnThings/qbit_manage/wiki/Config-Setup
# qBittorrent parameters
qbt:
@ -17,10 +19,13 @@ directory:
# <OPTIONAL> remote_dir var: </your/path/here/> # Path of docker host mapping of root_dir.
# Must be set if you're running qbit_manage locally and qBittorrent/cross_seed is in a docker
# <OPTIONAL> recycle_bin var: </your/path/here/> # Path of the RecycleBin folder. Default location is set to remote_dir/.RecycleBin
# <OPTIONAL> torrents_dir var: </your/path/here/> # Path of the your qbittorrent torrents directory. Required for `save_torrents` attribute in recyclebin
cross_seed: "/your/path/here/"
root_dir: "/data/torrents/"
remote_dir: "/mnt/user/data/torrents/"
recycle_bin: "/mnt/user/data/torrents/.RecycleBin"
torrents_dir: "/qbittorrent/data/BT_backup"
# Category & Path Parameters
cat:
@ -125,10 +130,15 @@ nohardlinks:
# By default the Recycle Bin will be emptied on every run of the qbit_manage script if empty_after_x_days is defined.
recyclebin:
enabled: true
# <OPTIONAL> empty_after_x_days var: Will automatically remove all files and folders in recycle bin after x days. (Checks every script run)
# If this variable is not defined it, the RecycleBin will never be emptied.
# WARNING: Setting this variable to 0 will delete all files immediately upon script run!
# <OPTIONAL> empty_after_x_days var:
# Will automatically remove all files and folders in recycle bin after x days. (Checks every script run)
# If this variable is not defined it, the RecycleBin will never be emptied.
# WARNING: Setting this variable to 0 will delete all files immediately upon script run!
empty_after_x_days: 60
# <OPTIONAL> save_torrents var:
# If this option is set to true you MUST fill out the torrents_dir in the directory attribute.
# This will save a copy of your .torrent and .fastresume file in the recycle bin before deleting it from qbittorrent
save_torrents: true
# Orphaned files are those in the root_dir download directory that are not referenced by any active torrents.
orphaned:

View file

@ -176,6 +176,7 @@ class Config:
self.recyclebin = {}
self.recyclebin['enabled'] = self.util.check_for_attribute(self.data, "enabled", parent="recyclebin", var_type="bool", default=True)
self.recyclebin['empty_after_x_days'] = self.util.check_for_attribute(self.data, "empty_after_x_days", parent="recyclebin", var_type="int", default_is_none=True)
self.recyclebin['save_torrents'] = self.util.check_for_attribute(self.data, "save_torrents", parent="recyclebin", var_type="bool", default=False)
# Add Orphaned
self.orphaned = {}
@ -195,6 +196,14 @@ class Config:
else:
self.cross_seed_dir = self.util.check_for_attribute(self.data, "cross_seed", parent="directory", default_is_none=True)
self.recycle_dir = self.util.check_for_attribute(self.data, "recycle_bin", parent="directory", var_type="path", default=os.path.join(self.remote_dir, '.RecycleBin'))
if self.recyclebin['enabled'] and self.recyclebin['save_torrents']:
self.torrents_dir = self.util.check_for_attribute(self.data, "torrents_dir", parent="directory", var_type="path")
if not any(File.endswith(".torrent") for File in os.listdir(self.torrents_dir)):
e = f"Config Error: The location {self.torrents_dir} does not contain any .torrents"
self.notify(e, 'Config')
raise Failed(e)
else:
self.torrents_dir = self.util.check_for_attribute(self.data, "torrents_dir", parent="directory", default_is_none=True)
else:
e = "Config Error: directory attribute not found"
self.notify(e, 'Config')

View file

@ -335,14 +335,14 @@ class Qbt:
"notifiarr_indexer": tracker["notifiarr"],
}
if (os.path.exists(torrent['content_path'].replace(root_dir, root_dir))):
if not dry_run: self.tor_delete_recycle(torrent)
del_tor_cont += 1
attr["torrents_deleted_and_contents"] = True
if not dry_run: self.tor_delete_recycle(torrent, attr)
body += print_line(util.insert_space('Deleted .torrent AND content files.', 8), loglevel)
else:
if not dry_run: torrent.delete(hash=torrent.hash, delete_files=False)
del_tor += 1
attr["torrents_deleted_and_contents"] = False
if not dry_run: self.tor_delete_recycle(torrent, attr)
body += print_line(util.insert_space('Deleted .torrent but NOT content files.', 8), loglevel)
attr["body"] = "\n".join(body)
self.config.send_notifications(attr)
@ -381,18 +381,18 @@ class Qbt:
if t_count > 1:
# Checks if any of the original torrents are working
if '' in t_msg or 2 in t_status:
if not dry_run: torrent.delete(hash=torrent.hash, delete_files=False)
attr["torrents_deleted_and_contents"] = False
if not dry_run: self.tor_delete_recycle(torrent, attr)
body += print_line(util.insert_space('Deleted .torrent but NOT content files.', 8), loglevel)
del_tor += 1
else:
if not dry_run: self.tor_delete_recycle(torrent)
attr["torrents_deleted_and_contents"] = True
if not dry_run: self.tor_delete_recycle(torrent, attr)
body += print_line(util.insert_space('Deleted .torrent AND content files.', 8), loglevel)
del_tor_cont += 1
else:
if not dry_run: self.tor_delete_recycle(torrent)
attr["torrents_deleted_and_contents"] = True
if not dry_run: self.tor_delete_recycle(torrent, attr)
body += print_line(util.insert_space('Deleted .torrent AND content files.', 8), loglevel)
del_tor_cont += 1
attr["body"] = "\n".join(body)
@ -706,10 +706,11 @@ class Qbt:
print_line("No Orphaned Files found.", loglevel)
return orphaned
def tor_delete_recycle(self, torrent):
def tor_delete_recycle(self, torrent, info):
if self.config.recyclebin['enabled']:
tor_files = []
try:
info_hash = torrent.hash
# Define torrent files/folders
for file in torrent.files:
tor_files.append(os.path.join(torrent.save_path, file.name))
@ -717,25 +718,63 @@ class Qbt:
return
# Create recycle bin if not exists
recycle_path = self.config.recycle_dir
torrent_path = os.path.join(recycle_path, 'torrents')
torrents_json_path = os.path.join(recycle_path, 'torrents_json')
os.makedirs(recycle_path, exist_ok=True)
if self.config.recyclebin['save_torrents']:
if os.path.isdir(torrent_path) is False: os.makedirs(torrent_path)
if os.path.isdir(torrents_json_path) is False: os.makedirs(torrents_json_path)
torrent_json_file = os.path.join(torrents_json_path, f"{info['torrent_name']}.json")
torrent_json = util.load_json(torrent_json_file)
if not torrent_json:
torrent_json["torrent_name"] = info["torrent_name"]
torrent_json["category"] = info["torrent_category"]
dot_torrent_files = []
for File in os.listdir(self.config.torrents_dir):
if File.startswith(info_hash):
dot_torrent_files.append(File)
try:
util.copy_files(os.path.join(self.config.torrents_dir, File), os.path.join(torrent_path, File))
except Exception as e:
util.print_stacktrace()
self.config.notify(e, 'Deleting Torrent', False)
logger.warning(f"RecycleBin Warning: {e}")
if "tracker_torrent_files" in torrent_json:
tracker_torrent_files = torrent_json["tracker_torrent_files"]
else:
tracker_torrent_files = {}
tracker_torrent_files[info["torrent_tracker"]] = dot_torrent_files
torrent_json["tracker_torrent_files"] = tracker_torrent_files
if "files" not in torrent_json:
files_cleaned = [f.replace(self.config.root_dir, '') for f in tor_files]
torrent_json["files"] = files_cleaned
torrent_json["deleted_contents"] = info['torrents_deleted_and_contents']
util.save_json(torrent_json, torrent_json_file)
if info['torrents_deleted_and_contents'] is True:
separator(f"Moving {len(tor_files)} files to RecycleBin", space=False, border=False, loglevel='DEBUG')
if len(tor_files) == 1: print_line(tor_files[0], 'DEBUG')
else: print_multiline("\n".join(tor_files), 'DEBUG')
logger.debug(f'Moved {len(tor_files)} files to {recycle_path.replace(self.config.remote_dir,self.config.root_dir)}')
separator(f"Moving {len(tor_files)} files to RecycleBin", space=False, border=False, loglevel='DEBUG')
if len(tor_files) == 1: print_line(tor_files[0], 'DEBUG')
else: print_multiline("\n".join(tor_files), 'DEBUG')
logger.debug(f'Moved {len(tor_files)} files to {recycle_path.replace(self.config.remote_dir,self.config.root_dir)}')
# Move files from torrent contents to Recycle bin
for file in tor_files:
src = file.replace(self.config.root_dir, self.config.remote_dir)
dest = os.path.join(recycle_path, file.replace(self.config.root_dir, ''))
# Move files and change date modified
try:
util.move_files(src, dest, True)
except FileNotFoundError:
print_line(f'RecycleBin Warning - FileNotFound: No such file or directory: {src} ', 'WARNING')
# Delete torrent and files
torrent.delete(hash=torrent.hash, delete_files=False)
# Remove any empty directories
util.remove_empty_directories(torrent.save_path.replace(self.config.root_dir, self.config.remote_dir), "**/*")
# Move files from torrent contents to Recycle bin
for file in tor_files:
src = file.replace(self.config.root_dir, self.config.remote_dir)
dest = os.path.join(recycle_path, file.replace(self.config.root_dir, ''))
# Move files and change date modified
try:
util.move_files(src, dest, True)
except FileNotFoundError:
e = print_line(f'RecycleBin Warning - FileNotFound: No such file or directory: {src} ', 'WARNING')
self.config.notify(e, 'Deleting Torrent', False)
# Delete torrent and files
torrent.delete(delete_files=False)
# Remove any empty directories
util.remove_empty_directories(torrent.save_path.replace(self.config.root_dir, self.config.remote_dir), "**/*")
else:
torrent.delete(delete_files=False)
else:
torrent.delete(hash=torrent.hash, delete_files=True)
if info['torrents_deleted_and_contents'] is True:
torrent.delete(delete_files=True)
else:
torrent.delete(delete_files=False)

View file

@ -1,4 +1,4 @@
import logging, os, shutil, traceback, time, signal
import logging, os, shutil, traceback, time, signal, json
from logging.handlers import RotatingFileHandler
from ruamel import yaml
from pathlib import Path
@ -297,6 +297,14 @@ def move_files(src, dest, mod=False):
os.utime(dest, (modTime, modTime))
# Copy Files from source to destination
def copy_files(src, dest):
dest_path = os.path.dirname(dest)
if os.path.isdir(dest_path) is False:
os.makedirs(dest_path)
shutil.copyfile(src, dest)
# Remove any empty directories after moving files
def remove_empty_directories(pathlib_root_dir, pattern):
pathlib_root_dir = Path(pathlib_root_dir)
@ -328,6 +336,23 @@ def nohardlink(file):
return check
# Load json file if exists
def load_json(file):
if (os.path.isfile(file)):
f = open(file, "r")
data = json.load(f)
f.close()
else:
data = {}
return data
# Save json file overwrite if exists
def save_json(torrent_json, dest):
with open(dest, 'w', encoding='utf-8') as f:
json.dump(torrent_json, f, ensure_ascii=False, indent=4)
# Gracefully kill script when docker stops
class GracefulKiller:
kill_now = False