Fixes bug #173 Major refactor in shhare limits

- Fixes bug where paused `noHL` torrents were being removed by qbit-manage when seed time/ratios not met
- Share Limit deletions are now handled directly in qbm. This means qbm determines whether or not things should be deleted rather than qBittorrent.
- Better logging when `noHL` torrents get deleted once share limits are reached
   - It displays the numbers and reason why the share limit has been reached
- `noHL` torrent share limits get updated on each qbm run to reflect the latest config
   - Previously you would have to untag all `noHL` torrents and re-run to apply the new share limits
- `noHL` torrents get tagged and deleted in the same run if share limits are reached
   - Previously you would need to run qbm twice, first run would tag and second run would delete if share limits are reached
- Automatic handling of min_seeding_time requiring no more manual intervention.
   - Torrents that have met share ratio but not met the min_seeding_time requirements will now automatically remove share limits in qbt and resume torrent state in order to seed longer until share ratios are met.
   - These torrents are tagged with a new tag `MinSeedTimeNotReached`. Any torrents with this tag will not have share ratio limits applied in order to avoid qbt from pausing the torrent, (qbm will still delete this torrent once the minimum seed time is reached)
This commit is contained in:
bobokun 2022-11-21 21:14:20 -05:00
parent 592687a734
commit 0f74ba471d
No known key found for this signature in database
GPG key ID: B73932169607D927
2 changed files with 206 additions and 89 deletions

View file

@ -1 +1 @@
3.3.2
3.4.0

View file

@ -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,