Merge pull request #344 from StuffAnThings/develop

4.0.2
This commit is contained in:
bobokun 2023-06-27 20:03:54 -04:00 committed by GitHub
commit 16d545067f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 121 additions and 54 deletions

View file

@ -27,16 +27,16 @@ repos:
args: [--format, parsable, --strict] args: [--format, parsable, --strict]
exclude: ^.github/ exclude: ^.github/
- repo: https://github.com/lyz-code/yamlfix - repo: https://github.com/lyz-code/yamlfix
rev: 1.10.0 rev: 1.11.0
hooks: hooks:
- id: yamlfix - id: yamlfix
exclude: ^.github/ exclude: ^.github/
- repo: https://github.com/asottile/reorder-python-imports - repo: https://github.com/asottile/reorder-python-imports
rev: v3.9.0 rev: v3.10.0
hooks: hooks:
- id: reorder-python-imports - id: reorder-python-imports
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.6.0 rev: v3.7.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py3-plus] args: [--py3-plus]

View file

@ -1,26 +1,13 @@
# Requirements Updated # Requirements Updated
- qbitorrent-api updated to 2023.6.49 - qbitorrent-api updated to 2023.6.50
- ruamel.yaml updated to 0.17.32
# New Features # New Features
- cross-seed will move torrent to an error folder if it fails to inject torrent - Adds min_num_seeds condition to share_limits (Closes [#321](https://github.com/StuffAnThings/qbit_manage/issues/321))
- Define multiple announce urls for one tracker (#328 Thanks to @buthed010203 for the PR)
# Bug Fixes # Bug Fixes
- Fixes #329 (Updates missing share_limits tag even when share_limits are satisfied) Fixes [#333](https://github.com/StuffAnThings/qbit_manage/issues/333)
- Fixes #327 (Zero value share limits not being applied correctly) Fixes [#340](https://github.com/StuffAnThings/qbit_manage/issues/340)
Fixes [#343](https://github.com/StuffAnThings/qbit_manage/issues/343)
Fixes bug when checking tags in torrents
# Enhancements **Full Changelog**: https://github.com/StuffAnThings/qbit_manage/compare/v4.0.1...v4.0.2
- Logic for `share_limits_suffix_tag` changed to become a prefix tag instead along with adding the priority of the group. The reason for this change is so it's easier to see the share limit groups togethered in qbitorrent ordered by priority.
- `share_limits_suffix_tag` key is now `share_limits_tag`
- No config changes are required as the qbm will automatically change the previous `share_limits_suffix_tag` key to `share_limits_tag`
- Changes the default value of `share_limits_tag` to `~share_limit`. The `~` is used to sort the `share_limits_tag` in the qbt webUI to the bottom for less clutter
Example based on config.sample:
| old tag (v4.0.0) | new tag (v4.0.1) |
| ----------- | ----------- |
| noHL.share_limit | ~share_limit_1.noHL |
| cross-seed.share_limit | ~share_limit_2.cross-seed |
| PTP.share_limit | ~share_limit_3.PTP |
| default.share_limit | ~share_limit_999.default |
**Full Changelog**: https://github.com/StuffAnThings/qbit_manage/compare/v4.0.0...v4.0.1

View file

@ -1 +1 @@
4.0.1 4.0.2

View file

@ -176,6 +176,7 @@ share_limits:
# Will default to -1 (no limit) if not specified for the group. # Will default to -1 (no limit) if not specified for the group.
max_seeding_time: 129600 max_seeding_time: 129600
# <OPTIONAL> min_seeding_time <int>: Will prevent torrent deletion by cleanup variable if torrent has not yet minimum seeding time (minutes). # <OPTIONAL> min_seeding_time <int>: Will prevent torrent deletion by cleanup variable if torrent has not yet minimum seeding time (minutes).
# If the torrent has not yet reached this minimum seeding time, it will change the share limits back to no limits and resume the torrent to continue seeding.
# Will default to 0 if not specified for the group. # Will default to 0 if not specified for the group.
min_seeding_time: 43200 min_seeding_time: 43200
# <OPTIONAL> Limit Upload Speed <int>: Will limit the upload speed KiB/s (KiloBytes/second) (`-1` : No Limit) # <OPTIONAL> Limit Upload Speed <int>: Will limit the upload speed KiB/s (KiloBytes/second) (`-1` : No Limit)
@ -187,6 +188,10 @@ share_limits:
# <OPTIONAL> add_group_to_tag <bool>: This adds your grouping as a tag with a prefix defined in settings . Default is true # <OPTIONAL> add_group_to_tag <bool>: This adds your grouping as a tag with a prefix defined in settings . Default is true
# Example: A grouping defined as noHL will have a tag set to ~share_limit.noHL (if using the default prefix) # Example: A grouping defined as noHL will have a tag set to ~share_limit.noHL (if using the default prefix)
add_group_to_tag: true add_group_to_tag: true
# <OPTIONAL> min_num_seeds <int>: This will prevent torrent deletion by cleanup variable if the number of seeds is less than the value set here.
# If the torrent has less number of seeds than the min_num_seeds, the share limits will be changed back to no limits and resume the torrent to continue seeding.
# Will default to 0 if not specified for the group.
min_num_seeds: 0
cross-seed: cross-seed:
priority: 2 priority: 2
include_all_tags: include_all_tags:
@ -237,7 +242,7 @@ orphaned:
- "**/@eaDir" - "**/@eaDir"
- "/data/torrents/temp/**" - "/data/torrents/temp/**"
- "**/*.!qB" - "**/*.!qB"
- "**/_unpackerred" - "**/*_unpackerred"
apprise: apprise:
# Apprise integration with webhooks # Apprise integration with webhooks

View file

@ -432,6 +432,17 @@ class Config:
do_print=False, do_print=False,
save=False, save=False,
) )
self.share_limits[group]["min_num_seeds"] = self.util.check_for_attribute(
self.data,
"min_num_seeds",
parent="share_limits",
subparent=group,
var_type="int",
min_int=0,
default=0,
do_print=False,
save=False,
)
self.share_limits[group]["resume_torrent_after_change"] = self.util.check_for_attribute( self.share_limits[group]["resume_torrent_after_change"] = self.util.check_for_attribute(
self.data, self.data,
"resume_torrent_after_change", "resume_torrent_after_change",

View file

@ -112,7 +112,7 @@ class CrossSeed:
t_name = torrent.name t_name = torrent.name
t_cat = torrent.category t_cat = torrent.category
if ( if (
"cross-seed" not in torrent.tags not util.is_tag_in_torrent("cross-seed", torrent.tags)
and self.qbt.torrentinfo[t_name]["count"] > 1 and self.qbt.torrentinfo[t_name]["count"] > 1
and self.qbt.torrentinfo[t_name]["first_hash"] != torrent.hash and self.qbt.torrentinfo[t_name]["first_hash"] != torrent.hash
): ):

View file

@ -31,7 +31,7 @@ class RemoveOrphaned:
orphaned_files = [] orphaned_files = []
excluded_orphan_files = [] excluded_orphan_files = []
root_files = self.executor.submit(util.get_root_files, self.remote_dir, self.root_dir, self.orphaned_dir) root_files = self.executor.submit(util.get_root_files, self.root_dir, self.remote_dir, self.orphaned_dir)
# Get an updated list of torrents # Get an updated list of torrents
logger.print_line("Locating orphan files", self.config.loglevel) logger.print_line("Locating orphan files", self.config.loglevel)

View file

@ -108,12 +108,6 @@ class RemoveUnregistered:
msg_up = trk.msg.upper() msg_up = trk.msg.upper()
msg = trk.msg msg = trk.msg
if TrackerStatus(trk.status) == TrackerStatus.NOT_WORKING: if TrackerStatus(trk.status) == TrackerStatus.NOT_WORKING:
# Tag any error torrents
if self.cfg_tag_error and self.tag_error not in check_tags:
if not list_in_text(msg_up, TorrentMessages.IGNORE_MSGS) and not list_in_text(
msg_up, TorrentMessages.UNREGISTERED_MSGS
):
self.tag_tracker_error(msg, tracker, torrent)
# Check for unregistered torrents # Check for unregistered torrents
if self.cfg_rem_unregistered: if self.cfg_rem_unregistered:
if list_in_text(msg_up, TorrentMessages.UNREGISTERED_MSGS) and not list_in_text( if list_in_text(msg_up, TorrentMessages.UNREGISTERED_MSGS) and not list_in_text(
@ -125,6 +119,9 @@ class RemoveUnregistered:
if self.check_for_unregistered_torrents_using_bhd_api(tracker, msg_up, torrent.hash): if self.check_for_unregistered_torrents_using_bhd_api(tracker, msg_up, torrent.hash):
self.del_unregistered(msg, tracker, torrent) self.del_unregistered(msg, tracker, torrent)
break break
# Tag any error torrents
if self.cfg_tag_error and self.tag_error not in check_tags:
self.tag_tracker_error(msg, tracker, torrent)
except NotFound404Error: except NotFound404Error:
continue continue

View file

@ -2,10 +2,14 @@ import os
from datetime import timedelta from datetime import timedelta
from modules import util from modules import util
from modules.util import is_tag_in_torrent
from modules.webhooks import GROUP_NOTIFICATION_LIMIT from modules.webhooks import GROUP_NOTIFICATION_LIMIT
logger = util.logger logger = util.logger
MIN_SEEDING_TIME_TAG = "MinSeedTimeNotReached"
MIN_NUM_SEEDS_TAG = "MinSeedsNotMet"
class ShareLimits: class ShareLimits:
def __init__(self, qbit_manager): def __init__(self, qbit_manager):
@ -179,7 +183,9 @@ class ShareLimits:
group_config["limit_upload_speed"] = -1 group_config["limit_upload_speed"] = -1
check_limit_upload_speed = group_config["limit_upload_speed"] != torrent_upload_limit check_limit_upload_speed = group_config["limit_upload_speed"] != torrent_upload_limit
hash_not_prev_checked = t_hash not in self.torrent_hash_checked hash_not_prev_checked = t_hash not in self.torrent_hash_checked
share_limits_not_yet_tagged = True if self.group_tag and self.group_tag not in torrent.tags else False share_limits_not_yet_tagged = (
True if self.group_tag and not is_tag_in_torrent(self.group_tag, torrent.tags) else False
)
logger.trace(f"Torrent: {t_name} [Hash: {t_hash}]") logger.trace(f"Torrent: {t_name} [Hash: {t_hash}]")
logger.trace(f"Torrent Category: {torrent.category}") logger.trace(f"Torrent Category: {torrent.category}")
logger.trace(f"Torrent Tags: {torrent.tags}") logger.trace(f"Torrent Tags: {torrent.tags}")
@ -201,7 +207,9 @@ class ShareLimits:
if ( if (
check_max_ratio or check_max_seeding_time or check_limit_upload_speed or share_limits_not_yet_tagged check_max_ratio or check_max_seeding_time or check_limit_upload_speed or share_limits_not_yet_tagged
) and hash_not_prev_checked: ) and hash_not_prev_checked:
if "MinSeedTimeNotReached" not in torrent.tags: if not is_tag_in_torrent(MIN_SEEDING_TIME_TAG, torrent.tags) and not is_tag_in_torrent(
MIN_NUM_SEEDS_TAG, torrent.tags
):
logger.print_line(logger.insert_space(f"Torrent Name: {t_name}", 3), self.config.loglevel) logger.print_line(logger.insert_space(f"Torrent Name: {t_name}", 3), self.config.loglevel)
logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), self.config.loglevel) logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), self.config.loglevel)
if self.group_tag: if self.group_tag:
@ -217,6 +225,7 @@ class ShareLimits:
max_ratio=group_config["max_ratio"], max_ratio=group_config["max_ratio"],
max_seeding_time=group_config["max_seeding_time"], max_seeding_time=group_config["max_seeding_time"],
min_seeding_time=group_config["min_seeding_time"], min_seeding_time=group_config["min_seeding_time"],
min_num_seeds=group_config["min_num_seeds"],
resume_torrent=group_config["resume_torrent_after_change"], resume_torrent=group_config["resume_torrent_after_change"],
tracker=tracker["url"], tracker=tracker["url"],
) )
@ -232,10 +241,9 @@ class ShareLimits:
"""Removes previous share limits tag, updates tag and share limits for a torrent, and resumes the torrent""" """Removes previous share limits tag, updates tag and share limits for a torrent, and resumes the torrent"""
# Remove previous share_limits tag # Remove previous share_limits tag
if not self.config.dry_run: if not self.config.dry_run:
tags = util.get_list(torrent.tags) tag = is_tag_in_torrent(self.share_limits_tag, torrent.tags, exact=False)
for tag in tags: if tag:
if self.share_limits_tag in tag: torrent.remove_tags(tag)
torrent.remove_tags(tag)
# Will tag the torrent with the group name if add_group_to_tag is True and set the share limits # Will tag the torrent with the group name if add_group_to_tag is True and set the share limits
self.set_tags_and_limits( self.set_tags_and_limits(
@ -345,7 +353,7 @@ class ShareLimits:
body.append(msg) body.append(msg)
# Update Torrents # Update Torrents
if not self.config.dry_run: if not self.config.dry_run:
if tags and tags not in torrent.tags: if tags and not is_tag_in_torrent(tags, torrent.tags):
torrent.add_tags(tags) torrent.add_tags(tags)
torrent_upload_limit = -1 if round(torrent.up_limit / 1024) == 0 else round(torrent.up_limit / 1024) torrent_upload_limit = -1 if round(torrent.up_limit / 1024) == 0 else round(torrent.up_limit / 1024)
if limit_upload_speed is not None and limit_upload_speed != torrent_upload_limit: if limit_upload_speed is not None and limit_upload_speed != torrent_upload_limit:
@ -357,24 +365,28 @@ class ShareLimits:
max_ratio = torrent.max_ratio max_ratio = torrent.max_ratio
if max_seeding_time is None: if max_seeding_time is None:
max_seeding_time = torrent.max_seeding_time max_seeding_time = torrent.max_seeding_time
if "MinSeedTimeNotReached" in torrent.tags: if is_tag_in_torrent(MIN_SEEDING_TIME_TAG, torrent.tags):
return []
if is_tag_in_torrent(MIN_NUM_SEEDS_TAG, torrent.tags):
return [] return []
torrent.set_share_limits(max_ratio, max_seeding_time) torrent.set_share_limits(max_ratio, max_seeding_time)
return body return body
def has_reached_seed_limit(self, torrent, max_ratio, max_seeding_time, min_seeding_time, resume_torrent, tracker): def has_reached_seed_limit(
self, torrent, max_ratio, max_seeding_time, min_seeding_time, min_num_seeds, resume_torrent, tracker
):
"""Check if torrent has reached seed limit""" """Check if torrent has reached seed limit"""
body = "" body = ""
def _has_reached_min_seeding_time_limit(): def _has_reached_min_seeding_time_limit():
print_log = [] print_log = []
if torrent.seeding_time >= min_seeding_time * 60: if torrent.seeding_time >= min_seeding_time * 60:
if "MinSeedTimeNotReached" in torrent.tags: if is_tag_in_torrent(MIN_SEEDING_TIME_TAG, torrent.tags):
if not self.config.dry_run: if not self.config.dry_run:
torrent.remove_tags(tags="MinSeedTimeNotReached") torrent.remove_tags(tags=MIN_SEEDING_TIME_TAG)
return True return True
else: else:
if "MinSeedTimeNotReached" not in torrent.tags: if not is_tag_in_torrent(MIN_SEEDING_TIME_TAG, torrent.tags):
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"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"Tracker: {tracker}", 8), self.config.loglevel)
print_log += logger.print_line( print_log += logger.print_line(
@ -389,15 +401,47 @@ class ShareLimits:
self.config.loglevel, self.config.loglevel,
) )
print_log += logger.print_line( print_log += logger.print_line(
logger.insert_space("Adding Tag: MinSeedTimeNotReached", 8), self.config.loglevel logger.insert_space(f"Adding Tag: {MIN_SEEDING_TIME_TAG}", 8), self.config.loglevel
) )
if not self.config.dry_run: if not self.config.dry_run:
torrent.add_tags("MinSeedTimeNotReached") torrent.add_tags(MIN_SEEDING_TIME_TAG)
torrent.set_share_limits(-1, -1) torrent.set_share_limits(-1, -1)
if resume_torrent: if resume_torrent:
torrent.resume() torrent.resume()
return False return False
def _is_less_than_min_num_seeds():
print_log = []
if min_num_seeds == 0 or torrent.num_complete >= min_num_seeds:
if is_tag_in_torrent(MIN_NUM_SEEDS_TAG, torrent.tags):
if not self.config.dry_run:
torrent.remove_tags(tags=MIN_NUM_SEEDS_TAG)
return False
else:
if not is_tag_in_torrent(MIN_NUM_SEEDS_TAG, torrent.tags):
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 number of seeds not met: Total Seeds ({torrent.num_complete}) <"
f"min_num_seeds({min_num_seeds}). Removing Share Limits so qBittorrent can continue"
" seeding."
),
8,
),
self.config.loglevel,
)
print_log += logger.print_line(
logger.insert_space(f"Adding Tag: {MIN_NUM_SEEDS_TAG}", 8), self.config.loglevel
)
if not self.config.dry_run:
torrent.add_tags(MIN_NUM_SEEDS_TAG)
torrent.set_share_limits(-1, -1)
if resume_torrent:
torrent.resume()
return True
def _has_reached_seeding_time_limit(): def _has_reached_seeding_time_limit():
nonlocal body nonlocal body
seeding_time_limit = None seeding_time_limit = None
@ -421,6 +465,9 @@ class ShareLimits:
return True return True
return False return False
if min_num_seeds is not None:
if _is_less_than_min_num_seeds():
return body
if max_ratio is not None: if max_ratio is not None:
if max_ratio >= 0: if max_ratio >= 0:
if torrent.ratio >= max_ratio and _has_reached_min_seeding_time_limit(): if torrent.ratio >= max_ratio and _has_reached_min_seeding_time_limit():

View file

@ -57,7 +57,7 @@ class TagNoHardLinks:
Checks for any previous torrents that were tagged with the nohardlinks tag and have since had hardlinks added. Checks for any previous torrents that were tagged with the nohardlinks tag and have since had hardlinks added.
If any are found, the nohardlinks tag is removed If any are found, the nohardlinks tag is removed
""" """
if not (has_nohardlinks) and (self.nohardlinks_tag in torrent.tags): if not (has_nohardlinks) and (util.is_tag_in_torrent(self.nohardlinks_tag, torrent.tags)):
self.stats_untagged += 1 self.stats_untagged += 1
body = [] body = []
body += logger.print_line( body += logger.print_line(
@ -102,7 +102,7 @@ class TagNoHardLinks:
has_nohardlinks = check_hardlinks.nohardlink( has_nohardlinks = check_hardlinks.nohardlink(
torrent["content_path"].replace(self.root_dir, self.remote_dir), self.config.notify torrent["content_path"].replace(self.root_dir, self.remote_dir), self.config.notify
) )
if any(tag in torrent.tags for tag in nohardlinks[category]["exclude_tags"]): if any(util.is_tag_in_torrent(tag, 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 # Skip to the next torrent if we find any torrents that are in the exclude tag
continue continue
else: else:
@ -111,7 +111,7 @@ class TagNoHardLinks:
if has_nohardlinks: if has_nohardlinks:
tracker = self.qbt.get_tags(torrent.trackers) tracker = self.qbt.get_tags(torrent.trackers)
# Will only tag new torrents that don't have nohardlinks_tag tag # Will only tag new torrents that don't have nohardlinks_tag tag
if self.nohardlinks_tag not in torrent.tags: if not util.is_tag_in_torrent(self.nohardlinks_tag, torrent.tags):
self.add_tag_no_hl( self.add_tag_no_hl(
torrent=torrent, torrent=torrent,
tracker=tracker, tracker=tracker,

View file

@ -36,7 +36,13 @@ class Qbt:
logger.debug(f"Host: {self.host}, Username: {self.username}, Password: {self.password}") logger.debug(f"Host: {self.host}, Username: {self.username}, Password: {self.password}")
ex = "" ex = ""
try: try:
self.client = Client(host=self.host, username=self.username, password=self.password, VERIFY_WEBUI_CERTIFICATE=False) self.client = Client(
host=self.host,
username=self.username,
password=self.password,
VERIFY_WEBUI_CERTIFICATE=False,
REQUESTS_ARGS={"timeout": (45, 60)},
)
self.client.auth_log_in() self.client.auth_log_in()
self.current_version = self.client.app.version self.current_version = self.client.app.version
logger.debug(f"qBittorrent: {self.current_version}") logger.debug(f"qBittorrent: {self.current_version}")

View file

@ -34,6 +34,18 @@ def get_list(data, lower=False, split=True, int_list=False):
return [d.strip() for d in str(data).split(",")] return [d.strip() for d in str(data).split(",")]
def is_tag_in_torrent(check_tag, torrent_tags, exact=True):
"""Check if tag is in torrent_tags"""
tags = get_list(torrent_tags)
if exact:
return check_tag in tags
else:
for t in tags:
if check_tag in t:
return t
return False
class TorrentMessages: class TorrentMessages:
"""Contains list of messages to check against a status of a torrent""" """Contains list of messages to check against a status of a torrent"""

View file

@ -370,6 +370,8 @@ with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "VERSION")) a
version = util.parse_version(line) version = util.parse_version(line)
break break
branch = util.guess_branch(version, env_version, git_branch) branch = util.guess_branch(version, env_version, git_branch)
if branch is None:
branch = "Unknown"
version = (version[0].replace("develop", branch), version[1].replace("develop", branch), version[2]) version = (version[0].replace("develop", branch), version[1].replace("develop", branch), version[2])

View file

@ -1,7 +1,7 @@
bencodepy==0.9.5 bencodepy==0.9.5
GitPython==3.1.31 GitPython==3.1.31
qbittorrent-api==2023.6.49 qbittorrent-api==2023.6.50
requests==2.31.0 requests==2.31.0
retrying==1.3.4 retrying==1.3.4
ruamel.yaml==0.17.31 ruamel.yaml==0.17.32
schedule==1.2.0 schedule==1.2.0