mirror of
https://github.com/StuffAnThings/qbit_manage.git
synced 2025-09-13 00:24:45 +08:00
commit
1a378ecafe
7 changed files with 109 additions and 24 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
3.2.0
|
||||
3.2.1
|
|
@ -200,7 +200,7 @@ class Config:
|
|||
else:
|
||||
self.cross_seed_dir = self.util.check_for_attribute(self.data, "cross_seed", parent="directory", default_is_none=True)
|
||||
if self.recyclebin['enabled']:
|
||||
if "recycle_bin" in self.data["directory"]:
|
||||
if "recycle_bin" in self.data["directory"] and self.data["directory"]["recycle_bin"] is not None:
|
||||
default_recycle = os.path.join(self.remote_dir, os.path.basename(self.data['directory']['recycle_bin'].rstrip('/')))
|
||||
else:
|
||||
default_recycle = os.path.join(self.remote_dir, '.RecycleBin')
|
||||
|
|
|
@ -14,7 +14,7 @@ class Qbt:
|
|||
|
||||
def __init__(self, config, params):
|
||||
self.config = config
|
||||
config_handler.set_global(bar=None, receipt_text=False)
|
||||
config_handler.set_global(bar=None, receipt=False)
|
||||
self.host = params["host"]
|
||||
self.username = params["username"]
|
||||
self.password = params["password"]
|
||||
|
@ -114,7 +114,14 @@ class Qbt:
|
|||
if x.url.startswith('http'):
|
||||
status = x.status
|
||||
msg = x.msg.upper()
|
||||
exception = ["DOWN", "UNREACHABLE", "BAD GATEWAY", "TRACKER UNAVAILABLE"]
|
||||
exception = [
|
||||
"DOWN",
|
||||
"DOWN.",
|
||||
"UNREACHABLE",
|
||||
"(UNREACHABLE)",
|
||||
"BAD GATEWAY",
|
||||
"TRACKER UNAVAILABLE"
|
||||
]
|
||||
if x.status == 2:
|
||||
working_tracker = True
|
||||
break
|
||||
|
@ -142,7 +149,7 @@ class Qbt:
|
|||
self.torrentinfo = None
|
||||
self.torrentissue = None
|
||||
self.torrentvalid = None
|
||||
if config.args['recheck'] or config.args['cross_seed'] or config.args['rem_unregistered'] or config.args['tag_tracker_error']:
|
||||
if config.args['recheck'] or config.args['cross_seed'] or config.args['rem_unregistered'] or config.args['tag_tracker_error'] or config.args['tag_nohardlinks']:
|
||||
# Get an updated torrent dictionary information of the torrents
|
||||
self.torrentinfo, self.torrentissue, self.torrentvalid = get_torrent_info(self.torrent_list)
|
||||
|
||||
|
@ -358,28 +365,39 @@ class Qbt:
|
|||
# loop through torrent list again for cleanup purposes
|
||||
if (nohardlinks[category]['cleanup']):
|
||||
for torrent in torrent_list:
|
||||
if torrent.name in tdel_dict.keys() and 'noHL' in torrent.tags:
|
||||
t_name = torrent.name
|
||||
if t_name in tdel_dict.keys() and 'noHL' in torrent.tags:
|
||||
t_count = self.torrentinfo[t_name]['count']
|
||||
t_msg = self.torrentinfo[t_name]['msg']
|
||||
t_status = self.torrentinfo[t_name]['status']
|
||||
# Double check that the content path is the same before we delete anything
|
||||
if torrent['content_path'].replace(root_dir, root_dir) == tdel_dict[torrent.name]:
|
||||
if torrent['content_path'].replace(root_dir, root_dir) == tdel_dict[t_name]:
|
||||
tracker = self.config.get_tags([x.url for x in torrent.trackers if x.url.startswith('http')])
|
||||
body = []
|
||||
body += print_line(util.insert_space(f'Torrent Name: {torrent.name}', 3), loglevel)
|
||||
body += print_line(util.insert_space(f'Torrent Name: {t_name}', 3), loglevel)
|
||||
body += print_line(util.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel)
|
||||
body += print_line(util.insert_space("Cleanup: True [No hard links found and meets Share Limits.]", 8), loglevel)
|
||||
attr = {
|
||||
"function": "cleanup_tag_nohardlinks",
|
||||
"title": "Removing NoHL Torrents and meets Share Limits",
|
||||
"torrent_name": torrent.name,
|
||||
"torrent_name": t_name,
|
||||
"torrent_category": torrent.category,
|
||||
"cleanup": 'True',
|
||||
"torrent_tracker": tracker["url"],
|
||||
"notifiarr_indexer": tracker["notifiarr"],
|
||||
}
|
||||
if (os.path.exists(torrent['content_path'].replace(root_dir, root_dir))):
|
||||
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)
|
||||
# Checks if any of the original torrents are working
|
||||
if t_count > 1 and ('' in t_msg or 2 in t_status):
|
||||
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)
|
||||
else:
|
||||
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:
|
||||
del_tor += 1
|
||||
attr["torrents_deleted_and_contents"] = False
|
||||
|
@ -387,6 +405,7 @@ class Qbt:
|
|||
body += print_line(util.insert_space('Deleted .torrent but NOT content files.', 8), loglevel)
|
||||
attr["body"] = "\n".join(body)
|
||||
self.config.send_notifications(attr)
|
||||
self.torrentinfo[t_name]['count'] -= 1
|
||||
if num_tags >= 1:
|
||||
print_line(f"{'Did not Tag/set' if dry_run else 'Tag/set'} share limits for {num_tags} .torrent{'s.' if num_tags > 1 else '.'}", loglevel)
|
||||
else:
|
||||
|
@ -465,6 +484,7 @@ class Qbt:
|
|||
del_tor_cont += 1
|
||||
attr["body"] = "\n".join(body)
|
||||
self.config.send_notifications(attr)
|
||||
self.torrentinfo[t_name]['count'] -= 1
|
||||
|
||||
if cfg_rem_unregistered or cfg_tag_error:
|
||||
if cfg_tag_error: separator("Tagging Torrents with Tracker Errors", space=False, border=False)
|
||||
|
@ -486,7 +506,9 @@ class Qbt:
|
|||
'MISSING PASSKEY',
|
||||
'MISSING INFO_HASH',
|
||||
'PASSKEY IS INVALID',
|
||||
'INVALID PASSKEY'
|
||||
'INVALID PASSKEY',
|
||||
'EXPECTED VALUE (LIST, DICT, INT OR STRING) IN BENCODED STRING',
|
||||
'COULD NOT PARSE BENCODED DATA'
|
||||
]
|
||||
for torrent in self.torrentvalid:
|
||||
check_tags = util.get_list(torrent.tags)
|
||||
|
@ -534,7 +556,7 @@ class Qbt:
|
|||
if 'tracker.beyond-hd.me' in tracker['url'] and self.config.BeyondHD is not None and not list_in_text(msg_up, ignore_msgs):
|
||||
json = {"info_hash": torrent.hash}
|
||||
response = self.config.BeyondHD.search(json)
|
||||
if response['total_results'] <= 1:
|
||||
if response['total_results'] == 0:
|
||||
del_unregistered()
|
||||
break
|
||||
tag_tracker_error()
|
||||
|
@ -866,12 +888,12 @@ class Qbt:
|
|||
dest = os.path.join(recycle_path, file.replace(self.config.remote_dir, ''))
|
||||
# Move files and change date modified
|
||||
try:
|
||||
util.move_files(src, dest, True)
|
||||
toDelete = 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)
|
||||
torrent.delete(delete_files=toDelete)
|
||||
# Remove any empty directories
|
||||
util.remove_empty_directories(save_path, "**/*")
|
||||
else:
|
||||
|
|
|
@ -59,7 +59,7 @@ class check:
|
|||
else:
|
||||
text = f"{parent} sub-attribute {attribute}"
|
||||
|
||||
if data is None or attribute not in data:
|
||||
if data is None or attribute not in data or (attribute in data and data[attribute] is None and not default_is_none):
|
||||
message = f"{text} not found"
|
||||
if parent and save is True:
|
||||
loaded_config, _, _ = yaml.util.load_yaml_guess_indent(open(self.config.config_path))
|
||||
|
@ -77,7 +77,7 @@ class check:
|
|||
endline = f"\n{parent} sub-attribute {attribute} added to config"
|
||||
if parent not in loaded_config or not loaded_config[parent]:
|
||||
loaded_config[parent] = {attribute: default}
|
||||
elif attribute not in loaded_config[parent]:
|
||||
elif attribute not in loaded_config[parent] or (attribute in loaded_config[parent] and loaded_config[parent][attribute] is None):
|
||||
loaded_config[parent][attribute] = default
|
||||
else:
|
||||
endline = ""
|
||||
|
@ -306,16 +306,22 @@ def trunc_val(s, d, n=3):
|
|||
# Move files from source to destination, mod variable is to change the date modified of the file being moved
|
||||
def move_files(src, dest, mod=False):
|
||||
dest_path = os.path.dirname(dest)
|
||||
toDelete = False
|
||||
if os.path.isdir(dest_path) is False:
|
||||
os.makedirs(dest_path)
|
||||
try:
|
||||
if mod is True:
|
||||
modTime = time.time()
|
||||
os.utime(src, (modTime, modTime))
|
||||
shutil.move(src, dest)
|
||||
except PermissionError as p:
|
||||
logger.warning(f"{p} : Copying files instead.")
|
||||
shutil.copyfile(src, dest)
|
||||
toDelete = True
|
||||
except Exception as e:
|
||||
print_stacktrace()
|
||||
logger.error(e)
|
||||
if mod is True:
|
||||
modTime = time.time()
|
||||
os.utime(dest, (modTime, modTime))
|
||||
return toDelete
|
||||
|
||||
|
||||
# Copy Files from source to destination
|
||||
|
|
|
@ -98,7 +98,7 @@ class Webhooks:
|
|||
"run_time": run_time,
|
||||
"torrents_added": stats["added"],
|
||||
"torrents_deleted": stats["deleted"],
|
||||
"torrents_deleted_and_contents": stats["deleted_contents"],
|
||||
"torrents_deleted_and_contents_count": stats["deleted_contents"],
|
||||
"torrents_resumed": stats["resumed"],
|
||||
"torrents_rechecked": stats["rechecked"],
|
||||
"torrents_categorized": stats["categorized"],
|
||||
|
|
|
@ -2,5 +2,5 @@ ruamel.yaml==0.17.20
|
|||
qbittorrent-api>=2022.1.27
|
||||
schedule==1.1.0
|
||||
retrying==1.3.3
|
||||
alive_progress==2.1.0
|
||||
alive_progress==2.2.0
|
||||
requests==2.27.1
|
57
scripts/mover.py
Normal file
57
scripts/mover.py
Normal file
|
@ -0,0 +1,57 @@
|
|||
#!/usr/bin/env python3
|
||||
# This standalone script is used to pause torrents older than last x days, run mover (in Unraid) and start torrents again once completed
|
||||
import os, sys, time
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
# --DEFINE VARIABLES--#
|
||||
# Set Number of Days to stop torrents for the move
|
||||
days = 2
|
||||
qbt_host = 'qbittorrent:8080'
|
||||
qbt_user = None
|
||||
qbt_pass = None
|
||||
# --DEFINE VARIABLES--#
|
||||
|
||||
# --START SCRIPT--#
|
||||
try:
|
||||
from qbittorrentapi import Client, LoginFailed, APIConnectionError
|
||||
except ModuleNotFoundError:
|
||||
print("Requirements Error: qbittorrentapi not installed. Please install with pip")
|
||||
sys.exit(0)
|
||||
|
||||
current = datetime.now()
|
||||
timeoffset = (current - timedelta(days=days)).timestamp()
|
||||
|
||||
|
||||
def stop_start_torrents(torrent_list, pause=True):
|
||||
for torrent in torrent_list:
|
||||
if (torrent.added_on >= timeoffset):
|
||||
if pause:
|
||||
torrent.pause()
|
||||
else:
|
||||
torrent.resume()
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
client = Client(host=qbt_host, username=qbt_user, password=qbt_pass)
|
||||
except LoginFailed:
|
||||
raise("Qbittorrent Error: Failed to login. Invalid username/password.")
|
||||
except APIConnectionError:
|
||||
raise("Qbittorrent Error: Unable to connect to the client.")
|
||||
except Exception:
|
||||
raise("Qbittorrent Error: Unable to connect to the client.")
|
||||
torrent_list = client.torrents.info(sort='added_on', reverse=True)
|
||||
|
||||
# Pause Torrents
|
||||
print(f"Pausing torrents from the last {days} Days")
|
||||
stop_start_torrents(torrent_list, True)
|
||||
time.sleep(10)
|
||||
# Start mover
|
||||
print("Starting Mover")
|
||||
os.system('/usr/local/sbin/mover.old start')
|
||||
# Start Torrents
|
||||
print(f"Resuming paused torrents from the last {days} Days")
|
||||
stop_start_torrents(torrent_list, False)
|
Loading…
Add table
Reference in a new issue