diff --git a/VERSION b/VERSION index 4772543..1809198 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.3.2 +3.4.0 diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index 352ddb4..5eb1aab 100755 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -76,28 +76,36 @@ class Qbt: logger.separator("Getting Torrent List", space=False, border=False) self.torrent_list = self.get_torrents({"sort": "added_on"}) - # 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)) + self.global_max_ratio_enabled = self.client.app.preferences.max_ratio_enabled + self.global_max_ratio = self.client.app.preferences.max_ratio + 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 = [] - t_obj_valid = [] - t_obj_list = [] + 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"]: @@ -320,34 +328,56 @@ class Qbt: 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): + def set_tags_and_limits( + self, torrent, max_ratio, max_seeding_time, limit_upload_speed=None, tags=None, restore=False, do_print=True + ): body = [] if limit_upload_speed: if limit_upload_speed == -1: - body += logger.print_line(logger.insert_space("Limit UL Speed: Infinity", 1), self.config.loglevel) + msg = logger.insert_space("Limit UL Speed: Infinity", 1) + if do_print: + body += logger.print_line(msg, self.config.loglevel) + else: + body.append(msg) else: - body += logger.print_line( - logger.insert_space(f"Limit UL Speed: {limit_upload_speed} kB/s", 1), self.config.loglevel - ) + msg = logger.insert_space(f"Limit UL Speed: {limit_upload_speed} kB/s", 1) + if do_print: + body += logger.print_line(msg, self.config.loglevel) + else: + body.append(msg) if max_ratio or max_seeding_time: if (max_ratio == -2 and max_seeding_time == -2) and not restore: - body += logger.print_line(logger.insert_space("Share Limit: Use Global Share Limit", 4), self.config.loglevel) + msg = logger.insert_space("Share Limit: Use Global Share Limit", 4) + if do_print: + body += logger.print_line(msg, self.config.loglevel) + else: + body.append(msg) elif (max_ratio == -1 and max_seeding_time == -1) and not restore: - body += logger.print_line(logger.insert_space("Share Limit: Set No Share Limit", 4), self.config.loglevel) + msg = logger.insert_space("Share Limit: Set No Share Limit", 4) + if do_print: + body += logger.print_line(msg, self.config.loglevel) + else: + body.append(msg) else: if max_ratio != torrent.max_ratio and (not max_seeding_time or max_seeding_time < 0): - body += logger.print_line( - logger.insert_space(f"Share Limit: Max Ratio = {max_ratio}", 4), self.config.loglevel - ) + msg = logger.insert_space(f"Share Limit: Max Ratio = {max_ratio}", 4) + if do_print: + body += logger.print_line(msg, self.config.loglevel) + else: + body.append(msg) + elif max_seeding_time != torrent.max_seeding_time and (not max_ratio or max_ratio < 0): - body += logger.print_line( - logger.insert_space(f"Share Limit: Max Seed Time = {max_seeding_time} min", 4), self.config.loglevel - ) - elif max_ratio != torrent.max_ratio and max_seeding_time != torrent.max_seeding_time: - body += logger.print_line( - logger.insert_space(f"Share Limit: Max Ratio = {max_ratio}, Max Seed Time = {max_seeding_time} min", 4), - self.config.loglevel, - ) + msg = logger.insert_space(f"Share Limit: Max Seed Time = {max_seeding_time} min", 4) + if do_print: + body += logger.print_line(msg, self.config.loglevel) + else: + body.append(msg) + elif max_ratio != torrent.max_ratio or max_seeding_time != torrent.max_seeding_time: + msg = logger.insert_space(f"Share Limit: Max Ratio = {max_ratio}, Max Seed Time = {max_seeding_time} min", 4) + if do_print: + body += logger.print_line(msg, self.config.loglevel) + else: + body.append(msg) # Update Torrents if not self.config.dry_run: if tags: @@ -361,9 +391,73 @@ class Qbt: max_ratio = torrent.max_ratio if not max_seeding_time: max_seeding_time = torrent.max_seeding_time + if "MinSeedTimeNotReached" in torrent.tags: + return [] torrent.set_share_limits(max_ratio, max_seeding_time) return body + def has_reached_seed_limit(self, torrent, max_ratio, max_seeding_time, min_seeding_time, resume_torrent, tracker): + body = "" + + def _has_reached_min_seeding_time_limit(): + print_log = [] + if torrent.seeding_time >= min_seeding_time * 60: + return True + else: + print_log += logger.print_line(logger.insert_space(f"Torrent Name: {torrent.name}", 3), self.config.loglevel) + print_log += logger.print_line(logger.insert_space(f"Tracker: {tracker}", 8), self.config.loglevel) + print_log += logger.print_line( + logger.insert_space( + f"Min seed time not met: {timedelta(seconds=torrent.seeding_time)} <= " + f"{timedelta(minutes=min_seeding_time)}. Removing Share Limits so qBittorrent can continue seeding.", + 8, + ), + self.config.loglevel, + ) + print_log += logger.print_line(logger.insert_space("Adding Tag: MinSeedTimeNotReached", 8), self.config.loglevel) + if not self.config.dry_run: + torrent.add_tags("MinSeedTimeNotReached") + torrent.set_share_limits(-1, -1) + if resume_torrent: + torrent.resume() + return False + + def _has_reached_seeding_time_limit(): + nonlocal body + seeding_time_limit = None + if not max_seeding_time: + return False + if max_seeding_time >= 0: + seeding_time_limit = max_seeding_time + elif max_seeding_time == -2 and self.global_max_seeding_time_enabled: + seeding_time_limit = self.global_max_seeding_time + else: + return False + if seeding_time_limit: + if (torrent.seeding_time >= seeding_time_limit * 60) and _has_reached_min_seeding_time_limit(): + body += logger.insert_space( + f"Seeding Time vs Max Seed Time: {timedelta(seconds=torrent.seeding_time)} >= " + f"{timedelta(minutes=seeding_time_limit)}", + 8, + ) + return True + return False + + if max_ratio: + if max_ratio >= 0: + if torrent.ratio >= max_ratio and _has_reached_min_seeding_time_limit(): + body += logger.insert_space(f"Ratio vs Max Ratio: {torrent.ratio:.2f} >= {max_ratio:.2f}", 8) + return body + elif max_ratio == -2 and self.global_max_ratio_enabled and _has_reached_min_seeding_time_limit(): + if torrent.ratio >= self.global_max_ratio: + body += logger.insert_space( + f"Ratio vs Global Max Ratio: {torrent.ratio:.2f} >= {self.global_max_ratio:.2f}", 8 + ) + return body + if _has_reached_seeding_time_limit(): + return body + return False + def tag_nohardlinks(self): num_tags = 0 # counter for the number of torrents that has no hard links del_tor = 0 # counter for the number of torrents that has no hard links and \ @@ -372,6 +466,48 @@ class Qbt: # meets the criteria for ratio limit/seed limit for deletion including contents num_untag = 0 # counter for number of torrents that previously had no hard links but now have hard links + def add_tag_noHL(add_tag=True): + nonlocal num_tags, torrent, tracker, nohardlinks, category + body = [] + body.append(logger.insert_space(f"Torrent Name: {torrent.name}", 3)) + if add_tag: + body.append(logger.insert_space("Added Tag: noHL", 6)) + title = "Tagging Torrents with No Hardlinks" + else: + title = "Changing Share Ratio of Torrents with No Hardlinks" + body.append(logger.insert_space(f'Tracker: {tracker["url"]}', 8)) + body_tags_and_limits = self.set_tags_and_limits( + torrent, + nohardlinks[category]["max_ratio"], + nohardlinks[category]["max_seeding_time"], + nohardlinks[category]["limit_upload_speed"], + tags="noHL", + do_print=False, + ) + if body_tags_and_limits: + num_tags += 1 + # Resume torrent if it was paused now that the share limit has changed + if torrent.state == "pausedUP" and nohardlinks[category]["resume_torrent_after_untagging_noHL"]: + if not self.config.dry_run: + torrent.resume() + body.extend(body_tags_and_limits) + for b in body: + logger.print_line(b, self.config.loglevel) + attr = { + "function": "tag_nohardlinks", + "title": title, + "body": "\n".join(body), + "torrent_name": torrent.name, + "torrent_category": torrent.category, + "torrent_tag": "noHL", + "torrent_tracker": tracker["url"], + "notifiarr_indexer": tracker["notifiarr"], + "torrent_max_ratio": nohardlinks[category]["max_ratio"], + "torrent_max_seeding_time": nohardlinks[category]["max_seeding_time"], + "torrent_limit_upload_speed": nohardlinks[category]["limit_upload_speed"], + } + self.config.send_notifications(attr) + if self.config.commands["tag_nohardlinks"]: logger.separator("Tagging Torrents with No Hardlinks", space=False, border=False) nohardlinks = self.config.nohardlinks @@ -387,7 +523,6 @@ class Qbt: + ") 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 torrent_list: @@ -400,56 +535,36 @@ class Qbt: if util.nohardlink(torrent["content_path"].replace(root_dir, remote_dir)): # Will only tag new torrents that don't have noHL tag if "noHL" not in torrent.tags: - num_tags += 1 - body = [] - body += logger.print_line( - logger.insert_space(f"Torrent Name: {torrent.name}", 3), self.config.loglevel - ) - body += logger.print_line(logger.insert_space("Added Tag: noHL", 6), 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, - nohardlinks[category]["max_ratio"], - nohardlinks[category]["max_seeding_time"], - nohardlinks[category]["limit_upload_speed"], - tags="noHL", - ) - ) - attr = { - "function": "tag_nohardlinks", - "title": "Tagging Torrents with No Hardlinks", - "body": "\n".join(body), - "torrent_name": torrent.name, - "torrent_category": torrent.category, - "torrent_tag": "noHL", - "torrent_tracker": tracker["url"], - "notifiarr_indexer": tracker["notifiarr"], - "torrent_max_ratio": nohardlinks[category]["max_ratio"], - "torrent_max_seeding_time": nohardlinks[category]["max_seeding_time"], - "torrent_limit_upload_speed": nohardlinks[category]["limit_upload_speed"], - } - self.config.send_notifications(attr) + add_tag_noHL(add_tag=True) # Cleans up previously tagged noHL torrents - else: - # Determine min_seeding_time. noHl > Tracker w/ default 0 - min_seeding_time = 0 - tracker = self.config.get_tags([x.url for x in torrent.trackers if x.url.startswith("http")]) - if nohardlinks[category]["min_seeding_time"]: - min_seeding_time = nohardlinks[category]["min_seeding_time"] - elif tracker["min_seeding_time"]: - min_seeding_time = tracker["min_seeding_time"] + # Determine min_seeding_time. noHl > Tracker w/ default 0 + min_seeding_time = 0 + tracker = self.config.get_tags([x.url for x in torrent.trackers if x.url.startswith("http")]) + if nohardlinks[category]["min_seeding_time"]: + min_seeding_time = nohardlinks[category]["min_seeding_time"] + elif tracker["min_seeding_time"]: + min_seeding_time = tracker["min_seeding_time"] - # Deletes torrent with data if cleanup is set to true and meets the ratio/seeding requirements - if ( - nohardlinks[category]["cleanup"] - and torrent.state_enum.is_paused - and len(nohardlinks[category]) > 0 - and torrent.seeding_time > (min_seeding_time * 60) - ): - tdel_dict[torrent.name] = torrent["content_path"].replace(root_dir, root_dir) + # Deletes torrent with data if cleanup is set to true and meets the ratio/seeding requirements + if nohardlinks[category]["cleanup"] and len(nohardlinks[category]) > 0: + tor_reach_seed_limit = self.has_reached_seed_limit( + torrent, + nohardlinks[category]["max_ratio"], + nohardlinks[category]["max_seeding_time"], + min_seeding_time, + nohardlinks[category]["resume_torrent_after_untagging_noHL"], + tracker["url"], + ) + if tor_reach_seed_limit: + if torrent.name not in tdel_dict: + tdel_dict[torrent.name] = {} + tdel_dict[torrent.name]["content_path"] = torrent["content_path"].replace(root_dir, root_dir) + tdel_dict[torrent.name]["body"] = tor_reach_seed_limit + else: + # Updates torrent to see if "MinSeedTimeNotReached" tag has been added + torrent = self.get_torrents({"torrent_hashes": [torrent.hash]}).data[0] + # Checks to see if previously noHL share limits have changed. + add_tag_noHL(add_tag=False) # Checks to see if previous noHL tagged torrents now have hard links. if not (util.nohardlink(torrent["content_path"].replace(root_dir, root_dir))) and ("noHL" in torrent.tags): num_untag += 1 @@ -496,6 +611,7 @@ class Qbt: self.config.send_notifications(attr) # loop through torrent list again for cleanup purposes if nohardlinks[category]["cleanup"]: + torrent_list = self.get_torrents({"category": category, "filter": "completed"}) for torrent in torrent_list: t_name = torrent.name if t_name in tdel_dict.keys() and "noHL" in torrent.tags: @@ -503,13 +619,14 @@ class Qbt: t_msg = self.torrentinfo[t_name]["msg"] 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, root_dir) == tdel_dict[t_name]: + if torrent["content_path"].replace(root_dir, root_dir) == tdel_dict[t_name]["content_path"]: tracker = self.config.get_tags([x.url for x in torrent.trackers if x.url.startswith("http")]) body = [] body += logger.print_line(logger.insert_space(f"Torrent Name: {t_name}", 3), self.config.loglevel) body += logger.print_line( logger.insert_space(f'Tracker: {tracker["url"]}', 8), self.config.loglevel ) + body += logger.print_line(tdel_dict[t_name]["body"], self.config.loglevel) body += logger.print_line( logger.insert_space("Cleanup: True [No hard links found and meets Share Limits.]", 8), self.config.loglevel,