mirror of
https://github.com/StuffAnThings/qbit_manage.git
synced 2025-09-13 16:45:23 +08:00
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:
parent
592687a734
commit
0f74ba471d
2 changed files with 206 additions and 89 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
3.3.2
|
||||
3.4.0
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue