mirror of
https://github.com/StuffAnThings/qbit_manage.git
synced 2025-10-06 03:46:40 +08:00
[FR] Save .torrents in Recycle Bin #73
Save the .torrent and .fastresume files into Recycle Bin before deleting
This commit is contained in:
parent
7bba88d9fc
commit
21c1d5a07b
4 changed files with 112 additions and 29 deletions
|
@ -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:
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue