From 3cc81bd74d00948e71b567ca98029fc16c278dfb Mon Sep 17 00:00:00 2001 From: bobokun Date: Sat, 8 Jan 2022 21:35:24 -0500 Subject: [PATCH 01/26] Bug Fix: Force ATM causing issue with category --- modules/qbittorrent.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index c365d73..872797e 100644 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -80,7 +80,7 @@ class Qbt: is_complete = False msg = None status = None - if torrent.auto_tmm is False and settings['force_auto_tmm'] and not dry_run: + 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 @@ -152,6 +152,8 @@ class Qbt: 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) From 660a421290a1656711de99fb9bebc698a26db526 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sat, 8 Jan 2022 21:59:00 -0500 Subject: [PATCH 02/26] 3.1.6 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 97ceee1..8a4b275 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.5 \ No newline at end of file +3.1.6 \ No newline at end of file From cc0554da129a84d1932b4bbe361427933390a969 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sun, 9 Jan 2022 10:38:20 -0500 Subject: [PATCH 03/26] Remove noHL error when no torrents in category --- modules/qbittorrent.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index 872797e..d596db5 100644 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -272,10 +272,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')]) From 14b14ae999a9f4e0422fd2b6ff1de567dd620048 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sun, 9 Jan 2022 15:29:12 -0500 Subject: [PATCH 04/26] Fix unreg torrent incorrectly marked as working --- modules/qbittorrent.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index d596db5..3c3316d 100644 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -80,6 +80,8 @@ class Qbt: is_complete = False msg = None status = None + 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: @@ -106,21 +108,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 all(x not in msg for x in 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 = { From 3066c323bfce4b69122c07155d5b377c26e25b52 Mon Sep 17 00:00:00 2001 From: bobokun Date: Mon, 10 Jan 2022 08:00:18 -0500 Subject: [PATCH 05/26] bug fix: don't make recyclebin if not enabled --- modules/config.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/config.py b/modules/config.py index aaf5ef1..ae5ecc6 100644 --- a/modules/config.py +++ b/modules/config.py @@ -192,7 +192,10 @@ 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']: + 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) + else: + self.recycle_dir = self.util.check_for_attribute(self.data, "recycle_bin", parent="directory", var_type="path", default=os.path.join(self.remote_dir, '.RecycleBin')) if self.recyclebin['enabled'] and self.recyclebin['save_torrents']: self.torrents_dir = self.util.check_for_attribute(self.data, "torrents_dir", parent="directory", var_type="path") if not any(File.endswith(".torrent") for File in os.listdir(self.torrents_dir)): From 8ae68ccc086fc84b93c82b516afc26363ce34956 Mon Sep 17 00:00:00 2001 From: bobokun Date: Mon, 10 Jan 2022 10:16:24 -0500 Subject: [PATCH 06/26] Bug Fix to not creating recyclebin when disabled --- modules/config.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/config.py b/modules/config.py index ae5ecc6..f8a9f80 100644 --- a/modules/config.py +++ b/modules/config.py @@ -195,7 +195,7 @@ class Config: if self.recyclebin['enabled']: 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) else: - self.recycle_dir = self.util.check_for_attribute(self.data, "recycle_bin", parent="directory", var_type="path", default=os.path.join(self.remote_dir, '.RecycleBin')) + self.recycle_dir = 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)): @@ -210,10 +210,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 From d9a6fcfab9f293b20ef3e1547da6c2e5d401f2f9 Mon Sep 17 00:00:00 2001 From: bobokun Date: Mon, 10 Jan 2022 11:54:18 -0500 Subject: [PATCH 07/26] Fix Bug when creating RecycleBin path --- modules/config.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/config.py b/modules/config.py index f8a9f80..b463ab8 100644 --- a/modules/config.py +++ b/modules/config.py @@ -193,7 +193,11 @@ class Config: else: self.cross_seed_dir = self.util.check_for_attribute(self.data, "cross_seed", parent="directory", default_is_none=True) if self.recyclebin['enabled']: - 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 "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') + 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']: From 393538acf52d6081ea519eefcdb387734c1dd975 Mon Sep 17 00:00:00 2001 From: bobokun Date: Mon, 10 Jan 2022 11:59:30 -0500 Subject: [PATCH 08/26] Added Remove Unregistered logic (fixes #90) --- modules/qbittorrent.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index 3c3316d..b040ac5 100644 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -448,7 +448,8 @@ 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' From ac78bd5025d853c20f10ea38feaed1038e0c9693 Mon Sep 17 00:00:00 2001 From: bobokun Date: Mon, 10 Jan 2022 15:37:12 -0500 Subject: [PATCH 09/26] bug_fix: creating empty recyclebin in root dir #77 --- modules/config.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/config.py b/modules/config.py index b463ab8..0ab939e 100644 --- a/modules/config.py +++ b/modules/config.py @@ -197,7 +197,10 @@ class Config: 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') - self.recycle_dir = self.util.check_for_attribute(self.data, "recycle_bin", parent="directory", var_type="path", default=default_recycle, make_dirs=True) + 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']: From eefdd746c0449cefb3201f73f98ffec97b3b346c Mon Sep 17 00:00:00 2001 From: bobokun Date: Mon, 10 Jan 2022 15:38:09 -0500 Subject: [PATCH 10/26] added default sleep time initial run 30s --- qbit_manage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qbit_manage.py b/qbit_manage.py index 2d4bf04..86403b3 100644 --- a/qbit_manage.py +++ b/qbit_manage.py @@ -354,6 +354,7 @@ if __name__ == '__main__': schedule.every(sch).minutes.do(start) time_str, _ = calc_next_run(sch) logger.info(f" Scheduled Mode: Running every {time_str}.") + time.sleep(30) start() while not killer.kill_now: schedule.run_pending() From 926c8803e0eb152e462d762f823f63fbcc1706f9 Mon Sep 17 00:00:00 2001 From: bobokun Date: Mon, 10 Jan 2022 16:21:13 -0500 Subject: [PATCH 11/26] Added StartupDelay Env Variable --- qbit_manage.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/qbit_manage.py b/qbit_manage.py index 86403b3..5b16798 100644 --- a/qbit_manage.py +++ b/qbit_manage.py @@ -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',) @@ -69,6 +70,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) @@ -99,6 +101,7 @@ else: for v in [ 'run', 'sch', + 'startupDelay', 'config_file', 'log_file', 'cross_seed', @@ -132,6 +135,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') @@ -329,6 +339,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}") @@ -354,7 +365,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}.") - time.sleep(30) + 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() From 49b006f7cee8f08da12f3ece4270d6a6d21e919a Mon Sep 17 00:00:00 2001 From: bobokun Date: Mon, 10 Jan 2022 17:33:06 -0500 Subject: [PATCH 12/26] Bug Fix: Continue to run on qbit connection error --- qbit_manage.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/qbit_manage.py b/qbit_manage.py index 5b16798..8e2ff18 100644 --- a/qbit_manage.py +++ b/qbit_manage.py @@ -198,6 +198,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, @@ -214,11 +218,27 @@ def start(): "pot_unreg": 0, "taggednoHL": 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') + FinishedRun() + return None + else: + util.print_stacktrace() + util.print_multiline(e, 'CRITICAL') if cfg: # Set Category @@ -258,7 +278,7 @@ 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 @@ -276,13 +296,8 @@ def start(): if stats["untagged"] > 0: stats_summary.append(f"Total noHL Torrents untagged: {stats['untagged']}") 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) From f1bca57039121ca4c15a5be39e356e2256921c03 Mon Sep 17 00:00:00 2001 From: bobokun Date: Mon, 10 Jan 2022 17:38:25 -0500 Subject: [PATCH 13/26] Add exit message --- qbit_manage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qbit_manage.py b/qbit_manage.py index 8e2ff18..4063c58 100644 --- a/qbit_manage.py +++ b/qbit_manage.py @@ -234,6 +234,7 @@ def start(): except Exception as e: if 'Qbittorrent Error' in e.args[0]: util.print_multiline(e, 'CRITICAL') + util.print_line('Exiting scheduled Run.', 'CRITICAL') FinishedRun() return None else: From a962207e9b6ca60f77e85c101567da6eda0b7b96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Jan 2022 00:40:48 +0000 Subject: [PATCH 14/26] Bump qbittorrent-api from 2021.12.26 to 2022.1.27 Bumps [qbittorrent-api](https://github.com/rmartin16/qbittorrent-api) from 2021.12.26 to 2022.1.27. - [Release notes](https://github.com/rmartin16/qbittorrent-api/releases) - [Changelog](https://github.com/rmartin16/qbittorrent-api/blob/master/CHANGELOG.txt) - [Commits](https://github.com/rmartin16/qbittorrent-api/compare/v2021.12.26...v2022.1.27) --- updated-dependencies: - dependency-name: qbittorrent-api dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 43281a7..341ef64 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 From 8d9ec4558e692f36ead83fa3a032142a3c50a7f7 Mon Sep 17 00:00:00 2001 From: bobokun Date: Mon, 10 Jan 2022 20:05:04 -0500 Subject: [PATCH 15/26] Update version support check using qbittorrent api --- modules/qbittorrent.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index b040ac5..4710c09 100644 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -1,5 +1,5 @@ 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 datetime import timedelta @@ -11,7 +11,6 @@ logger = logging.getLogger("qBit Manage") class Qbt: - SUPPORTED_VERSION = 'v4.3' def __init__(self, config, params): self.config = config @@ -23,12 +22,14 @@ class Qbt: try: self.client = Client(host=self.host, username=self.username, password=self.password) 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}" self.config.notify(e, "Qbittorrent") print_line(e, 'CRITICAL') sys.exit(0) From 5a30cd54ba587ef631afc3bdab7a50a7ab55f5b5 Mon Sep 17 00:00:00 2001 From: bobokun Date: Mon, 10 Jan 2022 20:05:31 -0500 Subject: [PATCH 16/26] 3.2.0 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 8a4b275..a4f52a5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.6 \ No newline at end of file +3.2.0 \ No newline at end of file From 938656fb4fad9882c80398b7ab5ea36370dadb68 Mon Sep 17 00:00:00 2001 From: bobokun Date: Mon, 10 Jan 2022 22:00:43 -0500 Subject: [PATCH 17/26] Adds [FR] #91 [FR]: Tag torrents which have a tracker issue. #91 --- config/config.yml.sample | 16 ++-- modules/config.py | 7 +- modules/qbittorrent.py | 195 +++++++++++++++++++++++---------------- modules/webhooks.py | 7 +- qbit_manage.py | 28 ++++-- 5 files changed, 152 insertions(+), 101 deletions(-) diff --git a/config/config.yml.sample b/config/config.yml.sample index 6731ae8..08837f7 100644 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -10,16 +10,17 @@ 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. directory: # Do not remove these - # Cross-seed var: # Output directory of cross-seed - # root_dir var: # Root downloads directory used to check for orphaned files, noHL, and RecycleBin. - # remote_dir var: # Path of docker host mapping of root_dir. + # Cross-seed var: # Output directory of cross-seed + # root_dir var: # Root downloads directory used to check for orphaned files, noHL, and RecycleBin. + # remote_dir var: # 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 - # recycle_bin var: # Path of the RecycleBin folder. Default location is set to remote_dir/.RecycleBin - # torrents_dir var: # Path of the your qbittorrent torrents directory. Required for `save_torrents` attribute in recyclebin + # recycle_bin var: # Path of the RecycleBin folder. Default location is set to remote_dir/.RecycleBin + # torrents_dir var: # Path of the your qbittorrent torrents directory. Required for `save_torrents` attribute in recyclebin cross_seed: "/your/path/here/" root_dir: "/data/torrents/" @@ -29,7 +30,7 @@ directory: # Category & Path Parameters cat: - # : # Path of your save directory. + # : # Path of your save directory. movies: "/data/torrents/Movies" tv: "/data/torrents/TV" @@ -186,6 +187,7 @@ webhooks: cat_update: apprise tag_update: notifiarr rem_unregistered: notifiarr + tag_tracker_error: notifiarr rem_orphaned: notifiarr tag_nohardlinks: notifiarr empty_recyclebin: notifiarr diff --git a/modules/config.py b/modules/config.py index 0ab939e..b28c766 100644 --- a/modules/config.py +++ b/modules/config.py @@ -79,6 +79,7 @@ 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_function = { @@ -87,9 +88,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 +100,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: diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index 4710c09..51ae85b 100644 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -20,7 +20,7 @@ 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() @@ -141,7 +141,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) @@ -154,35 +154,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) - 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 + 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: @@ -193,7 +193,8 @@ 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'] + tag_error = self.config.settings['tracker_error_tag'] + ignore_tags = ['noHL', tag_error, 'cross-seed'] if self.config.args['tag_update']: separator("Updating Tags", space=False, border=False) for torrent in self.torrent_list: @@ -400,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 @@ -438,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', @@ -457,9 +486,27 @@ class Qbt: ] 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'] @@ -472,53 +519,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 any(m in msg_up for m in 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 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 + tag_tracker_error() + if any(m in msg_up for m in 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): diff --git a/modules/webhooks.py b/modules/webhooks.py index d667cf4..1a7ffdc 100644 --- a/modules/webhooks.py +++ b/modules/webhooks.py @@ -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"] }) diff --git a/qbit_manage.py b/qbit_manage.py index 4063c58..42bd942 100644 --- a/qbit_manage.py +++ b/qbit_manage.py @@ -35,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. \ @@ -78,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) @@ -109,6 +111,7 @@ for v in [ 'cat_update', 'tag_update', 'rem_unregistered', + 'tag_tracker_error', 'rem_orphaned', 'tag_nohardlinks', 'skip_recycle', @@ -212,11 +215,12 @@ 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(): @@ -251,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() @@ -270,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 @@ -286,15 +292,16 @@ def start(): 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']}") FinishedRun() @@ -363,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}") From add5ed05b2862b38d9504997dbf6908ba0ca3b2f Mon Sep 17 00:00:00 2001 From: bobokun Date: Wed, 12 Jan 2022 08:36:01 -0500 Subject: [PATCH 18/26] add additional ignore msg to BHD integration --- modules/qbittorrent.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index 51ae85b..5af1618 100644 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -482,7 +482,10 @@ class Qbt: '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' ] for torrent in self.torrentvalid: check_tags = util.get_list(torrent.tags) From 2e032834651ae8c9cb8a44441680cfea8cd33e97 Mon Sep 17 00:00:00 2001 From: bobokun Date: Wed, 12 Jan 2022 16:15:43 -0500 Subject: [PATCH 19/26] bug fix: remove unregistered torrents better handling of unregistered torrents by matching full word --- .flake8 | 1 + modules/qbittorrent.py | 23 ++++++++++++----------- modules/util.py | 15 ++++++++++++++- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/.flake8 b/.flake8 index 6490f62..3b7e4bf 100644 --- a/.flake8 +++ b/.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 \ No newline at end of file diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index 5af1618..f12fd5a 100644 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -1,7 +1,7 @@ import logging, os, sys 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 @@ -113,12 +113,12 @@ class Qbt: if x.url.startswith('http'): status = x.status msg = x.msg.upper() - exception = ["DOWN", "UNREACHABLE", "BAD GATEWAY", "TRACKER UNAVAILABLE"] + exception = set(["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 all(x not in msg for x in exception): + if x.status == 4 and not list_in_text(msg, exception): issue['potential'] = True issue['msg'] = msg issue['status'] = status @@ -469,7 +469,7 @@ class Qbt: 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 = [ + unreg_msgs = set([ 'UNREGISTERED', 'TORRENT NOT FOUND', 'TORRENT IS NOT FOUND', @@ -480,13 +480,14 @@ class Qbt: 'RETITLED', 'TRUNCATED', 'TORRENT IS NOT AUTHORIZED FOR USE ON THIS TRACKER' - ] - ignore_msgs = [ + ]) + ignore_msgs = set([ 'YOU HAVE REACHED THE CLIENT LIMIT FOR THIS TORRENT', 'MISSING PASSKEY', 'MISSING INFO_HASH', - 'PASSKEY IS INVALID' - ] + 'PASSKEY IS INVALID', + 'INVALID PASSKEY' + ]) for torrent in self.torrentvalid: check_tags = util.get_list(torrent.tags) # Remove any error torrents Tags that are no longer unreachable. @@ -528,16 +529,16 @@ class Qbt: tag_tracker_error() if cfg_rem_unregistered: # Tag any error torrents that are not unregistered - if not any(m in msg_up for m in unreg_msgs) and x.status == 4 and tag_error not in check_tags: + 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 all(x not in msg_up for x in ignore_msgs): + 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 any(m in msg_up for m in unreg_msgs) and x.status == 4: + if list_in_text(msg_up, unreg_msgs) and x.status == 4: del_unregistered() break except NotFound404Error: diff --git a/modules/util.py b/modules/util.py index ac93bfc..68be063 100644 --- a/modules/util.py +++ b/modules/util.py @@ -1,4 +1,4 @@ -import logging, os, shutil, traceback, time, signal, json +import logging, os, shutil, traceback, time, signal, json, re from logging.handlers import RotatingFileHandler from ruamel import yaml from pathlib import Path @@ -189,6 +189,19 @@ def add_dict_list(keys, value, dict_map): dict_map[key] = [value] +def list_in_text(text, search_list, all=False): + length = len(search_list) + num = 0 + pattern = re.compile(r'\b' + + r'\b|\b'.join(re.escape(x) for x in search_list) + + r'\b') + for t in set(pattern.findall(text)): + num += 1 + if not all: return True + if(num == length and all): return True + return False + + def print_line(lines, loglevel='INFO'): logger.log(getattr(logging, loglevel.upper()), str(lines)) return [str(lines)] From f2db556f9063593ce16e1ab4def21efa669885f2 Mon Sep 17 00:00:00 2001 From: bobokun Date: Wed, 12 Jan 2022 18:48:54 -0500 Subject: [PATCH 20/26] optimize performance --- modules/qbittorrent.py | 10 +++++----- modules/util.py | 22 +++++++++++----------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index f12fd5a..36abce3 100644 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -113,7 +113,7 @@ class Qbt: if x.url.startswith('http'): status = x.status msg = x.msg.upper() - exception = set(["DOWN", "UNREACHABLE", "BAD GATEWAY", "TRACKER UNAVAILABLE"]) + exception = ["DOWN", "UNREACHABLE", "BAD GATEWAY", "TRACKER UNAVAILABLE"] if x.status == 2: working_tracker = True break @@ -469,7 +469,7 @@ class Qbt: 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 = set([ + unreg_msgs = [ 'UNREGISTERED', 'TORRENT NOT FOUND', 'TORRENT IS NOT FOUND', @@ -480,14 +480,14 @@ class Qbt: 'RETITLED', 'TRUNCATED', 'TORRENT IS NOT AUTHORIZED FOR USE ON THIS TRACKER' - ]) - ignore_msgs = set([ + ] + ignore_msgs = [ '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 error torrents Tags that are no longer unreachable. diff --git a/modules/util.py b/modules/util.py index 68be063..183b0a6 100644 --- a/modules/util.py +++ b/modules/util.py @@ -1,4 +1,4 @@ -import logging, os, shutil, traceback, time, signal, json, re +import logging, os, shutil, traceback, time, signal, json from logging.handlers import RotatingFileHandler from ruamel import yaml from pathlib import Path @@ -189,16 +189,16 @@ def add_dict_list(keys, value, dict_map): dict_map[key] = [value] -def list_in_text(text, search_list, all=False): - length = len(search_list) - num = 0 - pattern = re.compile(r'\b' - + r'\b|\b'.join(re.escape(x) for x in search_list) - + r'\b') - for t in set(pattern.findall(text)): - num += 1 - if not all: return True - if(num == length and all): return True +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 From 102a1e083bb1e0a321f5f454033a6e146265200e Mon Sep 17 00:00:00 2001 From: bobokun Date: Thu, 13 Jan 2022 14:39:40 -0500 Subject: [PATCH 21/26] Adds ability to have update multiple tags #95 [FR]: Tag Torrents with Existing Tags #95 --- modules/config.py | 20 +++++++++++--------- modules/qbittorrent.py | 6 +++--- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/modules/config.py b/modules/config.py index b28c766..e443b35 100644 --- a/modules/config.py +++ b/modules/config.py @@ -271,17 +271,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, @@ -292,11 +293,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) diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index 36abce3..2796bbf 100644 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -202,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 @@ -215,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"], From 8907abbf45792a74de81e9cc1d8d4d6492ac5b58 Mon Sep 17 00:00:00 2001 From: bobokun Date: Thu, 13 Jan 2022 15:00:05 -0500 Subject: [PATCH 22/26] Adds ignoreTags_OnUpdate in config settings #95 [FR]: Tag Torrents with Existing Tags #95 --- config/config.yml.sample | 14 ++++++++++---- modules/config.py | 2 ++ modules/qbittorrent.py | 3 +-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/config/config.yml.sample b/config/config.yml.sample index 08837f7..c53be76 100644 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -12,7 +12,10 @@ qbt: settings: 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: # Will ignore a list of tags when running the update tag function + - noHL + - issue + - cross-seed directory: # Do not remove these # Cross-seed var: # Output directory of cross-seed @@ -37,7 +40,7 @@ cat: # Tag Parameters tracker: # : # This is the keyword in the tracker url - # Set tag name + # Set tag name. Can be a list of tags or a single tag # tag: # 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 @@ -51,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 diff --git a/modules/config.py b/modules/config.py index e443b35..e29c635 100644 --- a/modules/config.py +++ b/modules/config.py @@ -81,6 +81,8 @@ class Config: "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, diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index 2796bbf..3a417ff 100644 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -193,8 +193,7 @@ class Qbt: dry_run = self.config.args['dry_run'] loglevel = 'DRYRUN' if dry_run else 'INFO' num_tags = 0 - tag_error = self.config.settings['tracker_error_tag'] - ignore_tags = ['noHL', tag_error, '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: From ddc1431c0b1eaca8cd932043663d3c1e8f6fe139 Mon Sep 17 00:00:00 2001 From: bobokun Date: Thu, 13 Jan 2022 19:06:56 -0500 Subject: [PATCH 23/26] update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 341ef64..533d8f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ ruamel.yaml==0.17.20 -qbittorrent-api==2022.1.27 +qbittorrent-api>=2022.1.27 schedule==1.1.0 retrying==1.3.3 alive_progress==2.1.0 From 6ed936cf7a130446e7bc284cba3f01d37c091110 Mon Sep 17 00:00:00 2001 From: bobokun Date: Fri, 14 Jan 2022 11:37:33 -0500 Subject: [PATCH 24/26] better ignoreTags_OnUpdate description --- config/config.yml.sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.yml.sample b/config/config.yml.sample index c53be76..3094d50 100644 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -12,7 +12,7 @@ qbt: settings: 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: # Will ignore a list of tags when running the update tag function + 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 From 52dcc9370dc90695216130659aa832a139eafb74 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sat, 15 Jan 2022 08:18:52 -0500 Subject: [PATCH 25/26] better error handling moving files --- modules/util.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/modules/util.py b/modules/util.py index 183b0a6..76c298e 100644 --- a/modules/util.py +++ b/modules/util.py @@ -308,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)) @@ -319,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 From 5fed8cd0e8b2458a05174bb754382b7676485172 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sat, 15 Jan 2022 11:33:08 -0500 Subject: [PATCH 26/26] better description of error #97 QBittorrent Version #97 --- modules/qbittorrent.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index 3a417ff..04a4312 100644 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -29,9 +29,10 @@ class Qbt: logger.debug(f'qBittorrent Web API: {self.client.app.web_api_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}" + 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: