mirror of
https://github.com/StuffAnThings/qbit_manage.git
synced 2024-09-20 07:16:04 +08:00
4.1.5 (#559)
* 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:
parent
01ed3fe7ee
commit
b5e6bb395a
18
CHANGELOG
18
CHANGELOG
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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> |
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue