Merge pull request #92 from StuffAnThings/develop

3.2.0
This commit is contained in:
bobokun 2022-01-16 23:05:54 -05:00 committed by GitHub
commit 56970218b7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 298 additions and 158 deletions

View file

@ -8,4 +8,5 @@ ignore =
E272, # E272 Multiple spaces before keyword E272, # E272 Multiple spaces before keyword
C901 # C901 Function is too complex C901 # C901 Function is too complex
E722 # E722 Do not use bare except, specify exception instead E722 # E722 Do not use bare except, specify exception instead
W503 # W503 Line break occurred before a binary operator
max-line-length = 200 max-line-length = 200

View file

@ -1 +1 @@
3.1.5 3.2.0

View file

@ -10,16 +10,20 @@ qbt:
pass: "password" pass: "password"
settings: 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: directory:
# Do not remove these # Do not remove these
# Cross-seed var: </your/path/here/> # Output directory of cross-seed # 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. # 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. # <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 # <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/"
@ -29,14 +33,14 @@ directory:
# Category & Path Parameters # Category & Path Parameters
cat: cat:
# <Category Name> : <save_path> # Path of your save directory. # <Category Name> : <save_path> # Path of your save directory.
movies: "/data/torrents/Movies" movies: "/data/torrents/Movies"
tv: "/data/torrents/TV" tv: "/data/torrents/TV"
# Tag Parameters # Tag Parameters
tracker: tracker:
# <Tracker URL Keyword>: # <MANDATORY> This is the keyword in the tracker url # <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> # 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. # <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 # max_ratio: 5.0
@ -50,13 +54,16 @@ tracker:
tag: AnimeBytes tag: AnimeBytes
notifiarr: animebytes notifiarr: animebytes
avistaz: avistaz:
tag: Avistaz tag:
- Avistaz
- tag2
- tag3
max_ratio: 5.0 max_ratio: 5.0
max_seeding_time: 129600 max_seeding_time: 129600
limit_upload_speed: 150 limit_upload_speed: 150
notifiarr: avistaz notifiarr: avistaz
beyond-hd: beyond-hd:
tag: Beyond-HD tag: [Beyond-HD, tag2, tag3]
notifiarr: beyondhd notifiarr: beyondhd
blutopia: blutopia:
tag: Blutopia tag: Blutopia
@ -186,6 +193,7 @@ webhooks:
cat_update: apprise cat_update: apprise
tag_update: notifiarr tag_update: notifiarr
rem_unregistered: notifiarr rem_unregistered: notifiarr
tag_tracker_error: notifiarr
rem_orphaned: notifiarr rem_orphaned: notifiarr
tag_nohardlinks: notifiarr tag_nohardlinks: notifiarr
empty_recyclebin: notifiarr empty_recyclebin: notifiarr

View file

@ -79,7 +79,10 @@ class Config:
self.settings = { self.settings = {
"force_auto_tmm": self.util.check_for_attribute(self.data, "force_auto_tmm", parent="settings", var_type="bool", default=False), "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 = { default_function = {
'cross_seed': None, 'cross_seed': None,
@ -87,9 +90,11 @@ class Config:
'cat_update': None, 'cat_update': None,
'tag_update': None, 'tag_update': None,
'rem_unregistered': None, 'rem_unregistered': None,
'tag_tracker_error': None,
'rem_orphaned': None, 'rem_orphaned': None,
'tag_nohardlinks': None, 'tag_nohardlinks': None,
'empty_recyclebin': None} 'empty_recyclebin': None
}
self.webhooks = { self.webhooks = {
"error": self.util.check_for_attribute(self.data, "error", parent="webhooks", var_type="list", default_is_none=True), "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), "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) "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 self.AppriseFactory = None
if "apprise" in self.data: 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") self.cross_seed_dir = self.util.check_for_attribute(self.data, "cross_seed", parent="directory", var_type="path")
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'), 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']: 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") 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)): if not any(File.endswith(".torrent") for File in os.listdir(self.torrents_dir)):
@ -207,10 +224,11 @@ class Config:
raise Failed(e) raise Failed(e)
# Add Orphaned # Add Orphaned
exclude_recycle = f"**/{os.path.basename(self.recycle_dir.rstrip('/'))}/*"
self.orphaned = {} 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'] = 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 # Connect to Qbittorrent
self.qbt = None self.qbt = None
@ -255,17 +273,18 @@ class Config:
logger.debug(e) logger.debug(e)
# If using Format 1 convert to format 2 # If using Format 1 convert to format 2
if isinstance(tag_details, str): if isinstance(tag_details, str):
tracker['tag'] = self.util.check_for_attribute(self.data, tag_url, parent="tracker", default=default_tag) 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) 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: if tracker['tag'] == default_tag:
try: try:
self.data['tracker'][tag_url]['tag'] = default_tag self.data['tracker'][tag_url]['tag'] = [default_tag]
except Exception: except Exception:
self.data['tracker'][tag_url] = {'tag': default_tag} self.data['tracker'][tag_url] = {'tag': [default_tag]}
# Using Format 2 # Using Format 2
else: else:
tracker['tag'] = self.util.check_for_attribute(self.data, "tag", parent="tracker", subparent=tag_url, default=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 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, 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) 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, 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) return (tracker)
if tracker['url']: if tracker['url']:
default_tag = tracker['url'].split('/')[2].split(':')[0] 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: try:
self.data['tracker'][default_tag]['tag'] = default_tag self.data['tracker'][default_tag]['tag'] = [default_tag]
except Exception: 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}') e = (f'No tags matched for {tracker["url"]}. Please check your config.yml file. Setting tag to {default_tag}')
self.notify(e, 'Tag', False) self.notify(e, 'Tag', False)
logger.warning(e) logger.warning(e)

View file

@ -1,7 +1,7 @@
import logging, os, sys 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 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 datetime import timedelta
from collections import Counter from collections import Counter
from fnmatch import fnmatch from fnmatch import fnmatch
@ -11,7 +11,6 @@ logger = logging.getLogger("qBit Manage")
class Qbt: class Qbt:
SUPPORTED_VERSION = 'v4.3'
def __init__(self, config, params): def __init__(self, config, params):
self.config = config self.config = config
@ -21,16 +20,19 @@ class Qbt:
self.password = params["password"] self.password = params["password"]
logger.debug(f'Host: {self.host}, Username: {self.username}, Password: {self.password if self.password is None else "[REDACTED]"}') logger.debug(f'Host: {self.host}, Username: {self.username}, Password: {self.password if self.password is None else "[REDACTED]"}')
try: 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() 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: {self.client.app.version}')
logger.debug(f'qBittorrent Web API: {self.client.app.web_api_version}') logger.debug(f'qBittorrent Web API: {self.client.app.web_api_version}')
logger.debug(f'qbit_manage support version: {self.SUPPORTED_VERSION}') logger.debug(f'qbit_manage support version: {SUPPORTED_VERSION}')
current_version = ".".join(self.client.app.version.split(".")[:2]) if not Version.is_app_version_supported(CURRENT_VERSION):
if current_version > self.SUPPORTED_VERSION: e = (f"Qbittorrent Error: qbit_manage is only comaptible with {SUPPORTED_VERSION} or lower. You are currently on {CURRENT_VERSION}." + '\n'
e = f"Qbittorrent Error: qbit_manage is only comaptible with {self.SUPPORTED_VERSION}.* or lower. You are currently on {self.client.app.version}" + f"Please downgrade to your Qbittorrent version to {SUPPORTED_VERSION} to use qbit_manage.")
self.config.notify(e, "Qbittorrent") self.config.notify(e, "Qbittorrent")
print_line(e, 'CRITICAL') print_multiline(e, 'CRITICAL')
sys.exit(0) sys.exit(0)
logger.info("Qbt Connection Successful") logger.info("Qbt Connection Successful")
except LoginFailed: except LoginFailed:
@ -80,7 +82,9 @@ class Qbt:
is_complete = False is_complete = False
msg = None msg = None
status = 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) torrent.set_auto_management(True)
try: try:
torrent_name = torrent.name torrent_name = torrent.name
@ -106,21 +110,27 @@ class Qbt:
status_list = [] status_list = []
is_complete = torrent_is_complete is_complete = torrent_is_complete
first_hash = torrent_hash 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: if working_tracker:
status = 2 status = 2
msg = '' msg = ''
t_obj_valid.append(torrent) t_obj_valid.append(torrent)
else: elif issue['potential']:
for x in torrent_trackers: status = issue['status']
if x.url.startswith('http'): msg = issue['msg']
status = x.status t_obj_unreg.append(torrent)
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
if msg is not None: msg_list.append(msg) if msg is not None: msg_list.append(msg)
if status is not None: status_list.append(status) if status is not None: status_list.append(status)
torrentattr = { torrentattr = {
@ -132,7 +142,7 @@ class Qbt:
self.torrentinfo = None self.torrentinfo = None
self.torrentissue = None self.torrentissue = None
self.torrentvalid = 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 # Get an updated torrent dictionary information of the torrents
self.torrentinfo, self.torrentissue, self.torrentvalid = get_torrent_info(self.torrent_list) self.torrentinfo, self.torrentissue, self.torrentvalid = get_torrent_info(self.torrent_list)
@ -145,33 +155,35 @@ class Qbt:
num_cat = 0 num_cat = 0
if self.config.args['cat_update']: if self.config.args['cat_update']:
separator("Updating Categories", space=False, border=False) separator("Updating Categories", space=False, border=False)
for torrent in self.torrent_list: torrent_list = self.get_torrents({'category': '', 'filter': 'completed'})
if torrent.category == '': for torrent in torrent_list:
new_cat = self.config.get_category(torrent.save_path) 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')]) tracker = self.config.get_tags([x.url for x in torrent.trackers if x.url.startswith('http')])
if not dry_run: if not dry_run:
try: try:
torrent.set_category(category=new_cat) torrent.set_category(category=new_cat)
except Conflict409Error: if torrent.auto_tmm is False and self.config.settings['force_auto_tmm']:
e = print_line(f'Existing category "{new_cat}" not found for save path {torrent.save_path}, category will be created.', loglevel) torrent.set_auto_management(True)
self.config.notify(e, 'Update Category', False) except Conflict409Error:
self.client.torrent_categories.create_category(name=new_cat, save_path=torrent.save_path) e = print_line(f'Existing category "{new_cat}" not found for save path {torrent.save_path}, category will be created.', loglevel)
torrent.set_category(category=new_cat) self.config.notify(e, 'Update Category', False)
body = [] self.client.torrent_categories.create_category(name=new_cat, save_path=torrent.save_path)
body += print_line(util.insert_space(f'Torrent Name: {torrent.name}', 3), loglevel) torrent.set_category(category=new_cat)
body += print_line(util.insert_space(f'New Category: {new_cat}', 3), loglevel) body = []
body += print_line(util.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel) body += print_line(util.insert_space(f'Torrent Name: {torrent.name}', 3), loglevel)
attr = { body += print_line(util.insert_space(f'New Category: {new_cat}', 3), loglevel)
"function": "cat_update", body += print_line(util.insert_space(f'Tracker: {tracker["url"]}', 8), loglevel)
"title": "Updating Categories", attr = {
"body": "\n".join(body), "function": "cat_update",
"torrent_name": torrent.name, "title": "Updating Categories",
"torrent_category": new_cat, "body": "\n".join(body),
"torrent_tracker": tracker["url"], "torrent_name": torrent.name,
"notifiarr_indexer": tracker["notifiarr"] "torrent_category": new_cat,
} "torrent_tracker": tracker["url"],
self.config.send_notifications(attr) "notifiarr_indexer": tracker["notifiarr"]
num_cat += 1 }
self.config.send_notifications(attr)
num_cat += 1
if num_cat >= 1: if num_cat >= 1:
print_line(f"{'Did not update' if dry_run else 'Updated'} {num_cat} new categories.", loglevel) print_line(f"{'Did not update' if dry_run else 'Updated'} {num_cat} new categories.", loglevel)
else: else:
@ -182,7 +194,7 @@ class Qbt:
dry_run = self.config.args['dry_run'] dry_run = self.config.args['dry_run']
loglevel = 'DRYRUN' if dry_run else 'INFO' loglevel = 'DRYRUN' if dry_run else 'INFO'
num_tags = 0 num_tags = 0
ignore_tags = ['noHL', 'issue', 'cross-seed'] ignore_tags = self.config.settings['ignoreTags_OnUpdate']
if self.config.args['tag_update']: if self.config.args['tag_update']:
separator("Updating Tags", space=False, border=False) separator("Updating Tags", space=False, border=False)
for torrent in self.torrent_list: 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): 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')]) tracker = self.config.get_tags([x.url for x in torrent.trackers if x.url.startswith('http')])
if tracker["tag"]: if tracker["tag"]:
num_tags += 1 num_tags += len(tracker["tag"])
body = [] body = []
body += print_line(util.insert_space(f'Torrent Name: {torrent.name}', 3), loglevel) 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 += 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"])) 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 category = self.config.get_category(torrent.save_path) if torrent.category == '' else torrent.category
@ -203,7 +215,7 @@ class Qbt:
"body": "\n".join(body), "body": "\n".join(body),
"torrent_name": torrent.name, "torrent_name": torrent.name,
"torrent_category": category, "torrent_category": category,
"torrent_tag": tracker["tag"], "torrent_tag": ", ".join(tracker["tag"]),
"torrent_tracker": tracker["url"], "torrent_tracker": tracker["url"],
"notifiarr_indexer": tracker["notifiarr"], "notifiarr_indexer": tracker["notifiarr"],
"torrent_max_ratio": tracker["max_ratio"], "torrent_max_ratio": tracker["max_ratio"],
@ -270,10 +282,10 @@ class Qbt:
for category in nohardlinks: for category in nohardlinks:
torrent_list = self.get_torrents({'category': category, 'filter': 'completed'}) torrent_list = self.get_torrents({'category': category, 'filter': 'completed'})
if len(torrent_list) == 0: if len(torrent_list) == 0:
e = 'No torrents found in the category ('+category+') defined under nohardlinks attribute in the config. \ 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.' '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) # self.config.notify(e, 'Tag No Hard Links', False)
logger.error(e) logger.warning(e)
continue continue
for torrent in alive_it(torrent_list): for torrent in alive_it(torrent_list):
tracker = self.config.get_tags([x.url for x in torrent.trackers if x.url.startswith('http')]) 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' loglevel = 'DRYRUN' if dry_run else 'INFO'
del_tor = 0 del_tor = 0
del_tor_cont = 0 del_tor_cont = 0
pot_unreg = 0 num_tor_error = 0
pot_unr_summary = '' 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(): def del_unregistered():
nonlocal dry_run, loglevel, del_tor, del_tor_cont, t_name, msg_up, tracker, t_cat, t_msg, t_status, torrent 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) attr["body"] = "\n".join(body)
self.config.send_notifications(attr) self.config.send_notifications(attr)
if self.config.args['rem_unregistered']: if cfg_rem_unregistered or cfg_tag_error:
separator("Removing Unregistered Torrents", space=False, border=False) 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 = [ unreg_msgs = [
'UNREGISTERED', 'UNREGISTERED',
'TORRENT NOT FOUND', 'TORRENT NOT FOUND',
@ -438,16 +478,39 @@ class Qbt:
'UNKNOWN TORRENT', 'UNKNOWN TORRENT',
'TRUMP', 'TRUMP',
'RETITLED', 'RETITLED',
'TRUNCATED' 'TRUNCATED',
'TORRENT IS NOT AUTHORIZED FOR USE ON THIS TRACKER'
] ]
ignore_msgs = [ 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: for torrent in self.torrentvalid:
check_tags = util.get_list(torrent.tags) check_tags = util.get_list(torrent.tags)
# Remove any potential unregistered torrents Tags that are no longer unreachable. # Remove any error torrents Tags that are no longer unreachable.
if 'issue' in check_tags: if tag_error in check_tags:
if not dry_run: torrent.remove_tags(tags='issue') 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: for torrent in self.torrentissue:
t_name = torrent.name t_name = torrent.name
t_cat = self.torrentinfo[t_name]['Category'] t_cat = self.torrentinfo[t_name]['Category']
@ -460,53 +523,41 @@ class Qbt:
if x.url.startswith('http'): if x.url.startswith('http'):
tracker = self.config.get_tags([x.url]) tracker = self.config.get_tags([x.url])
msg_up = x.msg.upper() msg_up = x.msg.upper()
# Tag any potential unregistered torrents # Tag any error torrents
if not any(m in msg_up for m in unreg_msgs) and x.status == 4 and 'issue' not in check_tags: if cfg_tag_error:
# Check for unregistered torrents using BHD API if the tracker is BHD if x.status == 4 and tag_error not in check_tags:
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): tag_tracker_error()
json = {"info_hash": torrent.hash} if cfg_rem_unregistered:
response = self.config.BeyondHD.search(json) # Tag any error torrents that are not unregistered
if response['total_results'] <= 1: if not list_in_text(msg_up, unreg_msgs) and x.status == 4 and tag_error not in check_tags:
del_unregistered() # Check for unregistered torrents using BHD API if the tracker is BHD
break if 'tracker.beyond-hd.me' in tracker['url'] and self.config.BeyondHD is not None and not list_in_text(msg_up, ignore_msgs):
pot_unr = '' json = {"info_hash": torrent.hash}
pot_unr += (util.insert_space(f'Torrent Name: {t_name}', 3)+'\n') response = self.config.BeyondHD.search(json)
pot_unr += (util.insert_space(f'Status: {msg_up}', 9)+'\n') if response['total_results'] <= 1:
pot_unr += (util.insert_space(f'Tracker: {tracker["url"]}', 8)+'\n') del_unregistered()
pot_unr += (util.insert_space("Added Tag: 'issue'", 6)+'\n') break
pot_unr_summary += pot_unr tag_tracker_error()
pot_unreg += 1 if list_in_text(msg_up, unreg_msgs) and x.status == 4:
attr = { del_unregistered()
"function": "potential_rem_unregistered", break
"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
except NotFound404Error: except NotFound404Error:
continue continue
except Exception as e: except Exception as e:
util.print_stacktrace() util.print_stacktrace()
self.config.notify(e, 'Remove Unregistered Torrents', False) self.config.notify(e, 'Remove Unregistered Torrents', False)
logger.error(f"Unknown Error: {e}") logger.error(f"Unknown Error: {e}")
if del_tor >= 1 or del_tor_cont >= 1: if cfg_rem_unregistered:
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 >= 1 or del_tor_cont >= 1:
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) 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)
else: 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)
print_line('No unregistered torrents found.', loglevel) else:
if (pot_unreg > 0): print_line('No unregistered torrents found.', loglevel)
separator(f"{pot_unreg} Potential Unregistered torrents found", space=False, border=False, loglevel=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)
print_multiline(pot_unr_summary.rstrip(), loglevel) if num_tor_error >= 1:
return del_tor, del_tor_cont, pot_unreg 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 # Function used to move any torrents from the cross seed directory to the correct save directory
def cross_seed(self): def cross_seed(self):

View file

@ -189,6 +189,19 @@ def add_dict_list(keys, value, dict_map):
dict_map[key] = [value] 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'): def print_line(lines, loglevel='INFO'):
logger.log(getattr(logging, loglevel.upper()), str(lines)) logger.log(getattr(logging, loglevel.upper()), str(lines))
return [str(lines)] return [str(lines)]
@ -295,7 +308,11 @@ def move_files(src, dest, mod=False):
dest_path = os.path.dirname(dest) dest_path = os.path.dirname(dest)
if os.path.isdir(dest_path) is False: if os.path.isdir(dest_path) is False:
os.makedirs(dest_path) 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: if mod is True:
modTime = time.time() modTime = time.time()
os.utime(dest, (modTime, modTime)) os.utime(dest, (modTime, modTime))
@ -306,7 +323,11 @@ def copy_files(src, dest):
dest_path = os.path.dirname(dest) dest_path = os.path.dirname(dest)
if os.path.isdir(dest_path) is False: if os.path.isdir(dest_path) is False:
os.makedirs(dest_path) 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 # Remove any empty directories after moving files

View file

@ -104,10 +104,11 @@ class Webhooks:
"torrents_categorized": stats["categorized"], "torrents_categorized": stats["categorized"],
"torrents_tagged": stats["tagged"], "torrents_tagged": stats["tagged"],
"remove_unregistered": stats["rem_unreg"], "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"], "orphaned_files_found": stats["orphaned"],
"torrents_tagged_no_hardlinks": stats["taggednoHL"], "torrents_tagged_no_hardlinks": stats["tagged_noHL"],
"torrents_untagged_no_hardlinks": stats["untagged"], "torrents_untagged_no_hardlinks": stats["untagged_noHL"],
"files_deleted_from_recyclebin": stats["recycle_emptied"] "files_deleted_from_recyclebin": stats["recycle_emptied"]
}) })

View file

@ -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("-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('-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('-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, 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') 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',) 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, 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)') 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('-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('-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, 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. \ 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) run = get_arg("QBT_RUN", args.run, arg_bool=True)
sch = get_arg("QBT_SCHEDULE", args.min) sch = get_arg("QBT_SCHEDULE", args.min)
startupDelay = get_arg("QBT_STARTUP_DELAY", args.startupDelay)
config_file = get_arg("QBT_CONFIG", args.configfile) config_file = get_arg("QBT_CONFIG", args.configfile)
log_file = get_arg("QBT_LOGFILE", args.logfile) log_file = get_arg("QBT_LOGFILE", args.logfile)
cross_seed = get_arg("QBT_CROSS_SEED", args.cross_seed, arg_bool=True) 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) 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) 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) 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) 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) 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) skip_recycle = get_arg("QBT_SKIP_RECYCLE", args.skip_recycle, arg_bool=True)
@ -99,6 +103,7 @@ else:
for v in [ for v in [
'run', 'run',
'sch', 'sch',
'startupDelay',
'config_file', 'config_file',
'log_file', 'log_file',
'cross_seed', 'cross_seed',
@ -106,6 +111,7 @@ for v in [
'cat_update', 'cat_update',
'tag_update', 'tag_update',
'rem_unregistered', 'rem_unregistered',
'tag_tracker_error',
'rem_orphaned', 'rem_orphaned',
'tag_nohardlinks', 'tag_nohardlinks',
'skip_recycle', 'skip_recycle',
@ -132,6 +138,13 @@ except ValueError:
print(f"Schedule Error: Schedule is not a number. Current value is set to '{sch}'") print(f"Schedule Error: Schedule is not a number. Current value is set to '{sch}'")
sys.exit(0) 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') logger = logging.getLogger('qBit Manage')
logging.DRYRUN = 25 logging.DRYRUN = 25
logging.addLevelName(logging.DRYRUN, 'DRYRUN') logging.addLevelName(logging.DRYRUN, 'DRYRUN')
@ -188,6 +201,10 @@ def start():
start_type = "" start_type = ""
util.separator(f"Starting {start_type}Run") util.separator(f"Starting {start_type}Run")
cfg = None cfg = None
body = ''
run_time = ''
end_time = None
next_run = None
global stats global stats
stats = { stats = {
"added": 0, "added": 0,
@ -198,17 +215,35 @@ def start():
"orphaned": 0, "orphaned": 0,
"recycle_emptied": 0, "recycle_emptied": 0,
"tagged": 0, "tagged": 0,
"untagged": 0,
"categorized": 0, "categorized": 0,
"rem_unreg": 0, "rem_unreg": 0,
"pot_unreg": 0, "tagged_tracker_error": 0,
"taggednoHL": 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: try:
cfg = Config(default_dir, args) cfg = Config(default_dir, args)
except Exception as e: except Exception as e:
util.print_stacktrace() if 'Qbittorrent Error' in e.args[0]:
util.print_multiline(e, 'CRITICAL') 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: if cfg:
# Set Category # Set Category
@ -220,11 +255,13 @@ def start():
stats["tagged"] += num_tagged stats["tagged"] += num_tagged
# Remove Unregistered Torrents # 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["rem_unreg"] += (num_deleted + num_deleted_contents)
stats["deleted"] += num_deleted stats["deleted"] += num_deleted
stats["deleted_contents"] += num_deleted_contents 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 # Set Cross Seed
num_added, num_tagged = cfg.qbt.cross_seed() num_added, num_tagged = cfg.qbt.cross_seed()
@ -239,8 +276,8 @@ def start():
# Tag NoHardLinks # Tag NoHardLinks
num_tagged, num_untagged, num_deleted, num_deleted_contents = cfg.qbt.tag_nohardlinks() num_tagged, num_untagged, num_deleted, num_deleted_contents = cfg.qbt.tag_nohardlinks()
stats["tagged"] += num_tagged stats["tagged"] += num_tagged
stats["taggednoHL"] += num_tagged stats["tagged_noHL"] += num_tagged
stats["untagged"] += num_untagged stats["untagged_noHL"] += num_untagged
stats["deleted"] += num_deleted stats["deleted"] += num_deleted
stats["deleted_contents"] += num_deleted_contents stats["deleted_contents"] += num_deleted_contents
@ -248,31 +285,27 @@ def start():
num_orphaned = cfg.qbt.rem_orphaned() num_orphaned = cfg.qbt.rem_orphaned()
stats["orphaned"] += num_orphaned stats["orphaned"] += num_orphaned
# mpty RecycleBin # Empty RecycleBin
recycle_emptied = cfg.empty_recycle() recycle_emptied = cfg.empty_recycle()
stats["recycle_emptied"] += recycle_emptied stats["recycle_emptied"] += recycle_emptied
if stats["categorized"] > 0: stats_summary.append(f"Total Torrents Categorized: {stats['categorized']}") 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["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["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["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["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["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"] > 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["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["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["tagged_noHL"] > 0: stats_summary.append(f"Total noHL Torrents Tagged: {stats['tagged_noHL']}")
if stats["untagged"] > 0: stats_summary.append(f"Total noHL Torrents untagged: {stats['untagged']}") 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']}") if stats["recycle_emptied"] > 0: stats_summary.append(f"Total Files Deleted from Recycle Bin: {stats['recycle_emptied']}")
end_time = datetime.now() FinishedRun()
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]
if cfg: if cfg:
try: try:
cfg.Webhooks.end_time_hooks(start_time, end_time, run_time, next_run, stats, body) 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') util.separator(loglevel='DEBUG')
logger.debug(f" --run (QBT_RUN): {run}") logger.debug(f" --run (QBT_RUN): {run}")
logger.debug(f" --schedule (QBT_SCHEDULE): {sch}") 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" --config-file (QBT_CONFIG): {config_file}")
logger.debug(f" --log-file (QBT_LOGFILE): {log_file}") logger.debug(f" --log-file (QBT_LOGFILE): {log_file}")
logger.debug(f" --cross-seed (QBT_CROSS_SEED): {cross_seed}") 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" --cat-update (QBT_CAT_UPDATE): {cat_update}")
logger.debug(f" --tag-update (QBT_TAG_UPDATE): {tag_update}") logger.debug(f" --tag-update (QBT_TAG_UPDATE): {tag_update}")
logger.debug(f" --rem-unregistered (QBT_REM_UNREGISTERED): {rem_unregistered}") 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" --rem-orphaned (QBT_REM_ORPHANED): {rem_orphaned}")
logger.debug(f" --tag-nohardlinks (QBT_TAG_NOHARDLINKS): {tag_nohardlinks}") logger.debug(f" --tag-nohardlinks (QBT_TAG_NOHARDLINKS): {tag_nohardlinks}")
logger.debug(f" --skip-recycle (QBT_SKIP_RECYCLE): {skip_recycle}") logger.debug(f" --skip-recycle (QBT_SKIP_RECYCLE): {skip_recycle}")
@ -354,6 +389,9 @@ if __name__ == '__main__':
schedule.every(sch).minutes.do(start) schedule.every(sch).minutes.do(start)
time_str, _ = calc_next_run(sch) time_str, _ = calc_next_run(sch)
logger.info(f" Scheduled Mode: Running every {time_str}.") 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() start()
while not killer.kill_now: while not killer.kill_now:
schedule.run_pending() schedule.run_pending()

View file

@ -1,5 +1,5 @@
ruamel.yaml==0.17.20 ruamel.yaml==0.17.20
qbittorrent-api==2021.12.26 qbittorrent-api>=2022.1.27
schedule==1.1.0 schedule==1.1.0
retrying==1.3.3 retrying==1.3.3
alive_progress==2.1.0 alive_progress==2.1.0