diff --git a/modules/__init__.py b/modules/__init__.py new file mode 100644 index 0000000..78f14cd --- /dev/null +++ b/modules/__init__.py @@ -0,0 +1,20 @@ +import os + +# Define an empty version_info tuple +__version_info__ = () + +# Get the path to the project directory +project_dir = os.path.dirname(os.path.abspath(__file__)) + +# Get the path to the VERSION file +version_file_path = os.path.join(project_dir, "..", "VERSION") + +# Read the version from the file +with open(version_file_path) as f: + version_str = f.read().strip() + +# Convert the version string to a tuple of integers +__version_info__ = tuple(map(int, version_str.split("."))) + +# Define the version string using the version_info tuple +__version__ = ".".join(str(i) for i in __version_info__) diff --git a/modules/config.py b/modules/config.py index 5d51c0f..8092b99 100755 --- a/modules/config.py +++ b/modules/config.py @@ -474,240 +474,6 @@ class Config: self.notify(e, "Config") raise Failed(e) - # Get tags from config file based on keyword - def get_tags(self, trackers): - urls = [x.url for x in trackers if x.url.startswith("http")] - tracker = {} - tracker["tag"] = None - tracker["max_ratio"] = None - tracker["min_seeding_time"] = None - tracker["max_seeding_time"] = None - tracker["limit_upload_speed"] = None - tracker["notifiarr"] = None - tracker["url"] = None - tracker_other_tag = self.util.check_for_attribute( - self.data, "tag", parent="tracker", subparent="other", default_is_none=True, var_type="list", save=False - ) - try: - tracker["url"] = util.trunc_val(urls[0], os.sep) - except IndexError as e: - tracker["url"] = None - if not urls: - urls = [] - if not tracker_other_tag: - tracker_other_tag = ["other"] - tracker["url"] = "No http URL found" - else: - logger.debug(f"Tracker Url:{urls}") - logger.debug(e) - if "tracker" in self.data and self.data["tracker"] is not None: - tag_values = self.data["tracker"] - for tag_url, tag_details in tag_values.items(): - for url in urls: - if tag_url in url: - if tracker["url"] is None: - default_tag = tracker_other_tag - else: - try: - tracker["url"] = util.trunc_val(url, os.sep) - default_tag = tracker["url"].split(os.sep)[2].split(":")[0] - except IndexError as e: - logger.debug(f"Tracker Url:{url}") - logger.debug(e) - # Tracker Format 1 deprecated. - if isinstance(tag_details, str): - e = ( - "Config Error: Tracker format invalid. Please see config.yml.sample for correct format and fix " - f"`{tag_details}` in the Tracker section of the config." - ) - self.notify(e, "Config") - raise Failed(e) - # Using new Format - else: - 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"]] - is_max_ratio_defined = self.data["tracker"].get("max_ratio") - is_max_seeding_time_defined = self.data["tracker"].get("max_seeding_time") - if is_max_ratio_defined or is_max_seeding_time_defined: - tracker["max_ratio"] = self.util.check_for_attribute( - self.data, - "max_ratio", - parent="tracker", - subparent=tag_url, - var_type="float", - min_int=-2, - do_print=False, - default=-1, - save=False, - ) - tracker["max_seeding_time"] = self.util.check_for_attribute( - self.data, - "max_seeding_time", - parent="tracker", - subparent=tag_url, - var_type="int", - min_int=-2, - do_print=False, - default=-1, - save=False, - ) - else: - tracker["max_ratio"] = self.util.check_for_attribute( - self.data, - "max_ratio", - parent="tracker", - subparent=tag_url, - var_type="float", - min_int=-2, - do_print=False, - default_is_none=True, - save=False, - ) - tracker["max_seeding_time"] = self.util.check_for_attribute( - self.data, - "max_seeding_time", - parent="tracker", - subparent=tag_url, - var_type="int", - min_int=-2, - do_print=False, - default_is_none=True, - save=False, - ) - tracker["min_seeding_time"] = self.util.check_for_attribute( - self.data, - "min_seeding_time", - parent="tracker", - subparent=tag_url, - var_type="int", - min_int=0, - do_print=False, - default=0, - save=False, - ) - tracker["limit_upload_speed"] = self.util.check_for_attribute( - self.data, - "limit_upload_speed", - parent="tracker", - subparent=tag_url, - var_type="int", - min_int=-1, - do_print=False, - default=0, - save=False, - ) - tracker["notifiarr"] = self.util.check_for_attribute( - self.data, - "notifiarr", - parent="tracker", - subparent=tag_url, - default_is_none=True, - do_print=False, - save=False, - ) - return tracker - if tracker_other_tag: - tracker["tag"] = tracker_other_tag - tracker["max_ratio"] = self.util.check_for_attribute( - self.data, - "max_ratio", - parent="tracker", - subparent="other", - var_type="float", - min_int=-2, - do_print=False, - default=-1, - save=False, - ) - tracker["min_seeding_time"] = self.util.check_for_attribute( - self.data, - "min_seeding_time", - parent="tracker", - subparent="other", - var_type="int", - min_int=0, - do_print=False, - default=-1, - save=False, - ) - tracker["max_seeding_time"] = self.util.check_for_attribute( - self.data, - "max_seeding_time", - parent="tracker", - subparent="other", - var_type="int", - min_int=-2, - do_print=False, - default=-1, - save=False, - ) - tracker["limit_upload_speed"] = self.util.check_for_attribute( - self.data, - "limit_upload_speed", - parent="tracker", - subparent="other", - var_type="int", - min_int=-1, - do_print=False, - default=0, - save=False, - ) - tracker["notifiarr"] = self.util.check_for_attribute( - self.data, - "notifiarr", - parent="tracker", - subparent="other", - default_is_none=True, - do_print=False, - save=False, - ) - return tracker - if tracker["url"]: - logger.trace(f"tracker url: {tracker['url']}") - if tracker_other_tag: - default_tag = tracker_other_tag - else: - default_tag = tracker["url"].split(os.sep)[2].split(":")[0] - 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] - except Exception: - 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) - return tracker - - # Get category from config file based on path provided - def get_category(self, path): - category = "" - path = os.path.join(path, "") - if "cat" in self.data and self.data["cat"] is not None: - cat_path = self.data["cat"] - for cat, save_path in cat_path.items(): - if os.path.join(save_path, "") == path: - category = cat - break - - if not category: - default_cat = path.split(os.sep)[-2] - category = str(default_cat) - self.util.check_for_attribute(self.data, default_cat, parent="cat", default=path) - self.data["cat"][str(default_cat)] = path - e = f"No categories matched for the save path {path}. Check your config.yml file. - Setting category to {default_cat}" - self.notify(e, "Category", False) - logger.warning(e) - return category - # Empty old files from recycle bin or orphaned def cleanup_dirs(self, location): num_del = 0 diff --git a/modules/core/__init__.py b/modules/core/__init__.py new file mode 100644 index 0000000..a285905 --- /dev/null +++ b/modules/core/__init__.py @@ -0,0 +1,3 @@ +""" + modules.core contains all the core functions of qbit_manage such as updating categories/tags etc.. +""" diff --git a/modules/core/category.py b/modules/core/category.py new file mode 100644 index 0000000..e57ecce --- /dev/null +++ b/modules/core/category.py @@ -0,0 +1,78 @@ +from qbittorrentapi import Conflict409Error + +from modules import util + +logger = util.logger + + +class Category: + def __init__(self, qbit_manager): + self.qbt = qbit_manager + self.config = qbit_manager.config + self.client = qbit_manager.client + self.stats = 0 + + self.category() + + def category(self): + """Update category for torrents that don't have any category defined and returns total number categories updated""" + self.stats = 0 + + logger.separator("Updating Categories", space=False, border=False) + torrent_list = self.qbt.get_torrents({"category": "", "status_filter": "completed"}) + for torrent in torrent_list: + new_cat = self.qbt.get_category(torrent.save_path) + self.update_cat(torrent, new_cat, False) + + # Change categories + if self.config.cat_change: + for old_cat in self.config.cat_change: + torrent_list = self.qbt.get_torrents({"category": old_cat, "status_filter": "completed"}) + for torrent in torrent_list: + new_cat = self.config.cat_change[old_cat] + self.update_cat(torrent, new_cat, True) + + if self.stats >= 1: + logger.print_line( + f"{'Did not update' if self.config.dry_run else 'Updated'} {self.stats} new categories.", self.config.loglevel + ) + else: + logger.print_line("No new torrents to categorize.", self.config.loglevel) + + def update_cat(self, torrent, new_cat, cat_change): + """Update category based on the torrent information""" + tracker = self.qbt.get_tags(torrent.trackers) + old_cat = torrent.category + if not self.config.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: + ex = logger.print_line( + f'Existing category "{new_cat}" not found for save path {torrent.save_path}, category will be created.', + self.config.loglevel, + ) + self.config.notify(ex, "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 += logger.print_line(logger.insert_space(f"Torrent Name: {torrent.name}", 3), self.config.loglevel) + if cat_change: + body += logger.print_line(logger.insert_space(f"Old Category: {old_cat}", 3), self.config.loglevel) + title = "Moving Categories" + else: + title = "Updating Categories" + body += logger.print_line(logger.insert_space(f"New Category: {new_cat}", 3), self.config.loglevel) + body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), self.config.loglevel) + attr = { + "function": "cat_update", + "title": title, + "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) + self.stats += 1 diff --git a/modules/core/tags.py b/modules/core/tags.py new file mode 100644 index 0000000..f574e6e --- /dev/null +++ b/modules/core/tags.py @@ -0,0 +1,62 @@ +from modules import util + +logger = util.logger + + +class Tags: + def __init__(self, qbit_manager): + self.qbt = qbit_manager + self.config = qbit_manager.config + self.client = qbit_manager.client + self.stats = 0 + + self.tags() + + def tags(self): + """Update tags for torrents""" + self.stats = 0 + ignore_tags = self.config.settings["ignoreTags_OnUpdate"] + logger.separator("Updating Tags", space=False, border=False) + for torrent in self.qbt.torrent_list: + check_tags = util.get_list(torrent.tags) + if torrent.tags == "" or (len([trk for trk in check_tags if trk not in ignore_tags]) == 0): + tracker = self.qbt.get_tags(torrent.trackers) + if tracker["tag"]: + self.stats += len(tracker["tag"]) + body = [] + body += logger.print_line(logger.insert_space(f"Torrent Name: {torrent.name}", 3), self.config.loglevel) + body += logger.print_line( + logger.insert_space(f'New Tag{"s" if len(tracker["tag"]) > 1 else ""}: {", ".join(tracker["tag"])}', 8), + self.config.loglevel, + ) + body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), self.config.loglevel) + body.extend( + self.qbt.set_tags_and_limits( + torrent, + tracker["max_ratio"], + tracker["max_seeding_time"], + tracker["limit_upload_speed"], + tracker["tag"], + ) + ) + category = self.qbt.get_category(torrent.save_path) if torrent.category == "" else torrent.category + attr = { + "function": "tag_update", + "title": "Updating Tags", + "body": "\n".join(body), + "torrent_name": torrent.name, + "torrent_category": category, + "torrent_tag": ", ".join(tracker["tag"]), + "torrent_tracker": tracker["url"], + "notifiarr_indexer": tracker["notifiarr"], + "torrent_max_ratio": tracker["max_ratio"], + "torrent_max_seeding_time": tracker["max_seeding_time"], + "torrent_limit_upload_speed": tracker["limit_upload_speed"], + } + self.config.send_notifications(attr) + if self.stats >= 1: + logger.print_line( + f"{'Did not update' if self.config.dry_run else 'Updated'} {self.stats} new tags.", self.config.loglevel + ) + else: + logger.print_line("No new torrents to tag.", self.config.loglevel) diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index 812af84..cdbdd14 100755 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -7,7 +7,6 @@ from fnmatch import fnmatch from qbittorrentapi import APIConnectionError from qbittorrentapi import Client -from qbittorrentapi import Conflict409Error from qbittorrentapi import LoginFailed from qbittorrentapi import NotFound404Error from qbittorrentapi import Version @@ -26,6 +25,7 @@ class Qbt: SUPPORTED_VERSION = Version.latest_supported_app_version() MIN_SUPPORTED_VERSION = "v4.3.0" + TORRENT_DICT_COMMANDS = ["recheck", "cross_seed", "rem_unregistered", "tag_tracker_error", "tag_nohardlinks"] def __init__(self, config, params): self.config = config @@ -83,256 +83,127 @@ class Qbt: self.global_max_seeding_time_enabled = self.client.app.preferences.max_seeding_time_enabled self.global_max_seeding_time = self.client.app.preferences.max_seeding_time - def get_torrent_info(torrent_list): - """ - Will create a 2D Dictionary with the torrent name as the key - torrentdict = {'TorrentName1' : {'Category':'TV', 'save_path':'/data/torrents/TV', 'count':1, 'msg':'[]'...}, - 'TorrentName2' : {'Category':'Movies', 'save_path':'/data/torrents/Movies'}, 'count':2, 'msg':'[]'...} - List of dictionary key definitions - Category = Returns category of the torrent (str) - save_path = Returns the save path of the torrent (str) - count = Returns a count of the total number of torrents with the same name (int) - msg = Returns a list of torrent messages by name (list of str) - status = Returns the list of status numbers of the torrent by name - (0: Tracker is disabled (used for DHT, PeX, and LSD), - 1: Tracker has not been contacted yet, - 2: Tracker has been contacted and is working, - 3: Tracker is updating, - 4: Tracker has been contacted, but it is not working (or doesn't send proper replies) - is_complete = Returns the state of torrent - (Returns True if at least one of the torrent with the State is categorized as Complete.) - first_hash = Returns the hash number of the original torrent (Assuming the torrent list is sorted by date added (Asc)) - Takes in a number n, returns the square of n - """ - torrentdict = {} - t_obj_unreg = [] # list of unregistered torrent objects - t_obj_valid = [] # list of working torrents - t_obj_list = [] # list of all torrent objects - settings = self.config.settings - logger.separator("Checking Settings", space=False, border=False) - if settings["force_auto_tmm"]: - logger.print_line( - "force_auto_tmm set to True. Will force Auto Torrent Management for all torrents.", self.config.loglevel - ) - logger.separator("Gathering Torrent Information", space=True, border=True) - for torrent in torrent_list: - 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 self.config.dry_run - ): - torrent.set_auto_management(True) - try: - torrent_name = torrent.name - torrent_hash = torrent.hash - torrent_is_complete = torrent.state_enum.is_complete - save_path = torrent.save_path - category = torrent.category - torrent_trackers = torrent.trackers - except Exception as ex: - self.config.notify(ex, "Get Torrent Info", False) - logger.warning(ex) - if torrent_name in torrentdict: - t_obj_list.append(torrent) - t_count = torrentdict[torrent_name]["count"] + 1 - msg_list = torrentdict[torrent_name]["msg"] - status_list = torrentdict[torrent_name]["status"] - is_complete = True if torrentdict[torrent_name]["is_complete"] is True else torrent_is_complete - first_hash = torrentdict[torrent_name]["first_hash"] - else: - t_obj_list = [torrent] - t_count = 1 - msg_list = [] - status_list = [] - is_complete = torrent_is_complete - first_hash = torrent_hash - for trk in torrent_trackers: - if trk.url.startswith("http"): - status = trk.status - msg = trk.msg.upper() - exception = [ - "DOWN", - "DOWN.", - "IT MAY BE DOWN,", - "UNREACHABLE", - "(UNREACHABLE)", - "BAD GATEWAY", - "TRACKER UNAVAILABLE", - ] - if trk.status == 2: - working_tracker = True - break - # Add any potential unregistered torrents to a list - if trk.status == 4 and not list_in_text(msg, exception): - issue["potential"] = True - issue["msg"] = msg - issue["status"] = status - if working_tracker: - status = 2 - msg = "" - t_obj_valid.append(torrent) - 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 = { - "torrents": t_obj_list, - "Category": category, - "save_path": save_path, - "count": t_count, - "msg": msg_list, - "status": status_list, - "is_complete": is_complete, - "first_hash": first_hash, - } - torrentdict[torrent_name] = torrentattr - return torrentdict, t_obj_unreg, t_obj_valid - - self.torrentinfo = None - self.torrentissue = None - self.torrentvalid = None - if ( - config.commands["recheck"] - or config.commands["cross_seed"] - or config.commands["rem_unregistered"] - or config.commands["tag_tracker_error"] - or config.commands["tag_nohardlinks"] - ): + if any(config.commands.get(command, False) for command in self.TORRENT_DICT_COMMANDS): # Get an updated torrent dictionary information of the torrents - self.torrentinfo, self.torrentissue, self.torrentvalid = get_torrent_info(self.torrent_list) + self.get_torrent_info() + else: + self.torrentinfo = None + self.torrentissue = None + self.torrentvalid = None + + def get_torrent_info(self): + """ + Will create a 2D Dictionary with the torrent name as the key + self.torrentinfo = {'TorrentName1' : {'Category':'TV', 'save_path':'/data/torrents/TV', 'count':1, 'msg':'[]'...}, + 'TorrentName2' : {'Category':'Movies', 'save_path':'/data/torrents/Movies'}, 'count':2, 'msg':'[]'...} + List of dictionary key definitions + Category = Returns category of the torrent (str) + save_path = Returns the save path of the torrent (str) + count = Returns a count of the total number of torrents with the same name (int) + msg = Returns a list of torrent messages by name (list of str) + status = Returns the list of status numbers of the torrent by name + (0: Tracker is disabled (used for DHT, PeX, and LSD), + 1: Tracker has not been contacted yet, + 2: Tracker has been contacted and is working, + 3: Tracker is updating, + 4: Tracker has been contacted, but it is not working (or doesn't send proper replies) + is_complete = Returns the state of torrent + (Returns True if at least one of the torrent with the State is categorized as Complete.) + first_hash = Returns the hash number of the original torrent (Assuming the torrent list is sorted by date added (Asc)) + Takes in a number n, returns the square of n + """ + self.torrentinfo = {} + self.torrentissue = [] # list of unregistered torrent objects + self.torrentvalid = [] # list of working torrents + t_obj_list = [] # list of all torrent objects + settings = self.config.settings + logger.separator("Checking Settings", space=False, border=False) + if settings["force_auto_tmm"]: + logger.print_line( + "force_auto_tmm set to True. Will force Auto Torrent Management for all torrents.", self.config.loglevel + ) + logger.separator("Gathering Torrent Information", space=True, border=True) + for torrent in self.torrent_list: + 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 self.config.dry_run: + torrent.set_auto_management(True) + try: + torrent_name = torrent.name + torrent_hash = torrent.hash + torrent_is_complete = torrent.state_enum.is_complete + save_path = torrent.save_path + category = torrent.category + torrent_trackers = torrent.trackers + except Exception as ex: + self.config.notify(ex, "Get Torrent Info", False) + logger.warning(ex) + if torrent_name in self.torrentinfo: + t_obj_list.append(torrent) + t_count = self.torrentinfo[torrent_name]["count"] + 1 + msg_list = self.torrentinfo[torrent_name]["msg"] + status_list = self.torrentinfo[torrent_name]["status"] + is_complete = True if self.torrentinfo[torrent_name]["is_complete"] is True else torrent_is_complete + first_hash = self.torrentinfo[torrent_name]["first_hash"] + else: + t_obj_list = [torrent] + t_count = 1 + msg_list = [] + status_list = [] + is_complete = torrent_is_complete + first_hash = torrent_hash + for trk in torrent_trackers: + if trk.url.startswith("http"): + status = trk.status + msg = trk.msg.upper() + exception = [ + "DOWN", + "DOWN.", + "IT MAY BE DOWN,", + "UNREACHABLE", + "(UNREACHABLE)", + "BAD GATEWAY", + "TRACKER UNAVAILABLE", + ] + if trk.status == 2: + working_tracker = True + break + # Add any potential unregistered torrents to a list + if trk.status == 4 and not list_in_text(msg, exception): + issue["potential"] = True + issue["msg"] = msg + issue["status"] = status + if working_tracker: + status = 2 + msg = "" + self.torrentvalid.append(torrent) + elif issue["potential"]: + status = issue["status"] + msg = issue["msg"] + self.torrentissue.append(torrent) + if msg is not None: + msg_list.append(msg) + if status is not None: + status_list.append(status) + torrentattr = { + "torrents": t_obj_list, + "Category": category, + "save_path": save_path, + "count": t_count, + "msg": msg_list, + "status": status_list, + "is_complete": is_complete, + "first_hash": first_hash, + } + self.torrentinfo[torrent_name] = torrentattr def get_torrents(self, params): """Get torrents from qBittorrent""" return self.client.torrents.info(**params) - def category(self): - """Update category for torrents""" - num_cat = 0 - - def update_cat(new_cat, cat_change): - nonlocal torrent, num_cat - tracker = self.config.get_tags(torrent.trackers) - old_cat = torrent.category - if not self.config.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: - ex = logger.print_line( - f'Existing category "{new_cat}" not found for save path {torrent.save_path}, category will be created.', - self.config.loglevel, - ) - self.config.notify(ex, "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 += logger.print_line(logger.insert_space(f"Torrent Name: {torrent.name}", 3), self.config.loglevel) - if cat_change: - body += logger.print_line(logger.insert_space(f"Old Category: {old_cat}", 3), self.config.loglevel) - title = "Moving Categories" - else: - title = "Updating Categories" - body += logger.print_line(logger.insert_space(f"New Category: {new_cat}", 3), self.config.loglevel) - body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), self.config.loglevel) - attr = { - "function": "cat_update", - "title": title, - "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 self.config.commands["cat_update"]: - logger.separator("Updating Categories", space=False, border=False) - torrent_list = self.get_torrents({"category": "", "status_filter": "completed"}) - for torrent in torrent_list: - new_cat = self.config.get_category(torrent.save_path) - update_cat(new_cat, False) - - # Change categories - if self.config.cat_change: - for old_cat in self.config.cat_change: - torrent_list = self.get_torrents({"category": old_cat, "status_filter": "completed"}) - for torrent in torrent_list: - new_cat = self.config.cat_change[old_cat] - update_cat(new_cat, True) - - if num_cat >= 1: - logger.print_line( - f"{'Did not update' if self.config.dry_run else 'Updated'} {num_cat} new categories.", self.config.loglevel - ) - else: - logger.print_line("No new torrents to categorize.", self.config.loglevel) - return num_cat - - def tags(self): - """Update tags for torrents""" - num_tags = 0 - ignore_tags = self.config.settings["ignoreTags_OnUpdate"] - if self.config.commands["tag_update"]: - logger.separator("Updating Tags", space=False, border=False) - for torrent in self.torrent_list: - check_tags = util.get_list(torrent.tags) - if torrent.tags == "" or (len([trk for trk in check_tags if trk not in ignore_tags]) == 0): - tracker = self.config.get_tags(torrent.trackers) - if tracker["tag"]: - num_tags += len(tracker["tag"]) - body = [] - body += logger.print_line(logger.insert_space(f"Torrent Name: {torrent.name}", 3), self.config.loglevel) - body += logger.print_line( - logger.insert_space( - f'New Tag{"s" if len(tracker["tag"]) > 1 else ""}: {", ".join(tracker["tag"])}', 8 - ), - self.config.loglevel, - ) - body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), self.config.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 - attr = { - "function": "tag_update", - "title": "Updating Tags", - "body": "\n".join(body), - "torrent_name": torrent.name, - "torrent_category": category, - "torrent_tag": ", ".join(tracker["tag"]), - "torrent_tracker": tracker["url"], - "notifiarr_indexer": tracker["notifiarr"], - "torrent_max_ratio": tracker["max_ratio"], - "torrent_max_seeding_time": tracker["max_seeding_time"], - "torrent_limit_upload_speed": tracker["limit_upload_speed"], - } - self.config.send_notifications(attr) - if num_tags >= 1: - logger.print_line( - f"{'Did not update' if self.config.dry_run else 'Updated'} {num_tags} new tags.", self.config.loglevel - ) - else: - logger.print_line("No new torrents to tag.", self.config.loglevel) - return num_tags - def set_tags_and_limits( self, torrent, max_ratio, max_seeding_time, limit_upload_speed=None, tags=None, restore=False, do_print=True ): @@ -460,6 +331,240 @@ class Qbt: return body return False + def get_tags(self, trackers): + """Get tags from config file based on keyword""" + urls = [x.url for x in trackers if x.url.startswith("http")] + tracker = {} + tracker["tag"] = None + tracker["max_ratio"] = None + tracker["min_seeding_time"] = None + tracker["max_seeding_time"] = None + tracker["limit_upload_speed"] = None + tracker["notifiarr"] = None + tracker["url"] = None + tracker_other_tag = self.config.util.check_for_attribute( + self.config.data, "tag", parent="tracker", subparent="other", default_is_none=True, var_type="list", save=False + ) + try: + tracker["url"] = util.trunc_val(urls[0], os.sep) + except IndexError as e: + tracker["url"] = None + if not urls: + urls = [] + if not tracker_other_tag: + tracker_other_tag = ["other"] + tracker["url"] = "No http URL found" + else: + logger.debug(f"Tracker Url:{urls}") + logger.debug(e) + if "tracker" in self.config.data and self.config.data["tracker"] is not None: + tag_values = self.config.data["tracker"] + for tag_url, tag_details in tag_values.items(): + for url in urls: + if tag_url in url: + if tracker["url"] is None: + default_tag = tracker_other_tag + else: + try: + tracker["url"] = util.trunc_val(url, os.sep) + default_tag = tracker["url"].split(os.sep)[2].split(":")[0] + except IndexError as e: + logger.debug(f"Tracker Url:{url}") + logger.debug(e) + # Tracker Format 1 deprecated. + if isinstance(tag_details, str): + e = ( + "Config Error: Tracker format invalid. Please see config.yml.sample for correct format and fix " + f"`{tag_details}` in the Tracker section of the config." + ) + self.config.notify(e, "Config") + raise Failed(e) + # Using new Format + else: + tracker["tag"] = self.config.util.check_for_attribute( + self.config.data, "tag", parent="tracker", subparent=tag_url, default=tag_url, var_type="list" + ) + if tracker["tag"] == [tag_url]: + self.config.data["tracker"][tag_url]["tag"] = [tag_url] + if isinstance(tracker["tag"], str): + tracker["tag"] = [tracker["tag"]] + is_max_ratio_defined = self.config.data["tracker"].get("max_ratio") + is_max_seeding_time_defined = self.config.data["tracker"].get("max_seeding_time") + if is_max_ratio_defined or is_max_seeding_time_defined: + tracker["max_ratio"] = self.config.util.check_for_attribute( + self.config.data, + "max_ratio", + parent="tracker", + subparent=tag_url, + var_type="float", + min_int=-2, + do_print=False, + default=-1, + save=False, + ) + tracker["max_seeding_time"] = self.config.util.check_for_attribute( + self.config.data, + "max_seeding_time", + parent="tracker", + subparent=tag_url, + var_type="int", + min_int=-2, + do_print=False, + default=-1, + save=False, + ) + else: + tracker["max_ratio"] = self.config.util.check_for_attribute( + self.config.data, + "max_ratio", + parent="tracker", + subparent=tag_url, + var_type="float", + min_int=-2, + do_print=False, + default_is_none=True, + save=False, + ) + tracker["max_seeding_time"] = self.config.util.check_for_attribute( + self.config.data, + "max_seeding_time", + parent="tracker", + subparent=tag_url, + var_type="int", + min_int=-2, + do_print=False, + default_is_none=True, + save=False, + ) + tracker["min_seeding_time"] = self.config.util.check_for_attribute( + self.config.data, + "min_seeding_time", + parent="tracker", + subparent=tag_url, + var_type="int", + min_int=0, + do_print=False, + default=0, + save=False, + ) + tracker["limit_upload_speed"] = self.config.util.check_for_attribute( + self.config.data, + "limit_upload_speed", + parent="tracker", + subparent=tag_url, + var_type="int", + min_int=-1, + do_print=False, + default=0, + save=False, + ) + tracker["notifiarr"] = self.config.util.check_for_attribute( + self.config.data, + "notifiarr", + parent="tracker", + subparent=tag_url, + default_is_none=True, + do_print=False, + save=False, + ) + return tracker + if tracker_other_tag: + tracker["tag"] = tracker_other_tag + tracker["max_ratio"] = self.config.util.check_for_attribute( + self.config.data, + "max_ratio", + parent="tracker", + subparent="other", + var_type="float", + min_int=-2, + do_print=False, + default=-1, + save=False, + ) + tracker["min_seeding_time"] = self.config.util.check_for_attribute( + self.config.data, + "min_seeding_time", + parent="tracker", + subparent="other", + var_type="int", + min_int=0, + do_print=False, + default=-1, + save=False, + ) + tracker["max_seeding_time"] = self.config.util.check_for_attribute( + self.config.data, + "max_seeding_time", + parent="tracker", + subparent="other", + var_type="int", + min_int=-2, + do_print=False, + default=-1, + save=False, + ) + tracker["limit_upload_speed"] = self.config.util.check_for_attribute( + self.config.data, + "limit_upload_speed", + parent="tracker", + subparent="other", + var_type="int", + min_int=-1, + do_print=False, + default=0, + save=False, + ) + tracker["notifiarr"] = self.config.util.check_for_attribute( + self.config.data, + "notifiarr", + parent="tracker", + subparent="other", + default_is_none=True, + do_print=False, + save=False, + ) + return tracker + if tracker["url"]: + logger.trace(f"tracker url: {tracker['url']}") + if tracker_other_tag: + default_tag = tracker_other_tag + else: + default_tag = tracker["url"].split(os.sep)[2].split(":")[0] + tracker["tag"] = self.config.util.check_for_attribute( + self.config.data, "tag", parent="tracker", subparent=default_tag, default=default_tag, var_type="list" + ) + if isinstance(tracker["tag"], str): + tracker["tag"] = [tracker["tag"]] + try: + self.config.data["tracker"][default_tag]["tag"] = [default_tag] + except Exception: + self.config.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.config.notify(e, "Tag", False) + logger.warning(e) + return tracker + + def get_category(self, path): + """Get category from config file based on path provided""" + category = "" + path = os.path.join(path, "") + if "cat" in self.config.data and self.config.data["cat"] is not None: + cat_path = self.config.data["cat"] + for cat, save_path in cat_path.items(): + if os.path.join(save_path, "") == path: + category = cat + break + + if not category: + default_cat = path.split(os.sep)[-2] + category = str(default_cat) + self.config.util.check_for_attribute(self.config.data, default_cat, parent="cat", default=path) + self.config.data["cat"][str(default_cat)] = path + e = f"No categories matched for the save path {path}. Check your config.yml file. - Setting category to {default_cat}" + self.config.notify(e, "Category", False) + logger.warning(e) + return category + def tag_nohardlinks(self): """Tag torrents with no hardlinks""" num_tags = 0 # counter for the number of torrents that has no hardlinks @@ -530,7 +635,7 @@ class Qbt: logger.warning(ex) continue for torrent in torrent_list: - tracker = self.config.get_tags(torrent.trackers) + tracker = self.get_tags(torrent.trackers) has_nohardlinks = util.nohardlink(torrent["content_path"].replace(root_dir, remote_dir), self.config.notify) if any(tag in torrent.tags for tag in nohardlinks[category]["exclude_tags"]): # Skip to the next torrent if we find any torrents that are in the exclude tag @@ -539,7 +644,7 @@ class Qbt: # Checks for any hardlinks and not already tagged # Cleans up previously tagged nohardlinks_tag torrents that no longer have hardlinks if has_nohardlinks: - tracker = self.config.get_tags(torrent.trackers) + tracker = self.get_tags(torrent.trackers) # Determine min_seeding_time. # If only tracker setting is set, use tracker's min_seeding_time # If only nohardlinks category setting is set, use nohardlinks category's min_seeding_time @@ -666,7 +771,7 @@ class Qbt: t_status = self.torrentinfo[t_name]["status"] # Double check that the content path is the same before we delete anything if torrent["content_path"].replace(root_dir, remote_dir) == tdel_dict[t_hash]["content_path"]: - tracker = self.config.get_tags(torrent.trackers) + tracker = self.get_tags(torrent.trackers) body = [] body += logger.print_line(logger.insert_space(f"Torrent Name: {t_name}", 3), self.config.loglevel) body += logger.print_line( @@ -852,7 +957,7 @@ class Qbt: check_tags = util.get_list(torrent.tags) # Remove any error torrents Tags that are no longer unreachable. if tag_error in check_tags: - tracker = self.config.get_tags(torrent.trackers) + tracker = self.get_tags(torrent.trackers) num_untag += 1 body = [] body += logger.print_line( @@ -884,7 +989,7 @@ class Qbt: try: for trk in torrent.trackers: if trk.url.startswith("http"): - tracker = self.config.get_tags([trk]) + tracker = self.get_tags([trk]) msg_up = trk.msg.upper() msg = trk.msg # Tag any error torrents @@ -964,16 +1069,16 @@ class Qbt: for file in cs_files: tr_name = file.split("]", 2)[2].split(".torrent")[0] t_tracker = file.split("]", 2)[1][1:] - # Substring Key match in dictionary (used because t_name might not match exactly with torrentdict key) + # Substring Key match in dictionary (used because t_name might not match exactly with self.torrentinfo key) # Returned the dictionary of filtered item torrentdict_file = dict(filter(lambda item: tr_name in item[0], self.torrentinfo.items())) if torrentdict_file: - # Get the exact torrent match name from torrentdict + # Get the exact torrent match name from self.torrentinfo t_name = next(iter(torrentdict_file)) dest = os.path.join(self.torrentinfo[t_name]["save_path"], "") src = os.path.join(dir_cs, file) dir_cs_out = os.path.join(dir_cs, "qbit_manage_added", file) - category = self.config.get_category(dest) + category = self.get_category(dest) # Only add cross-seed torrent if original torrent is complete if self.torrentinfo[t_name]["is_complete"]: categories.append(category) @@ -1023,7 +1128,7 @@ class Qbt: and self.torrentinfo[t_name]["count"] > 1 and self.torrentinfo[t_name]["first_hash"] != torrent.hash ): - tracker = self.config.get_tags(torrent.trackers) + tracker = self.get_tags(torrent.trackers) tagged += 1 body = logger.print_line( f"{'Not Adding' if self.config.dry_run else 'Adding'} 'cross-seed' tag to {t_name}", self.config.loglevel @@ -1069,7 +1174,7 @@ class Qbt: torrent_list = self.get_torrents({"status_filter": "paused", "sort": "size"}) if torrent_list: for torrent in torrent_list: - tracker = self.config.get_tags(torrent.trackers) + tracker = self.get_tags(torrent.trackers) # Resume torrent if completed if torrent.progress == 1: if torrent.max_ratio < 0 and torrent.max_seeding_time < 0: diff --git a/qbit_manage.py b/qbit_manage.py index c2c6742..7a4a918 100755 --- a/qbit_manage.py +++ b/qbit_manage.py @@ -287,6 +287,8 @@ util.logger = logger from modules.config import Config # noqa from modules.util import GracefulKiller # noqa from modules.util import Failed # noqa +from modules.core.category import Category # noqa +from modules.core.tags import Tags # noqa def my_except_hook(exctype, value, tbi): @@ -369,6 +371,8 @@ def start(): try: cfg = Config(default_dir, args) + qbit_manager = cfg.qbt + except Exception as ex: if "Qbittorrent Error" in ex.args[0]: logger.print_line(ex, "CRITICAL") @@ -379,14 +383,14 @@ def start(): logger.stacktrace() logger.print_line(ex, "CRITICAL") - if cfg: + if qbit_manager: # Set Category - num_categorized = cfg.qbt.category() - stats["categorized"] += num_categorized + if cfg.commands["cat_update"]: + stats["categorized"] += Category(qbit_manager).stats # Set Tags - num_tagged = cfg.qbt.tags() - stats["tagged"] += num_tagged + if cfg.commands["tag_update"]: + stats["tagged"] += Tags(qbit_manager).stats # Remove Unregistered Torrents num_deleted, num_deleted_contents, num_tagged, num_untagged = cfg.qbt.rem_unregistered()