mirror of
https://github.com/StuffAnThings/qbit_manage.git
synced 2025-10-08 21:06:28 +08:00
commit
56970218b7
9 changed files with 298 additions and 158 deletions
1
.flake8
1
.flake8
|
@ -8,4 +8,5 @@ ignore =
|
|||
E272, # E272 Multiple spaces before keyword
|
||||
C901 # C901 Function is too complex
|
||||
E722 # E722 Do not use bare except, specify exception instead
|
||||
W503 # W503 Line break occurred before a binary operator
|
||||
max-line-length = 200
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
3.1.5
|
||||
3.2.0
|
|
@ -10,16 +10,20 @@ qbt:
|
|||
pass: "password"
|
||||
|
||||
settings:
|
||||
force_auto_tmm: False # Will force qBittorrent to enable Automatic Torrent Management for each torrent.
|
||||
|
||||
force_auto_tmm: False # Will force qBittorrent to enable Automatic Torrent Management for each torrent.
|
||||
tracker_error_tag: issue # Will set the tag of any torrents that do not have a working tracker.
|
||||
ignoreTags_OnUpdate: # When running tag-update function, it will update torrent tags for a given torrent even if the torrent has one or more of the tags defined here.
|
||||
- noHL
|
||||
- issue
|
||||
- cross-seed
|
||||
directory:
|
||||
# Do not remove these
|
||||
# Cross-seed var: </your/path/here/> # Output directory of cross-seed
|
||||
# root_dir var: </your/path/here/> # Root downloads directory used to check for orphaned files, noHL, and RecycleBin.
|
||||
# <OPTIONAL> remote_dir var: </your/path/here/> # Path of docker host mapping of root_dir.
|
||||
# Cross-seed var: </your/path/here/> # Output directory of cross-seed
|
||||
# root_dir var: </your/path/here/> # Root downloads directory used to check for orphaned files, noHL, and RecycleBin.
|
||||
# <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
|
||||
# <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/"
|
||||
|
@ -29,14 +33,14 @@ directory:
|
|||
|
||||
# Category & Path Parameters
|
||||
cat:
|
||||
# <Category Name> : <save_path> # Path of your save directory.
|
||||
# <Category Name> : <save_path> # Path of your save directory.
|
||||
movies: "/data/torrents/Movies"
|
||||
tv: "/data/torrents/TV"
|
||||
|
||||
# Tag Parameters
|
||||
tracker:
|
||||
# <Tracker URL Keyword>: # <MANDATORY> This is the keyword in the tracker url
|
||||
# <MANDATORY> Set tag name
|
||||
# <MANDATORY> Set tag name. Can be a list of tags or a single tag
|
||||
# tag: <Tag Name>
|
||||
# <OPTIONAL> Will set the torrent Maximum share ratio until torrent is stopped from seeding/uploading. -2 means the global limit should be used, -1 means no limit.
|
||||
# max_ratio: 5.0
|
||||
|
@ -50,13 +54,16 @@ tracker:
|
|||
tag: AnimeBytes
|
||||
notifiarr: animebytes
|
||||
avistaz:
|
||||
tag: Avistaz
|
||||
tag:
|
||||
- Avistaz
|
||||
- tag2
|
||||
- tag3
|
||||
max_ratio: 5.0
|
||||
max_seeding_time: 129600
|
||||
limit_upload_speed: 150
|
||||
notifiarr: avistaz
|
||||
beyond-hd:
|
||||
tag: Beyond-HD
|
||||
tag: [Beyond-HD, tag2, tag3]
|
||||
notifiarr: beyondhd
|
||||
blutopia:
|
||||
tag: Blutopia
|
||||
|
@ -186,6 +193,7 @@ webhooks:
|
|||
cat_update: apprise
|
||||
tag_update: notifiarr
|
||||
rem_unregistered: notifiarr
|
||||
tag_tracker_error: notifiarr
|
||||
rem_orphaned: notifiarr
|
||||
tag_nohardlinks: notifiarr
|
||||
empty_recyclebin: notifiarr
|
||||
|
|
|
@ -79,7 +79,10 @@ class Config:
|
|||
|
||||
self.settings = {
|
||||
"force_auto_tmm": self.util.check_for_attribute(self.data, "force_auto_tmm", parent="settings", var_type="bool", default=False),
|
||||
"tracker_error_tag": self.util.check_for_attribute(self.data, "tracker_error_tag", parent="settings", default='issue')
|
||||
}
|
||||
default_ignore_tags = ['noHL', self.settings["tracker_error_tag"], 'cross-seed']
|
||||
self.settings["ignoreTags_OnUpdate"] = self.util.check_for_attribute(self.data, "ignoreTags_OnUpdate", parent="settings", default=default_ignore_tags, var_type="list")
|
||||
|
||||
default_function = {
|
||||
'cross_seed': None,
|
||||
|
@ -87,9 +90,11 @@ class Config:
|
|||
'cat_update': None,
|
||||
'tag_update': None,
|
||||
'rem_unregistered': None,
|
||||
'tag_tracker_error': None,
|
||||
'rem_orphaned': None,
|
||||
'tag_nohardlinks': None,
|
||||
'empty_recyclebin': None}
|
||||
'empty_recyclebin': None
|
||||
}
|
||||
|
||||
self.webhooks = {
|
||||
"error": self.util.check_for_attribute(self.data, "error", parent="webhooks", var_type="list", default_is_none=True),
|
||||
|
@ -97,6 +102,8 @@ class Config:
|
|||
"run_end": self.util.check_for_attribute(self.data, "run_end", parent="webhooks", var_type="list", default_is_none=True),
|
||||
"function": self.util.check_for_attribute(self.data, "function", parent="webhooks", var_type="list", default=default_function)
|
||||
}
|
||||
for func in default_function:
|
||||
self.util.check_for_attribute(self.data, func, parent="webhooks", subparent="function", default_is_none=True)
|
||||
|
||||
self.AppriseFactory = None
|
||||
if "apprise" in self.data:
|
||||
|
@ -192,7 +199,17 @@ class Config:
|
|||
self.cross_seed_dir = self.util.check_for_attribute(self.data, "cross_seed", parent="directory", var_type="path")
|
||||
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'), make_dirs=True)
|
||||
if self.recyclebin['enabled']:
|
||||
if "recycle_bin" in self.data["directory"]:
|
||||
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')
|
||||
if self.recyclebin['split_by_category']:
|
||||
self.recycle_dir = self.util.check_for_attribute(self.data, "recycle_bin", parent="directory", default=default_recycle)
|
||||
else:
|
||||
self.recycle_dir = self.util.check_for_attribute(self.data, "recycle_bin", parent="directory", var_type="path", default=default_recycle, make_dirs=True)
|
||||
else:
|
||||
self.recycle_dir = None
|
||||
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)):
|
||||
|
@ -207,10 +224,11 @@ class Config:
|
|||
raise Failed(e)
|
||||
|
||||
# Add Orphaned
|
||||
exclude_recycle = f"**/{os.path.basename(self.recycle_dir.rstrip('/'))}/*"
|
||||
self.orphaned = {}
|
||||
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)
|
||||
self.orphaned['exclude_patterns'].append(exclude_recycle) if exclude_recycle not in self.orphaned['exclude_patterns'] else self.orphaned['exclude_patterns']
|
||||
if self.recyclebin['enabled']:
|
||||
exclude_recycle = f"**/{os.path.basename(self.recycle_dir.rstrip('/'))}/*"
|
||||
self.orphaned['exclude_patterns'].append(exclude_recycle) if exclude_recycle not in self.orphaned['exclude_patterns'] else self.orphaned['exclude_patterns']
|
||||
|
||||
# Connect to Qbittorrent
|
||||
self.qbt = None
|
||||
|
@ -255,17 +273,18 @@ class Config:
|
|||
logger.debug(e)
|
||||
# If using Format 1 convert to format 2
|
||||
if isinstance(tag_details, str):
|
||||
tracker['tag'] = self.util.check_for_attribute(self.data, tag_url, parent="tracker", default=default_tag)
|
||||
self.util.check_for_attribute(self.data, "tag", parent="tracker", subparent=tag_url, default=tracker['tag'], do_print=False)
|
||||
tracker['tag'] = self.util.check_for_attribute(self.data, tag_url, parent="tracker", default=default_tag, var_type="list")
|
||||
self.util.check_for_attribute(self.data, "tag", parent="tracker", subparent=tag_url, default=tracker['tag'], do_print=False, var_type="list")
|
||||
if tracker['tag'] == default_tag:
|
||||
try:
|
||||
self.data['tracker'][tag_url]['tag'] = default_tag
|
||||
self.data['tracker'][tag_url]['tag'] = [default_tag]
|
||||
except Exception:
|
||||
self.data['tracker'][tag_url] = {'tag': default_tag}
|
||||
self.data['tracker'][tag_url] = {'tag': [default_tag]}
|
||||
# Using Format 2
|
||||
else:
|
||||
tracker['tag'] = self.util.check_for_attribute(self.data, "tag", parent="tracker", subparent=tag_url, default=tag_url)
|
||||
if tracker['tag'] == tag_url: self.data['tracker'][tag_url]['tag'] = tag_url
|
||||
tracker['tag'] = self.util.check_for_attribute(self.data, "tag", parent="tracker", subparent=tag_url, default=tag_url, var_type="list")
|
||||
if tracker['tag'] == [tag_url]: self.data['tracker'][tag_url]['tag'] = [tag_url]
|
||||
if isinstance(tracker['tag'], str): tracker['tag'] = [tracker['tag']]
|
||||
tracker['max_ratio'] = self.util.check_for_attribute(self.data, "max_ratio", parent="tracker", subparent=tag_url,
|
||||
var_type="float", default_int=-2, default_is_none=True, do_print=False, save=False)
|
||||
tracker['max_seeding_time'] = self.util.check_for_attribute(self.data, "max_seeding_time", parent="tracker", subparent=tag_url,
|
||||
|
@ -276,11 +295,12 @@ class Config:
|
|||
return (tracker)
|
||||
if tracker['url']:
|
||||
default_tag = tracker['url'].split('/')[2].split(':')[0]
|
||||
tracker['tag'] = self.util.check_for_attribute(self.data, "tag", parent="tracker", subparent=default_tag, default=default_tag)
|
||||
tracker['tag'] = self.util.check_for_attribute(self.data, "tag", parent="tracker", subparent=default_tag, default=default_tag, var_type="list")
|
||||
if isinstance(tracker['tag'], str): tracker['tag'] = [tracker['tag']]
|
||||
try:
|
||||
self.data['tracker'][default_tag]['tag'] = default_tag
|
||||
self.data['tracker'][default_tag]['tag'] = [default_tag]
|
||||
except Exception:
|
||||
self.data['tracker'][default_tag] = {'tag': default_tag}
|
||||
self.data['tracker'][default_tag] = {'tag': [default_tag]}
|
||||
e = (f'No tags matched for {tracker["url"]}. Please check your config.yml file. Setting tag to {default_tag}')
|
||||
self.notify(e, 'Tag', False)
|
||||
logger.warning(e)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import logging, os, sys
|
||||
from qbittorrentapi import Client, LoginFailed, APIConnectionError, NotFound404Error, Conflict409Error
|
||||
from qbittorrentapi import Client, Version, LoginFailed, APIConnectionError, NotFound404Error, Conflict409Error
|
||||
from modules import util
|
||||
from modules.util import Failed, print_line, print_multiline, separator
|
||||
from modules.util import Failed, print_line, print_multiline, separator, list_in_text
|
||||
from datetime import timedelta
|
||||
from collections import Counter
|
||||
from fnmatch import fnmatch
|
||||
|
@ -11,7 +11,6 @@ logger = logging.getLogger("qBit Manage")
|
|||
|
||||
|
||||
class Qbt:
|
||||
SUPPORTED_VERSION = 'v4.3'
|
||||
|
||||
def __init__(self, config, params):
|
||||
self.config = config
|
||||
|
@ -21,16 +20,19 @@ class Qbt:
|
|||
self.password = params["password"]
|
||||
logger.debug(f'Host: {self.host}, Username: {self.username}, Password: {self.password if self.password is None else "[REDACTED]"}')
|
||||
try:
|
||||
self.client = Client(host=self.host, username=self.username, password=self.password)
|
||||
self.client = Client(host=self.host, username=self.username, password=self.password, VERIFY_WEBUI_CERTIFICATE=False)
|
||||
self.client.auth_log_in()
|
||||
|
||||
SUPPORTED_VERSION = Version.latest_supported_app_version()
|
||||
CURRENT_VERSION = self.client.app.version
|
||||
logger.debug(f'qBittorrent: {self.client.app.version}')
|
||||
logger.debug(f'qBittorrent Web API: {self.client.app.web_api_version}')
|
||||
logger.debug(f'qbit_manage support version: {self.SUPPORTED_VERSION}')
|
||||
current_version = ".".join(self.client.app.version.split(".")[:2])
|
||||
if current_version > self.SUPPORTED_VERSION:
|
||||
e = f"Qbittorrent Error: qbit_manage is only comaptible with {self.SUPPORTED_VERSION}.* or lower. You are currently on {self.client.app.version}"
|
||||
logger.debug(f'qbit_manage support version: {SUPPORTED_VERSION}')
|
||||
if not Version.is_app_version_supported(CURRENT_VERSION):
|
||||
e = (f"Qbittorrent Error: qbit_manage is only comaptible with {SUPPORTED_VERSION} or lower. You are currently on {CURRENT_VERSION}." + '\n'
|
||||
+ f"Please downgrade to your Qbittorrent version to {SUPPORTED_VERSION} to use qbit_manage.")
|
||||
self.config.notify(e, "Qbittorrent")
|
||||
print_line(e, 'CRITICAL')
|
||||
print_multiline(e, 'CRITICAL')
|
||||
sys.exit(0)
|
||||
logger.info("Qbt Connection Successful")
|
||||
except LoginFailed:
|
||||
|
@ -80,7 +82,9 @@ class Qbt:
|
|||
is_complete = False
|
||||
msg = None
|
||||
status = None
|
||||
if torrent.auto_tmm is False and settings['force_auto_tmm'] and not dry_run:
|
||||
working_tracker = None
|
||||
issue = {'potential': False}
|
||||
if torrent.auto_tmm is False and settings['force_auto_tmm'] and torrent.category != '' and not dry_run:
|
||||
torrent.set_auto_management(True)
|
||||
try:
|
||||
torrent_name = torrent.name
|
||||
|
@ -106,21 +110,27 @@ class Qbt:
|
|||
status_list = []
|
||||
is_complete = torrent_is_complete
|
||||
first_hash = torrent_hash
|
||||
working_tracker = torrent.tracker
|
||||
for x in torrent_trackers:
|
||||
if x.url.startswith('http'):
|
||||
status = x.status
|
||||
msg = x.msg.upper()
|
||||
exception = ["DOWN", "UNREACHABLE", "BAD GATEWAY", "TRACKER UNAVAILABLE"]
|
||||
if x.status == 2:
|
||||
working_tracker = True
|
||||
break
|
||||
# Add any potential unregistered torrents to a list
|
||||
if x.status == 4 and not list_in_text(msg, exception):
|
||||
issue['potential'] = True
|
||||
issue['msg'] = msg
|
||||
issue['status'] = status
|
||||
if working_tracker:
|
||||
status = 2
|
||||
msg = ''
|
||||
t_obj_valid.append(torrent)
|
||||
else:
|
||||
for x in torrent_trackers:
|
||||
if x.url.startswith('http'):
|
||||
status = x.status
|
||||
msg = x.msg.upper()
|
||||
exception = ["DOWN", "UNREACHABLE", "BAD GATEWAY", "TRACKER UNAVAILABLE"]
|
||||
# Add any potential unregistered torrents to a list
|
||||
if x.status == 4 and all(x not in msg for x in exception):
|
||||
t_obj_unreg.append(torrent)
|
||||
break
|
||||
elif issue['potential']:
|
||||
status = issue['status']
|
||||
msg = issue['msg']
|
||||
t_obj_unreg.append(torrent)
|
||||
if msg is not None: msg_list.append(msg)
|
||||
if status is not None: status_list.append(status)
|
||||
torrentattr = {
|
||||
|
@ -132,7 +142,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']:
|
||||
if config.args['recheck'] or config.args['cross_seed'] or config.args['rem_unregistered'] or config.args['tag_tracker_error']:
|
||||
# Get an updated torrent dictionary information of the torrents
|
||||
self.torrentinfo, self.torrentissue, self.torrentvalid = get_torrent_info(self.torrent_list)
|
||||
|
||||
|
@ -145,33 +155,35 @@ class Qbt:
|
|||
num_cat = 0
|
||||
if self.config.args['cat_update']:
|
||||
separator("Updating Categories", space=False, border=False)
|
||||
for torrent in self.torrent_list:
|
||||
if torrent.category == '':
|
||||
new_cat = self.config.get_category(torrent.save_path)
|
||||
tracker = self.config.get_tags([x.url for x in torrent.trackers if x.url.startswith('http')])
|
||||
if not dry_run:
|
||||
try:
|
||||
torrent.set_category(category=new_cat)
|
||||
except Conflict409Error:
|
||||
e = print_line(f'Existing category "{new_cat}" not found for save path {torrent.save_path}, category will be created.', loglevel)
|
||||
self.config.notify(e, 'Update Category', False)
|
||||
self.client.torrent_categories.create_category(name=new_cat, save_path=torrent.save_path)
|
||||
torrent.set_category(category=new_cat)
|
||||
body = []
|
||||
body += print_line(util.insert_space(f'Torrent Name: {torrent.name}', 3), loglevel)
|
||||
body += print_line(util.insert_space(f'New Category: {new_cat}', 3), loglevel)
|
||||
body += print_line(util.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel)
|
||||
attr = {
|
||||
"function": "cat_update",
|
||||
"title": "Updating Categories",
|
||||
"body": "\n".join(body),
|
||||
"torrent_name": torrent.name,
|
||||
"torrent_category": new_cat,
|
||||
"torrent_tracker": tracker["url"],
|
||||
"notifiarr_indexer": tracker["notifiarr"]
|
||||
}
|
||||
self.config.send_notifications(attr)
|
||||
num_cat += 1
|
||||
torrent_list = self.get_torrents({'category': '', 'filter': 'completed'})
|
||||
for torrent in torrent_list:
|
||||
new_cat = self.config.get_category(torrent.save_path)
|
||||
tracker = self.config.get_tags([x.url for x in torrent.trackers if x.url.startswith('http')])
|
||||
if not dry_run:
|
||||
try:
|
||||
torrent.set_category(category=new_cat)
|
||||
if torrent.auto_tmm is False and self.config.settings['force_auto_tmm']:
|
||||
torrent.set_auto_management(True)
|
||||
except Conflict409Error:
|
||||
e = print_line(f'Existing category "{new_cat}" not found for save path {torrent.save_path}, category will be created.', loglevel)
|
||||
self.config.notify(e, 'Update Category', False)
|
||||
self.client.torrent_categories.create_category(name=new_cat, save_path=torrent.save_path)
|
||||
torrent.set_category(category=new_cat)
|
||||
body = []
|
||||
body += print_line(util.insert_space(f'Torrent Name: {torrent.name}', 3), loglevel)
|
||||
body += print_line(util.insert_space(f'New Category: {new_cat}', 3), loglevel)
|
||||
body += print_line(util.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel)
|
||||
attr = {
|
||||
"function": "cat_update",
|
||||
"title": "Updating Categories",
|
||||
"body": "\n".join(body),
|
||||
"torrent_name": torrent.name,
|
||||
"torrent_category": new_cat,
|
||||
"torrent_tracker": tracker["url"],
|
||||
"notifiarr_indexer": tracker["notifiarr"]
|
||||
}
|
||||
self.config.send_notifications(attr)
|
||||
num_cat += 1
|
||||
if num_cat >= 1:
|
||||
print_line(f"{'Did not update' if dry_run else 'Updated'} {num_cat} new categories.", loglevel)
|
||||
else:
|
||||
|
@ -182,7 +194,7 @@ class Qbt:
|
|||
dry_run = self.config.args['dry_run']
|
||||
loglevel = 'DRYRUN' if dry_run else 'INFO'
|
||||
num_tags = 0
|
||||
ignore_tags = ['noHL', 'issue', 'cross-seed']
|
||||
ignore_tags = self.config.settings['ignoreTags_OnUpdate']
|
||||
if self.config.args['tag_update']:
|
||||
separator("Updating Tags", space=False, border=False)
|
||||
for torrent in self.torrent_list:
|
||||
|
@ -190,10 +202,10 @@ class Qbt:
|
|||
if torrent.tags == '' or (len([x for x in check_tags if x not in ignore_tags]) == 0):
|
||||
tracker = self.config.get_tags([x.url for x in torrent.trackers if x.url.startswith('http')])
|
||||
if tracker["tag"]:
|
||||
num_tags += 1
|
||||
num_tags += len(tracker["tag"])
|
||||
body = []
|
||||
body += print_line(util.insert_space(f'Torrent Name: {torrent.name}', 3), loglevel)
|
||||
body += print_line(util.insert_space(f'New Tag: {tracker["tag"]}', 8), loglevel)
|
||||
body += print_line(util.insert_space(f'New Tag{"s" if len(tracker["tag"]) > 1 else ""}: {", ".join(tracker["tag"])}', 8), loglevel)
|
||||
body += print_line(util.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel)
|
||||
body.extend(self.set_tags_and_limits(torrent, tracker["max_ratio"], tracker["max_seeding_time"], tracker["limit_upload_speed"], tracker["tag"]))
|
||||
category = self.config.get_category(torrent.save_path) if torrent.category == '' else torrent.category
|
||||
|
@ -203,7 +215,7 @@ class Qbt:
|
|||
"body": "\n".join(body),
|
||||
"torrent_name": torrent.name,
|
||||
"torrent_category": category,
|
||||
"torrent_tag": tracker["tag"],
|
||||
"torrent_tag": ", ".join(tracker["tag"]),
|
||||
"torrent_tracker": tracker["url"],
|
||||
"notifiarr_indexer": tracker["notifiarr"],
|
||||
"torrent_max_ratio": tracker["max_ratio"],
|
||||
|
@ -270,10 +282,10 @@ class Qbt:
|
|||
for category in nohardlinks:
|
||||
torrent_list = self.get_torrents({'category': category, 'filter': 'completed'})
|
||||
if len(torrent_list) == 0:
|
||||
e = 'No torrents found in the category ('+category+') defined under nohardlinks attribute in the config. \
|
||||
Please check if this matches with any category in qbittorrent and has 1 or more torrents.'
|
||||
self.config.notify(e, 'Tag No Hard Links', False)
|
||||
logger.error(e)
|
||||
e = 'No torrents found in the category ('+category+') defined under nohardlinks attribute in the config. ' + \
|
||||
'Please check if this matches with any category in qbittorrent and has 1 or more torrents.'
|
||||
# self.config.notify(e, 'Tag No Hard Links', False)
|
||||
logger.warning(e)
|
||||
continue
|
||||
for torrent in alive_it(torrent_list):
|
||||
tracker = self.config.get_tags([x.url for x in torrent.trackers if x.url.startswith('http')])
|
||||
|
@ -389,8 +401,35 @@ class Qbt:
|
|||
loglevel = 'DRYRUN' if dry_run else 'INFO'
|
||||
del_tor = 0
|
||||
del_tor_cont = 0
|
||||
pot_unreg = 0
|
||||
pot_unr_summary = ''
|
||||
num_tor_error = 0
|
||||
num_untag = 0
|
||||
tor_error_summary = ''
|
||||
tag_error = self.config.settings['tracker_error_tag']
|
||||
cfg_rem_unregistered = self.config.args['rem_unregistered']
|
||||
cfg_tag_error = self.config.args['tag_tracker_error']
|
||||
|
||||
def tag_tracker_error():
|
||||
nonlocal dry_run, t_name, msg_up, tracker, t_cat, torrent, tag_error, tor_error_summary, num_tor_error
|
||||
tor_error = ''
|
||||
tor_error += (util.insert_space(f'Torrent Name: {t_name}', 3)+'\n')
|
||||
tor_error += (util.insert_space(f'Status: {msg_up}', 9)+'\n')
|
||||
tor_error += (util.insert_space(f'Tracker: {tracker["url"]}', 8)+'\n')
|
||||
tor_error += (util.insert_space(f"Added Tag: {tag_error}", 6)+'\n')
|
||||
tor_error_summary += tor_error
|
||||
num_tor_error += 1
|
||||
attr = {
|
||||
"function": "tag_tracker_error",
|
||||
"title": "Tag Tracker Error Torrents",
|
||||
"body": tor_error,
|
||||
"torrent_name": t_name,
|
||||
"torrent_category": t_cat,
|
||||
"torrent_tag": tag_error,
|
||||
"torrent_status": msg_up,
|
||||
"torrent_tracker": tracker["url"],
|
||||
"notifiarr_indexer": tracker["notifiarr"],
|
||||
}
|
||||
self.config.send_notifications(attr)
|
||||
if not dry_run: torrent.add_tags(tags=tag_error)
|
||||
|
||||
def del_unregistered():
|
||||
nonlocal dry_run, loglevel, del_tor, del_tor_cont, t_name, msg_up, tracker, t_cat, t_msg, t_status, torrent
|
||||
|
@ -427,8 +466,9 @@ class Qbt:
|
|||
attr["body"] = "\n".join(body)
|
||||
self.config.send_notifications(attr)
|
||||
|
||||
if self.config.args['rem_unregistered']:
|
||||
separator("Removing Unregistered Torrents", space=False, border=False)
|
||||
if cfg_rem_unregistered or cfg_tag_error:
|
||||
if cfg_tag_error: separator("Tagging Torrents with Tracker Errors", space=False, border=False)
|
||||
elif cfg_rem_unregistered: separator("Removing Unregistered Torrents", space=False, border=False)
|
||||
unreg_msgs = [
|
||||
'UNREGISTERED',
|
||||
'TORRENT NOT FOUND',
|
||||
|
@ -438,16 +478,39 @@ class Qbt:
|
|||
'UNKNOWN TORRENT',
|
||||
'TRUMP',
|
||||
'RETITLED',
|
||||
'TRUNCATED'
|
||||
'TRUNCATED',
|
||||
'TORRENT IS NOT AUTHORIZED FOR USE ON THIS TRACKER'
|
||||
]
|
||||
ignore_msgs = [
|
||||
'YOU HAVE REACHED THE CLIENT LIMIT FOR THIS TORRENT'
|
||||
'YOU HAVE REACHED THE CLIENT LIMIT FOR THIS TORRENT',
|
||||
'MISSING PASSKEY',
|
||||
'MISSING INFO_HASH',
|
||||
'PASSKEY IS INVALID',
|
||||
'INVALID PASSKEY'
|
||||
]
|
||||
for torrent in self.torrentvalid:
|
||||
check_tags = util.get_list(torrent.tags)
|
||||
# Remove any potential unregistered torrents Tags that are no longer unreachable.
|
||||
if 'issue' in check_tags:
|
||||
if not dry_run: torrent.remove_tags(tags='issue')
|
||||
# Remove any error torrents Tags that are no longer unreachable.
|
||||
if tag_error in check_tags:
|
||||
tracker = self.config.get_tags([x.url for x in torrent.trackers if x.url.startswith('http')])
|
||||
num_untag += 1
|
||||
body = []
|
||||
body += print_line(f'Previous Tagged {tag_error} torrent currently has a working tracker.', loglevel)
|
||||
body += print_line(util.insert_space(f'Torrent Name: {torrent.name}', 3), loglevel)
|
||||
body += print_line(util.insert_space(f'Removed Tag: {tag_error}', 4), loglevel)
|
||||
body += print_line(util.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel)
|
||||
if not dry_run: torrent.remove_tags(tags=tag_error)
|
||||
attr = {
|
||||
"function": "untag_tracker_error",
|
||||
"title": "Untagging Tracker Error Torrent",
|
||||
"body": "\n".join(body),
|
||||
"torrent_name": torrent.name,
|
||||
"torrent_category": torrent.category,
|
||||
"torrent_tag": tag_error,
|
||||
"torrent_tracker": tracker["url"],
|
||||
"notifiarr_indexer": tracker["notifiarr"]
|
||||
}
|
||||
self.config.send_notifications(attr)
|
||||
for torrent in self.torrentissue:
|
||||
t_name = torrent.name
|
||||
t_cat = self.torrentinfo[t_name]['Category']
|
||||
|
@ -460,53 +523,41 @@ class Qbt:
|
|||
if x.url.startswith('http'):
|
||||
tracker = self.config.get_tags([x.url])
|
||||
msg_up = x.msg.upper()
|
||||
# Tag any potential unregistered torrents
|
||||
if not any(m in msg_up for m in unreg_msgs) and x.status == 4 and 'issue' not in check_tags:
|
||||
# Check for unregistered torrents using BHD API if the tracker is BHD
|
||||
if 'tracker.beyond-hd.me' in tracker['url'] and self.config.BeyondHD is not None and all(x not in msg_up for x in ignore_msgs):
|
||||
json = {"info_hash": torrent.hash}
|
||||
response = self.config.BeyondHD.search(json)
|
||||
if response['total_results'] <= 1:
|
||||
del_unregistered()
|
||||
break
|
||||
pot_unr = ''
|
||||
pot_unr += (util.insert_space(f'Torrent Name: {t_name}', 3)+'\n')
|
||||
pot_unr += (util.insert_space(f'Status: {msg_up}', 9)+'\n')
|
||||
pot_unr += (util.insert_space(f'Tracker: {tracker["url"]}', 8)+'\n')
|
||||
pot_unr += (util.insert_space("Added Tag: 'issue'", 6)+'\n')
|
||||
pot_unr_summary += pot_unr
|
||||
pot_unreg += 1
|
||||
attr = {
|
||||
"function": "potential_rem_unregistered",
|
||||
"title": "Potential Unregistered Torrents",
|
||||
"body": pot_unr,
|
||||
"torrent_name": t_name,
|
||||
"torrent_category": t_cat,
|
||||
"torrent_tag": "issue",
|
||||
"torrent_status": msg_up,
|
||||
"torrent_tracker": tracker["url"],
|
||||
"notifiarr_indexer": tracker["notifiarr"],
|
||||
}
|
||||
self.config.send_notifications(attr)
|
||||
if not dry_run: torrent.add_tags(tags='issue')
|
||||
if any(m in msg_up for m in unreg_msgs) and x.status == 4:
|
||||
del_unregistered()
|
||||
break
|
||||
# Tag any error torrents
|
||||
if cfg_tag_error:
|
||||
if x.status == 4 and tag_error not in check_tags:
|
||||
tag_tracker_error()
|
||||
if cfg_rem_unregistered:
|
||||
# Tag any error torrents that are not unregistered
|
||||
if not list_in_text(msg_up, unreg_msgs) and x.status == 4 and tag_error not in check_tags:
|
||||
# Check for unregistered torrents using BHD API if the tracker is BHD
|
||||
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:
|
||||
del_unregistered()
|
||||
break
|
||||
tag_tracker_error()
|
||||
if list_in_text(msg_up, unreg_msgs) and x.status == 4:
|
||||
del_unregistered()
|
||||
break
|
||||
except NotFound404Error:
|
||||
continue
|
||||
except Exception as e:
|
||||
util.print_stacktrace()
|
||||
self.config.notify(e, 'Remove Unregistered Torrents', False)
|
||||
logger.error(f"Unknown Error: {e}")
|
||||
if del_tor >= 1 or del_tor_cont >= 1:
|
||||
if del_tor >= 1: print_line(f"{'Did not delete' if dry_run else 'Deleted'} {del_tor} .torrent{'s' if del_tor > 1 else ''} but not content files.", loglevel)
|
||||
if del_tor_cont >= 1: print_line(f"{'Did not delete' if dry_run else 'Deleted'} {del_tor_cont} .torrent{'s' if del_tor_cont > 1 else ''} AND content files.", loglevel)
|
||||
else:
|
||||
print_line('No unregistered torrents found.', loglevel)
|
||||
if (pot_unreg > 0):
|
||||
separator(f"{pot_unreg} Potential Unregistered torrents found", space=False, border=False, loglevel=loglevel)
|
||||
print_multiline(pot_unr_summary.rstrip(), loglevel)
|
||||
return del_tor, del_tor_cont, pot_unreg
|
||||
if cfg_rem_unregistered:
|
||||
if del_tor >= 1 or del_tor_cont >= 1:
|
||||
if del_tor >= 1: print_line(f"{'Did not delete' if dry_run else 'Deleted'} {del_tor} .torrent{'s' if del_tor > 1 else ''} but not content files.", loglevel)
|
||||
if del_tor_cont >= 1: print_line(f"{'Did not delete' if dry_run else 'Deleted'} {del_tor_cont} .torrent{'s' if del_tor_cont > 1 else ''} AND content files.", loglevel)
|
||||
else:
|
||||
print_line('No unregistered torrents found.', loglevel)
|
||||
if num_untag >= 1: print_line(f"{'Did not delete' if dry_run else 'Deleted'} {tag_error} tags for {num_untag} .torrent{'s.' if num_untag > 1 else '.'}", loglevel)
|
||||
if num_tor_error >= 1:
|
||||
separator(f"{num_tor_error} Torrents with tracker errors found", space=False, border=False, loglevel=loglevel)
|
||||
print_multiline(tor_error_summary.rstrip(), loglevel)
|
||||
return del_tor, del_tor_cont, num_tor_error, num_untag
|
||||
|
||||
# Function used to move any torrents from the cross seed directory to the correct save directory
|
||||
def cross_seed(self):
|
||||
|
|
|
@ -189,6 +189,19 @@ def add_dict_list(keys, value, dict_map):
|
|||
dict_map[key] = [value]
|
||||
|
||||
|
||||
def list_in_text(text, search_list, match_all=False):
|
||||
if isinstance(search_list, list): search_list = set(search_list)
|
||||
contains = set([x for x in search_list if ' ' in x])
|
||||
exception = search_list - contains
|
||||
if match_all:
|
||||
if all(x == m for m in text.split(" ") for x in exception) or all(x in text for x in contains):
|
||||
return True
|
||||
else:
|
||||
if any(x == m for m in text.split(" ") for x in exception) or any(x in text for x in contains):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def print_line(lines, loglevel='INFO'):
|
||||
logger.log(getattr(logging, loglevel.upper()), str(lines))
|
||||
return [str(lines)]
|
||||
|
@ -295,7 +308,11 @@ def move_files(src, dest, mod=False):
|
|||
dest_path = os.path.dirname(dest)
|
||||
if os.path.isdir(dest_path) is False:
|
||||
os.makedirs(dest_path)
|
||||
shutil.move(src, dest)
|
||||
try:
|
||||
shutil.move(src, dest)
|
||||
except Exception as e:
|
||||
print_stacktrace()
|
||||
logger.error(e)
|
||||
if mod is True:
|
||||
modTime = time.time()
|
||||
os.utime(dest, (modTime, modTime))
|
||||
|
@ -306,7 +323,11 @@ 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)
|
||||
try:
|
||||
shutil.copyfile(src, dest)
|
||||
except Exception as e:
|
||||
print_stacktrace()
|
||||
logger.error(e)
|
||||
|
||||
|
||||
# Remove any empty directories after moving files
|
||||
|
|
|
@ -104,10 +104,11 @@ class Webhooks:
|
|||
"torrents_categorized": stats["categorized"],
|
||||
"torrents_tagged": stats["tagged"],
|
||||
"remove_unregistered": stats["rem_unreg"],
|
||||
"potential_unregistered": stats["pot_unreg"],
|
||||
"torrents_tagged_tracker_error": stats["tagged_tracker_error"],
|
||||
"torrents_untagged_tracker_error": stats["untagged_tracker_error"],
|
||||
"orphaned_files_found": stats["orphaned"],
|
||||
"torrents_tagged_no_hardlinks": stats["taggednoHL"],
|
||||
"torrents_untagged_no_hardlinks": stats["untagged"],
|
||||
"torrents_tagged_no_hardlinks": stats["tagged_noHL"],
|
||||
"torrents_untagged_no_hardlinks": stats["untagged_noHL"],
|
||||
"files_deleted_from_recyclebin": stats["recycle_emptied"]
|
||||
})
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ parser.add_argument("-db", "--debug", dest="debug", help=argparse.SUPPRESS, acti
|
|||
parser.add_argument("-tr", "--trace", dest="trace", help=argparse.SUPPRESS, action="store_true", default=False)
|
||||
parser.add_argument('-r', '--run', dest='run', action='store_true', default=False, help='Run without the scheduler. Script will exit after completion.')
|
||||
parser.add_argument('-sch', '--schedule', dest='min', default='1440', type=str, help='Schedule to run every x minutes. (Default set to 1440 (1 day))')
|
||||
parser.add_argument('-sd', '--startup-delay', dest='startupDelay', default='0', type=str, help='Set delay in seconds on the first run of a schedule (Default set to 0)')
|
||||
parser.add_argument('-c', '--config-file', dest='configfile', action='store', default='config.yml', type=str,
|
||||
help='This is used if you want to use a different name for your config.yml. Example: tv.yml')
|
||||
parser.add_argument('-lf', '--log-file', dest='logfile', action='store', default='activity.log', type=str, help='This is used if you want to use a different name for your log file. Example: tv.log',)
|
||||
|
@ -34,6 +35,7 @@ parser.add_argument('-cu', '--cat-update', dest='cat_update', action="store_true
|
|||
parser.add_argument('-tu', '--tag-update', dest='tag_update', action="store_true", default=False,
|
||||
help='Use this if you would like to update your tags and/or set seed goals/limit upload speed by tag. (Only adds tags to untagged torrents)')
|
||||
parser.add_argument('-ru', '--rem-unregistered', dest='rem_unregistered', action="store_true", default=False, help='Use this if you would like to remove unregistered torrents.')
|
||||
parser.add_argument('-tte', '--tag-tracker-error', dest='tag_tracker_error', action="store_true", default=False, help='Use this if you would like to tag torrents that do not have a working tracker.')
|
||||
parser.add_argument('-ro', '--rem-orphaned', dest='rem_orphaned', action="store_true", default=False, help='Use this if you would like to remove unregistered torrents.')
|
||||
parser.add_argument('-tnhl', '--tag-nohardlinks', dest='tag_nohardlinks', action="store_true", default=False,
|
||||
help='Use this to tag any torrents that do not have any hard links associated with any of the files. \
|
||||
|
@ -69,6 +71,7 @@ def get_arg(env_str, default, arg_bool=False, arg_int=False):
|
|||
|
||||
run = get_arg("QBT_RUN", args.run, arg_bool=True)
|
||||
sch = get_arg("QBT_SCHEDULE", args.min)
|
||||
startupDelay = get_arg("QBT_STARTUP_DELAY", args.startupDelay)
|
||||
config_file = get_arg("QBT_CONFIG", args.configfile)
|
||||
log_file = get_arg("QBT_LOGFILE", args.logfile)
|
||||
cross_seed = get_arg("QBT_CROSS_SEED", args.cross_seed, arg_bool=True)
|
||||
|
@ -76,6 +79,7 @@ recheck = get_arg("QBT_RECHECK", args.recheck, arg_bool=True)
|
|||
cat_update = get_arg("QBT_CAT_UPDATE", args.cat_update, arg_bool=True)
|
||||
tag_update = get_arg("QBT_TAG_UPDATE", args.tag_update, arg_bool=True)
|
||||
rem_unregistered = get_arg("QBT_REM_UNREGISTERED", args.rem_unregistered, arg_bool=True)
|
||||
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)
|
||||
|
@ -99,6 +103,7 @@ else:
|
|||
for v in [
|
||||
'run',
|
||||
'sch',
|
||||
'startupDelay',
|
||||
'config_file',
|
||||
'log_file',
|
||||
'cross_seed',
|
||||
|
@ -106,6 +111,7 @@ for v in [
|
|||
'cat_update',
|
||||
'tag_update',
|
||||
'rem_unregistered',
|
||||
'tag_tracker_error',
|
||||
'rem_orphaned',
|
||||
'tag_nohardlinks',
|
||||
'skip_recycle',
|
||||
|
@ -132,6 +138,13 @@ except ValueError:
|
|||
print(f"Schedule Error: Schedule is not a number. Current value is set to '{sch}'")
|
||||
sys.exit(0)
|
||||
|
||||
# Check if StartupDelay parameter is a number
|
||||
try:
|
||||
startupDelay = int(startupDelay)
|
||||
except ValueError:
|
||||
print(f"startupDelay Error: startupDelay is not a number. Current value is set to '{startupDelay}'")
|
||||
sys.exit(0)
|
||||
|
||||
logger = logging.getLogger('qBit Manage')
|
||||
logging.DRYRUN = 25
|
||||
logging.addLevelName(logging.DRYRUN, 'DRYRUN')
|
||||
|
@ -188,6 +201,10 @@ def start():
|
|||
start_type = ""
|
||||
util.separator(f"Starting {start_type}Run")
|
||||
cfg = None
|
||||
body = ''
|
||||
run_time = ''
|
||||
end_time = None
|
||||
next_run = None
|
||||
global stats
|
||||
stats = {
|
||||
"added": 0,
|
||||
|
@ -198,17 +215,35 @@ def start():
|
|||
"orphaned": 0,
|
||||
"recycle_emptied": 0,
|
||||
"tagged": 0,
|
||||
"untagged": 0,
|
||||
"categorized": 0,
|
||||
"rem_unreg": 0,
|
||||
"pot_unreg": 0,
|
||||
"taggednoHL": 0
|
||||
"tagged_tracker_error": 0,
|
||||
"untagged_tracker_error": 0,
|
||||
"tagged_noHL": 0,
|
||||
"untagged_noHL": 0
|
||||
}
|
||||
|
||||
def FinishedRun():
|
||||
nonlocal end_time, start_time, start_type, stats_summary, run_time, next_run, body
|
||||
end_time = datetime.now()
|
||||
run_time = str(end_time - start_time).split('.')[0]
|
||||
_, nr = calc_next_run(sch, True)
|
||||
next_run_str = nr['next_run_str']
|
||||
next_run = nr['next_run']
|
||||
body = util.separator(f"Finished {start_type}Run\n{os.linesep.join(stats_summary) if len(stats_summary)>0 else ''}\nRun Time: {run_time}\n{next_run_str if len(next_run_str)>0 else ''}"
|
||||
.replace('\n\n', '\n').rstrip())[0]
|
||||
return next_run, body
|
||||
try:
|
||||
cfg = Config(default_dir, args)
|
||||
except Exception as e:
|
||||
util.print_stacktrace()
|
||||
util.print_multiline(e, 'CRITICAL')
|
||||
if 'Qbittorrent Error' in e.args[0]:
|
||||
util.print_multiline(e, 'CRITICAL')
|
||||
util.print_line('Exiting scheduled Run.', 'CRITICAL')
|
||||
FinishedRun()
|
||||
return None
|
||||
else:
|
||||
util.print_stacktrace()
|
||||
util.print_multiline(e, 'CRITICAL')
|
||||
|
||||
if cfg:
|
||||
# Set Category
|
||||
|
@ -220,11 +255,13 @@ def start():
|
|||
stats["tagged"] += num_tagged
|
||||
|
||||
# Remove Unregistered Torrents
|
||||
num_deleted, num_deleted_contents, num_pot_unreg = cfg.qbt.rem_unregistered()
|
||||
num_deleted, num_deleted_contents, num_tagged, num_untagged = cfg.qbt.rem_unregistered()
|
||||
stats["rem_unreg"] += (num_deleted + num_deleted_contents)
|
||||
stats["deleted"] += num_deleted
|
||||
stats["deleted_contents"] += num_deleted_contents
|
||||
stats["pot_unreg"] += num_pot_unreg
|
||||
stats["tagged_tracker_error"] += num_tagged
|
||||
stats["untagged_tracker_error"] += num_untagged
|
||||
stats["tagged"] += num_tagged
|
||||
|
||||
# Set Cross Seed
|
||||
num_added, num_tagged = cfg.qbt.cross_seed()
|
||||
|
@ -239,8 +276,8 @@ def start():
|
|||
# Tag NoHardLinks
|
||||
num_tagged, num_untagged, num_deleted, num_deleted_contents = cfg.qbt.tag_nohardlinks()
|
||||
stats["tagged"] += num_tagged
|
||||
stats["taggednoHL"] += num_tagged
|
||||
stats["untagged"] += num_untagged
|
||||
stats["tagged_noHL"] += num_tagged
|
||||
stats["untagged_noHL"] += num_untagged
|
||||
stats["deleted"] += num_deleted
|
||||
stats["deleted_contents"] += num_deleted_contents
|
||||
|
||||
|
@ -248,31 +285,27 @@ def start():
|
|||
num_orphaned = cfg.qbt.rem_orphaned()
|
||||
stats["orphaned"] += num_orphaned
|
||||
|
||||
# mpty RecycleBin
|
||||
# Empty RecycleBin
|
||||
recycle_emptied = cfg.empty_recycle()
|
||||
stats["recycle_emptied"] += recycle_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']}")
|
||||
if stats["pot_unreg"] > 0: stats_summary.append(f"Total Potential Unregistered Torrents Found: {stats['pot_unreg']}")
|
||||
if stats["tagged_tracker_error"] > 0: stats_summary.append(f"Total {cfg.settings['tracker_error_tag']} Torrents Tagged: {stats['tagged_tracker_error']}")
|
||||
if stats["untagged_tracker_error"] > 0: stats_summary.append(f"Total {cfg.settings['tracker_error_tag']} Torrents untagged: {stats['untagged_tracker_error']}")
|
||||
if stats["added"] > 0: stats_summary.append(f"Total Torrents Added: {stats['added']}")
|
||||
if stats["resumed"] > 0: stats_summary.append(f"Total Torrents Resumed: {stats['resumed']}")
|
||||
if stats["rechecked"] > 0: stats_summary.append(f"Total Torrents Rechecked: {stats['rechecked']}")
|
||||
if stats["deleted"] > 0: stats_summary.append(f"Total Torrents Deleted: {stats['deleted']}")
|
||||
if stats["deleted_contents"] > 0: stats_summary.append(f"Total Torrents + Contents Deleted : {stats['deleted_contents']}")
|
||||
if stats["orphaned"] > 0: stats_summary.append(f"Total Orphaned Files: {stats['orphaned']}")
|
||||
if stats["taggednoHL"] > 0: stats_summary.append(f"Total noHL Torrents Tagged: {stats['taggednoHL']}")
|
||||
if stats["untagged"] > 0: stats_summary.append(f"Total noHL Torrents untagged: {stats['untagged']}")
|
||||
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']}")
|
||||
|
||||
end_time = datetime.now()
|
||||
run_time = str(end_time - start_time).split('.')[0]
|
||||
_, nr = calc_next_run(sch, True)
|
||||
next_run_str = nr['next_run_str']
|
||||
next_run = nr['next_run']
|
||||
body = util.separator(f"Finished {start_type}Run\n{os.linesep.join(stats_summary) if len(stats_summary)>0 else ''}\nRun Time: {run_time}\n{next_run_str if len(next_run_str)>0 else ''}"
|
||||
.replace('\n\n', '\n').rstrip())[0]
|
||||
FinishedRun()
|
||||
|
||||
if cfg:
|
||||
try:
|
||||
cfg.Webhooks.end_time_hooks(start_time, end_time, run_time, next_run, stats, body)
|
||||
|
@ -329,6 +362,7 @@ if __name__ == '__main__':
|
|||
util.separator(loglevel='DEBUG')
|
||||
logger.debug(f" --run (QBT_RUN): {run}")
|
||||
logger.debug(f" --schedule (QBT_SCHEDULE): {sch}")
|
||||
logger.debug(f" --startup-delay (QBT_STARTUP_DELAY): {startupDelay}")
|
||||
logger.debug(f" --config-file (QBT_CONFIG): {config_file}")
|
||||
logger.debug(f" --log-file (QBT_LOGFILE): {log_file}")
|
||||
logger.debug(f" --cross-seed (QBT_CROSS_SEED): {cross_seed}")
|
||||
|
@ -336,6 +370,7 @@ if __name__ == '__main__':
|
|||
logger.debug(f" --cat-update (QBT_CAT_UPDATE): {cat_update}")
|
||||
logger.debug(f" --tag-update (QBT_TAG_UPDATE): {tag_update}")
|
||||
logger.debug(f" --rem-unregistered (QBT_REM_UNREGISTERED): {rem_unregistered}")
|
||||
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}")
|
||||
|
@ -354,6 +389,9 @@ if __name__ == '__main__':
|
|||
schedule.every(sch).minutes.do(start)
|
||||
time_str, _ = calc_next_run(sch)
|
||||
logger.info(f" Scheduled Mode: Running every {time_str}.")
|
||||
if startupDelay:
|
||||
logger.info(f" Startup Delay: Initial Run will start after {startupDelay} seconds")
|
||||
time.sleep(startupDelay)
|
||||
start()
|
||||
while not killer.kill_now:
|
||||
schedule.run_pending()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
ruamel.yaml==0.17.20
|
||||
qbittorrent-api==2021.12.26
|
||||
qbittorrent-api>=2022.1.27
|
||||
schedule==1.1.0
|
||||
retrying==1.3.3
|
||||
alive_progress==2.1.0
|
||||
|
|
Loading…
Add table
Reference in a new issue