* 4.1.5-develop1

* Fixes #552

* Fix max_seeding_time limit to 1 year (525600 minutes) due to qbt limits

* Adds [FR]: Option to completely customize tag name. #551

* --- (#556)

updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Fixes #557

* Adds #483

* Fixes #558

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
bobokun 2024-05-24 20:39:18 -04:00 committed by GitHub
parent 01ed3fe7ee
commit b5e6bb395a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 134 additions and 46 deletions

View file

@ -1,11 +1,15 @@
# Requirements Updated
- requests==2.32.2
# New Updates
- Adds additional remove unregistered logic for Blutopia
- Customize tag names in share limits (Adds [#551](https://github.com/StuffAnThings/qbit_manage/issues/551))
- Force category updates for all torrents (Adds [#483](https://github.com/StuffAnThings/qbit_manage/issues/483))
# Bug Fixes
- Fixes [#545](https://github.com/StuffAnThings/qbit_manage/issues/545)
- Fixes [#546](https://github.com/StuffAnThings/qbit_manage/issues/546)
- Fixes [#548](https://github.com/StuffAnThings/qbit_manage/issues/548)
- Fixes [#550](https://github.com/StuffAnThings/qbit_manage/issues/550)
- Optimizes Remove Empty Directories and remove empty folders in orphaned_data
- Fixes [#552](https://github.com/StuffAnThings/qbit_manage/issues/552)
- Fixes [#557](https://github.com/StuffAnThings/qbit_manage/issues/557)
- Fixes [#558](https://github.com/StuffAnThings/qbit_manage/issues/558)
**Full Changelog**: https://github.com/StuffAnThings/qbit_manage/compare/v4.1.3...v4.1.4
**Full Changelog**: https://github.com/StuffAnThings/qbit_manage/compare/v4.1.4...v4.1.5

View file

@ -1 +1 @@
4.1.4
4.1.5

View file

@ -186,7 +186,7 @@ share_limits:
# <OPTIONAL> max_seeding_time <str>: Will set the torrent Maximum seeding time until torrent is stopped from seeding/uploading and may be cleaned up / removed if the minimums have been met.
# See Some examples of valid time expressions (https://github.com/onegreyonewhite/pytimeparse2)
# 32m, 2h32m, 3d2h32m, 1w3d2h32m
# 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 value of 1 year (525600 minutes))
max_seeding_time: 90d
# <OPTIONAL> min_seeding_time <str>: Will prevent torrent deletion by cleanup variable if torrent has not yet minimum seeding time (minutes).
# This should only be set if you are using this in conjunction with max_seeding_time and max_ratio. If you are not setting a max_ratio, then use max_seeding_time instead.
@ -216,6 +216,8 @@ share_limits:
# 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
# <OPTIONAL> custom_tag <str>: Apply a custom tag name for this particular group. **WARNING (This tag MUST be unique as it will be used to determine share limits. Please ensure it does not overlap with any other tags in qbt)**
custom_tag: sharelimits_noHL
cross-seed:
priority: 2
include_all_tags:

View file

@ -161,7 +161,7 @@ Control how torrent share limits are set depending on the priority of your group
| `categories` | Filter by including one or more categories. Multiple categories are checked with an **OR** condition. Since one torrent can only be associated with a single category, multiple categories are checked with an **OR** condition | None | list | <center></center> |
| `cleanup` | **WARNING!!** Setting this as true will remove and delete contents of any torrents that satisfies the share limits **(max time OR max ratio)** It will also delete the torrent's data if and only if no other torrents are using the same folder/files. | False | bool | <center></center> |
| `max_ratio` | Will set the torrent Maximum share ratio until torrent is stopped from seeding/uploading and may be cleaned up / removed if the minimums have been met. (`-2` : Global Limit , `-1` : No Limit) | -1 | float | <center></center> |
| `max_seeding_time` | Will set the torrent Maximum seeding time until torrent is stopped from seeding/uploading and may be cleaned up / removed if the minimums have been met. (`-2` : Global Limit , `-1` : No Limit) See Some examples of [valid time expressions](https://github.com/onegreyonewhite/pytimeparse2?tab=readme-ov-file#pytimeparse2-time-expression-parser) 32m, 2h32m, 3d2h32m, 1w3d2h32m | -1 | str | <center></center> |
| `max_seeding_time` | Will set the torrent Maximum seeding time until torrent is stopped from seeding/uploading and may be cleaned up / removed if the minimums have been met. (`-2` : Global Limit , `-1` : No Limit) (Max value of 1 year (525600 minutes)) See Some examples of [valid time expressions](https://github.com/onegreyonewhite/pytimeparse2?tab=readme-ov-file#pytimeparse2-time-expression-parser) 32m, 2h32m, 3d2h32m, 1w3d2h32m | -1 | str | <center></center> |
| `min_seeding_time` | Will prevent torrent deletion by the cleanup variable if the torrent has reached the `max_ratio` limit you have set. 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. See Some examples of [valid time expressions](https://github.com/onegreyonewhite/pytimeparse2?tab=readme-ov-file#pytimeparse2-time-expression-parser) 32m, 2h32m, 3d2h32m, 1w3d2h32m. **MANDATORY: Must use also `max_ratio` with a value greater than `0` (default: `-1`) for this to work.** If you use both `min_seed_time` and `max_seed_time`, then you must set the value of `max_seed_time` to a number greater than `min_seed_time`. | 0 | str | <center></center> |
| `last_active` |Will prevent torrent deletion by cleanup variable if torrent has been active within the last x minutes. If the torrent has been active within the last x minutes, it will change the share limits back to no limits and resume the torrent to continue seeding. See Some examples of [valid time expressions](https://github.com/onegreyonewhite/pytimeparse2?tab=readme-ov-file#pytimeparse2-time-expression-parser) 32m, 2h32m, 3d2h32m, 1w3d2h32m | 0 | str | <center></center> |
| `limit_upload_speed` | Will limit the upload speed KiB/s (KiloBytes/second) (`-1` : No Limit) | -1 | int | <center></center> |

View file

@ -16,7 +16,19 @@ class Apprise:
self.api_url = params["api_url"]
logger.secret(self.api_url)
self.notify_url = ",".join(params["notify_url"])
response = self.config.get(self.api_url)
response = self.check_api_url()
time.sleep(1) # Pause for 1 second before sending the next request
if response.status_code != 200:
raise Failed(f"Apprise Error: Unable to connect to Apprise using {self.api_url}")
def check_api_url(self):
"""Check if the API URL is valid"""
# Check the response using application.json get header
status_endpoint = self.api_url + "/status"
response = self.config.get(status_endpoint, headers={"Accept": "application/json"})
if response.status_code != 200:
raise Failed(
f"Apprise Error: Unable to connect to Apprise using {status_endpoint} "
f"with status code {response.status_code}: {response.reason}"
)
return response

View file

@ -192,6 +192,7 @@ class Config:
self.tracker_error_tag = self.settings["tracker_error_tag"]
self.nohardlinks_tag = self.settings["nohardlinks_tag"]
self.share_limits_tag = self.settings["share_limits_tag"]
self.share_limits_custom_tags = []
self.share_limits_min_seeding_time_tag = self.settings["share_limits_min_seeding_time_tag"]
self.share_limits_min_num_seeds_tag = self.settings["share_limits_min_num_seeds_tag"]
self.share_limits_last_active_tag = self.settings["share_limits_last_active_tag"]
@ -204,6 +205,7 @@ class Config:
self.share_limits_min_seeding_time_tag,
self.share_limits_min_num_seeds_tag,
self.share_limits_last_active_tag,
self.share_limits_tag,
]
# "Migrate settings from v4.0.0 to v4.0.1 and beyond. Convert 'share_limits_suffix_tag' to 'share_limits_tag'"
if "share_limits_suffix_tag" in self.data["settings"]:
@ -522,6 +524,28 @@ class Config:
do_print=False,
save=False,
)
self.share_limits[group]["custom_tag"] = self.util.check_for_attribute(
self.data,
"custom_tag",
parent="share_limits",
subparent=group,
default_is_none=True,
do_print=False,
save=False,
)
if self.share_limits[group]["custom_tag"]:
if (
self.share_limits[group]["custom_tag"] not in self.share_limits_custom_tags
and self.share_limits[group]["custom_tag"] not in self.default_ignore_tags
):
self.share_limits_custom_tags.append(self.share_limits[group]["custom_tag"])
else:
err = (
f"Config Error: Duplicate custom tag '{self.share_limits[group]['custom_tag']}' "
f"found in share_limits for the grouping '{group}'. Custom tag must be a unique value."
)
self.notify(err, "Config")
raise Failed(err)
self.share_limits[group]["torrents"] = []
if (
self.share_limits[group]["min_seeding_time"] > 0
@ -542,6 +566,13 @@ class Config:
)
self.notify(err, "Config")
raise Failed(err)
if self.share_limits[group]["max_seeding_time"] > 525600:
err = (
f"Config Error: max_seeding_time ({self.share_limits[group]['max_seeding_time']}) cannot be set > 1 year "
f"(525600 minutes) in qbitorrent. Please adjust the max_seeding_time for the grouping '{group}'."
)
self.notify(err, "Config")
raise Failed(err)
else:
if self.commands["share_limits"]:
err = "Config Error: share_limits. No valid grouping found."

View file

@ -22,21 +22,20 @@ class Category:
def category(self):
"""Update category for torrents that don't have any category defined and returns total number categories updated"""
logger.separator("Updating Categories", space=False, border=False)
torrent_list = self.qbt.get_torrents({"category": "", "status_filter": self.status_filter})
torrent_list = self.qbt.get_torrents({"status_filter": self.status_filter})
for torrent in torrent_list:
new_cat = self.get_tracker_cat(torrent) or self.qbt.get_category(torrent.save_path)
if new_cat == self.uncategorized_mapping:
torrent_category = torrent.category
new_cat = []
new_cat.extend(self.get_tracker_cat(torrent) or self.qbt.get_category(torrent.save_path))
if new_cat[0] == self.uncategorized_mapping:
logger.print_line(f"{torrent.name} remains uncategorized.", self.config.loglevel)
continue
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": self.status_filter})
for torrent in torrent_list:
new_cat = self.config.cat_change[old_cat]
self.update_cat(torrent, new_cat, True)
if torrent_category not in new_cat:
self.update_cat(torrent, new_cat[0], False)
# Change categories
if self.config.cat_change and torrent_category in self.config.cat_change:
updated_cat = self.config.cat_change[torrent_category]
self.update_cat(torrent, updated_cat, True)
if self.stats >= 1:
logger.print_line(

View file

@ -47,7 +47,7 @@ class CrossSeed:
# Get the exact torrent match name from self.qbt.torrentinfo
t_name = next(iter(torrentdict_file))
dest = os.path.join(self.qbt.torrentinfo[t_name]["save_path"], "")
category = self.qbt.torrentinfo[t_name].get("Category", self.qbt.get_category(dest))
category = self.qbt.torrentinfo[t_name].get("Category", self.qbt.get_category(dest)[0])
# Only add cross-seed torrent if original torrent is complete
if self.qbt.torrentinfo[t_name]["is_complete"]:
categories.append(category)

View file

@ -28,6 +28,7 @@ class ShareLimits:
self.torrents_updated = [] # list of torrents that have been updated
self.torrent_hash_checked = [] # list of torrent hashes that have been checked for share limits
self.share_limits_tag = qbit_manager.config.share_limits_tag # tag for share limits
self.share_limits_custom_tags = qbit_manager.config.share_limits_custom_tags # All possible custom share limits tags
self.min_seeding_time_tag = qbit_manager.config.share_limits_min_seeding_time_tag # tag for min seeding time
self.min_num_seeds_tag = qbit_manager.config.share_limits_min_num_seeds_tag # tag for min num seeds
self.last_active_tag = qbit_manager.config.share_limits_last_active_tag # tag for last active
@ -175,9 +176,13 @@ class ShareLimits:
for torrent in torrents:
t_name = torrent.name
t_hash = torrent.hash
self.group_tag = (
f"{self.share_limits_tag}_{group_config['priority']}.{group_name}" if group_config["add_group_to_tag"] else None
)
if group_config["add_group_to_tag"]:
if group_config["custom_tag"]:
self.group_tag = group_config["custom_tag"]
else:
self.group_tag = f"{self.share_limits_tag}_{group_config['priority']}.{group_name}"
else:
self.group_tag = None
tracker = self.qbt.get_tags(self.qbt.get_tracker_urls(torrent.trackers))
check_max_ratio = group_config["max_ratio"] != torrent.max_ratio
check_max_seeding_time = group_config["max_seeding_time"] != torrent.max_seeding_time
@ -195,12 +200,34 @@ class ShareLimits:
group_config["limit_upload_speed"] = round(group_upload_speed / len(torrents))
check_limit_upload_speed = group_config["limit_upload_speed"] != torrent_upload_limit
hash_not_prev_checked = t_hash not in self.torrent_hash_checked
share_limits_not_yet_tagged = (
True if self.group_tag and not is_tag_in_torrent(self.group_tag, torrent.tags) else False
)
check_multiple_share_limits_tag = (
self.group_tag and len(is_tag_in_torrent(self.share_limits_tag, torrent.tags, exact=False)) > 1
)
if self.group_tag:
if group_config["custom_tag"] and not is_tag_in_torrent(self.group_tag, torrent.tags):
share_limits_not_yet_tagged = True
elif not group_config["custom_tag"] and not is_tag_in_torrent(self.group_tag, torrent.tags, exact=False):
share_limits_not_yet_tagged = True
else:
share_limits_not_yet_tagged = False
check_multiple_share_limits_tag = False # Default assume no multiple share limits tag
# Check if any of the previous share limits custom tags are there
for custom_tag in self.share_limits_custom_tags:
if custom_tag != self.group_tag and is_tag_in_torrent(custom_tag, torrent.tags):
check_multiple_share_limits_tag = True
break
# Check if there are any other share limits tags in the torrent
if group_config["custom_tag"] and len(is_tag_in_torrent(self.share_limits_tag, torrent.tags, exact=False)) > 0:
check_multiple_share_limits_tag = True
elif (
not group_config["custom_tag"]
and len(is_tag_in_torrent(self.share_limits_tag, torrent.tags, exact=False)) > 1
):
check_multiple_share_limits_tag = True
else:
share_limits_not_yet_tagged = False
check_multiple_share_limits_tag = False
logger.trace(f"Torrent: {t_name} [Hash: {t_hash}]")
logger.trace(f"Torrent Category: {torrent.category}")
logger.trace(f"Torrent Tags: {torrent.tags}")
@ -287,6 +314,10 @@ class ShareLimits:
tag = is_tag_in_torrent(self.share_limits_tag, torrent.tags, exact=False)
if tag:
torrent.remove_tags(tag)
# Check if any of the previous share limits custom tags are there
for custom_tag in self.share_limits_custom_tags:
if is_tag_in_torrent(custom_tag, torrent.tags):
torrent.remove_tags(custom_tag)
# 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(

View file

@ -10,7 +10,6 @@ class Tags:
self.client = qbit_manager.client
self.stats = 0
self.share_limits_tag = qbit_manager.config.share_limits_tag # suffix tag for share limits
self.default_ignore_tags = qbit_manager.config.default_ignore_tags # default ignore tags
self.torrents_updated = [] # List of torrents updated
self.notify_attr = [] # List of single torrent attributes to send to notifiarr
@ -35,7 +34,7 @@ class Tags:
body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), self.config.loglevel)
if not self.config.dry_run:
torrent.add_tags(tracker["tag"])
category = self.qbt.get_category(torrent.save_path) if torrent.category == "" else torrent.category
category = self.qbt.get_category(torrent.save_path)[0] if torrent.category == "" else torrent.category
attr = {
"function": "tag_update",
"title": "Updating Tags",

View file

@ -95,6 +95,9 @@ class Qbt:
self.torrentinfo = None
self.torrentissue = None
self.torrentvalid = None
self.get_tags = cache(self.get_tags)
self.get_category = cache(self.get_category)
self.get_category_save_paths = cache(self.get_category_save_paths)
def get_torrent_info(self):
"""
@ -266,7 +269,6 @@ class Qbt:
"""Get tracker urls from torrent"""
return tuple(x.url for x in trackers if x.url.startswith(("http", "udp", "ws")))
@cache
def get_tags(self, urls):
"""Get tags from config file based on keyword"""
urls = list(urls)
@ -363,21 +365,19 @@ class Qbt:
logger.warning(e)
return tracker
@cache
def get_category(self, path):
"""Get category from config file based on path provided"""
category = ""
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
category.append(cat)
if not category:
default_cat = path.split(os.sep)[-2]
category = str(default_cat)
category = [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}"
@ -385,7 +385,6 @@ class Qbt:
logger.warning(e)
return category
@cache
def get_category_save_paths(self):
"""Get all categories from qbitorrenta and return a list of save_paths"""
save_paths = set()

View file

@ -381,7 +381,18 @@ class Failed(Exception):
def list_in_text(text, search_list, match_all=False):
"""Check if a list of strings is in a string"""
"""
Check if elements from a search list are present in a given text.
Args:
text (str): The text to search in.
search_list (list or set): The list of elements to search for in the text.
match_all (bool, optional): If True, all elements in the search list must be present in the text.
If False, at least one element must be present. Defaults to False.
Returns:
bool: True if the search list elements are found in the text, False otherwise.
"""
if isinstance(search_list, list):
search_list = set(search_list)
contains = {x for x in search_list if " " in x}

View file

@ -9,7 +9,7 @@ import sys
import time
from datetime import datetime
from datetime import timedelta
from functools import cache
from functools import lru_cache
try:
import schedule
@ -238,7 +238,7 @@ def get_arg(env_str, default, arg_bool=False, arg_int=False):
return default
@cache
@lru_cache(maxsize=1)
def is_valid_cron_syntax(cron_expression):
try:
croniter(str(cron_expression))

View file

@ -4,7 +4,7 @@ GitPython==3.1.43
humanize==4.9.0
pytimeparse2==1.7.1
qbittorrent-api==2024.3.60
requests==2.32.0
requests==2.32.2
retrying==1.3.4
ruamel.yaml==0.18.6
schedule==1.2.1