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}")