mirror of
https://github.com/StuffAnThings/qbit_manage.git
synced 2025-10-08 04:47:25 +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.
|
# This is an example configuration file that documents all the options.
|
||||||
# It will need to be modified for your specific use case.
|
# 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
|
# qBittorrent parameters
|
||||||
qbt:
|
qbt:
|
||||||
|
@ -17,10 +19,13 @@ directory:
|
||||||
# <OPTIONAL> remote_dir var: </your/path/here/> # Path of docker host mapping of root_dir.
|
# <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
|
# 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> 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/"
|
cross_seed: "/your/path/here/"
|
||||||
root_dir: "/data/torrents/"
|
root_dir: "/data/torrents/"
|
||||||
remote_dir: "/mnt/user/data/torrents/"
|
remote_dir: "/mnt/user/data/torrents/"
|
||||||
recycle_bin: "/mnt/user/data/torrents/.RecycleBin"
|
recycle_bin: "/mnt/user/data/torrents/.RecycleBin"
|
||||||
|
torrents_dir: "/qbittorrent/data/BT_backup"
|
||||||
|
|
||||||
# Category & Path Parameters
|
# Category & Path Parameters
|
||||||
cat:
|
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.
|
# By default the Recycle Bin will be emptied on every run of the qbit_manage script if empty_after_x_days is defined.
|
||||||
recyclebin:
|
recyclebin:
|
||||||
enabled: true
|
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)
|
# <OPTIONAL> empty_after_x_days var:
|
||||||
# If this variable is not defined it, the RecycleBin will never be emptied.
|
# Will automatically remove all files and folders in recycle bin after x days. (Checks every script run)
|
||||||
# WARNING: Setting this variable to 0 will delete all files immediately upon 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
|
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 files are those in the root_dir download directory that are not referenced by any active torrents.
|
||||||
orphaned:
|
orphaned:
|
||||||
|
|
|
@ -176,6 +176,7 @@ class Config:
|
||||||
self.recyclebin = {}
|
self.recyclebin = {}
|
||||||
self.recyclebin['enabled'] = self.util.check_for_attribute(self.data, "enabled", parent="recyclebin", var_type="bool", default=True)
|
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['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
|
# Add Orphaned
|
||||||
self.orphaned = {}
|
self.orphaned = {}
|
||||||
|
@ -195,6 +196,14 @@ class Config:
|
||||||
else:
|
else:
|
||||||
self.cross_seed_dir = self.util.check_for_attribute(self.data, "cross_seed", parent="directory", default_is_none=True)
|
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'))
|
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:
|
else:
|
||||||
e = "Config Error: directory attribute not found"
|
e = "Config Error: directory attribute not found"
|
||||||
self.notify(e, 'Config')
|
self.notify(e, 'Config')
|
||||||
|
|
|
@ -335,14 +335,14 @@ class Qbt:
|
||||||
"notifiarr_indexer": tracker["notifiarr"],
|
"notifiarr_indexer": tracker["notifiarr"],
|
||||||
}
|
}
|
||||||
if (os.path.exists(torrent['content_path'].replace(root_dir, root_dir))):
|
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
|
del_tor_cont += 1
|
||||||
attr["torrents_deleted_and_contents"] = True
|
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)
|
body += print_line(util.insert_space('Deleted .torrent AND content files.', 8), loglevel)
|
||||||
else:
|
else:
|
||||||
if not dry_run: torrent.delete(hash=torrent.hash, delete_files=False)
|
|
||||||
del_tor += 1
|
del_tor += 1
|
||||||
attr["torrents_deleted_and_contents"] = 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)
|
body += print_line(util.insert_space('Deleted .torrent but NOT content files.', 8), loglevel)
|
||||||
attr["body"] = "\n".join(body)
|
attr["body"] = "\n".join(body)
|
||||||
self.config.send_notifications(attr)
|
self.config.send_notifications(attr)
|
||||||
|
@ -381,18 +381,18 @@ class Qbt:
|
||||||
if t_count > 1:
|
if t_count > 1:
|
||||||
# Checks if any of the original torrents are working
|
# Checks if any of the original torrents are working
|
||||||
if '' in t_msg or 2 in t_status:
|
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
|
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)
|
body += print_line(util.insert_space('Deleted .torrent but NOT content files.', 8), loglevel)
|
||||||
del_tor += 1
|
del_tor += 1
|
||||||
else:
|
else:
|
||||||
if not dry_run: self.tor_delete_recycle(torrent)
|
|
||||||
attr["torrents_deleted_and_contents"] = True
|
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)
|
body += print_line(util.insert_space('Deleted .torrent AND content files.', 8), loglevel)
|
||||||
del_tor_cont += 1
|
del_tor_cont += 1
|
||||||
else:
|
else:
|
||||||
if not dry_run: self.tor_delete_recycle(torrent)
|
|
||||||
attr["torrents_deleted_and_contents"] = True
|
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)
|
body += print_line(util.insert_space('Deleted .torrent AND content files.', 8), loglevel)
|
||||||
del_tor_cont += 1
|
del_tor_cont += 1
|
||||||
attr["body"] = "\n".join(body)
|
attr["body"] = "\n".join(body)
|
||||||
|
@ -706,10 +706,11 @@ class Qbt:
|
||||||
print_line("No Orphaned Files found.", loglevel)
|
print_line("No Orphaned Files found.", loglevel)
|
||||||
return orphaned
|
return orphaned
|
||||||
|
|
||||||
def tor_delete_recycle(self, torrent):
|
def tor_delete_recycle(self, torrent, info):
|
||||||
if self.config.recyclebin['enabled']:
|
if self.config.recyclebin['enabled']:
|
||||||
tor_files = []
|
tor_files = []
|
||||||
try:
|
try:
|
||||||
|
info_hash = torrent.hash
|
||||||
# Define torrent files/folders
|
# Define torrent files/folders
|
||||||
for file in torrent.files:
|
for file in torrent.files:
|
||||||
tor_files.append(os.path.join(torrent.save_path, file.name))
|
tor_files.append(os.path.join(torrent.save_path, file.name))
|
||||||
|
@ -717,25 +718,63 @@ class Qbt:
|
||||||
return
|
return
|
||||||
# Create recycle bin if not exists
|
# Create recycle bin if not exists
|
||||||
recycle_path = self.config.recycle_dir
|
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)
|
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')
|
# Move files from torrent contents to Recycle bin
|
||||||
if len(tor_files) == 1: print_line(tor_files[0], 'DEBUG')
|
for file in tor_files:
|
||||||
else: print_multiline("\n".join(tor_files), 'DEBUG')
|
src = file.replace(self.config.root_dir, self.config.remote_dir)
|
||||||
logger.debug(f'Moved {len(tor_files)} files to {recycle_path.replace(self.config.remote_dir,self.config.root_dir)}')
|
dest = os.path.join(recycle_path, file.replace(self.config.root_dir, ''))
|
||||||
|
# Move files and change date modified
|
||||||
# Move files from torrent contents to Recycle bin
|
try:
|
||||||
for file in tor_files:
|
util.move_files(src, dest, True)
|
||||||
src = file.replace(self.config.root_dir, self.config.remote_dir)
|
except FileNotFoundError:
|
||||||
dest = os.path.join(recycle_path, file.replace(self.config.root_dir, ''))
|
e = print_line(f'RecycleBin Warning - FileNotFound: No such file or directory: {src} ', 'WARNING')
|
||||||
# Move files and change date modified
|
self.config.notify(e, 'Deleting Torrent', False)
|
||||||
try:
|
# Delete torrent and files
|
||||||
util.move_files(src, dest, True)
|
torrent.delete(delete_files=False)
|
||||||
except FileNotFoundError:
|
# Remove any empty directories
|
||||||
print_line(f'RecycleBin Warning - FileNotFound: No such file or directory: {src} ', 'WARNING')
|
util.remove_empty_directories(torrent.save_path.replace(self.config.root_dir, self.config.remote_dir), "**/*")
|
||||||
# Delete torrent and files
|
else:
|
||||||
torrent.delete(hash=torrent.hash, delete_files=False)
|
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:
|
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 logging.handlers import RotatingFileHandler
|
||||||
from ruamel import yaml
|
from ruamel import yaml
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@ -297,6 +297,14 @@ def move_files(src, dest, mod=False):
|
||||||
os.utime(dest, (modTime, modTime))
|
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
|
# Remove any empty directories after moving files
|
||||||
def remove_empty_directories(pathlib_root_dir, pattern):
|
def remove_empty_directories(pathlib_root_dir, pattern):
|
||||||
pathlib_root_dir = Path(pathlib_root_dir)
|
pathlib_root_dir = Path(pathlib_root_dir)
|
||||||
|
@ -328,6 +336,23 @@ def nohardlink(file):
|
||||||
return check
|
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
|
# Gracefully kill script when docker stops
|
||||||
class GracefulKiller:
|
class GracefulKiller:
|
||||||
kill_now = False
|
kill_now = False
|
||||||
|
|
Loading…
Add table
Reference in a new issue