From 6ec6cff115b4dd636af72623ba8e31533b6c3781 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sun, 28 May 2023 16:49:21 -0400 Subject: [PATCH 01/40] fixes trace logging --- VERSION | 2 +- modules/logs.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 0f44168a..70253387 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.6.4 +3.6.4-develop1 diff --git a/modules/logs.py b/modules/logs.py index ef007436..b425544e 100755 --- a/modules/logs.py +++ b/modules/logs.py @@ -17,7 +17,7 @@ DRYRUN = 25 INFO = 20 DEBUG = 10 -TRACE = 0 +TRACE = 1 def fmt_filter(record): From 7176e7d3c86363e3847fd7b854d52095ca6abc24 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sun, 28 May 2023 18:00:38 -0400 Subject: [PATCH 02/40] updates docstring for check_for_attribute --- modules/util.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/modules/util.py b/modules/util.py index c145d732..97a47962 100755 --- a/modules/util.py +++ b/modules/util.py @@ -153,7 +153,31 @@ def check_for_attribute( save=True, make_dirs=False, ): - """Check for attribute in config.""" + """ + Check for attribute in config. + + Args: + data (dict): The configuration data to search. + attribute (str): The name of the attribute key to search for. + parent (str, optional): The name of the top level attribute to search under. Defaults to None. + subparent (str, optional): The name of the second level attribute to search under. Defaults to None. + test_list (dict, optional): A dictionary of valid values for the attribute. Defaults to None. + default (any, optional): The default value to use if the attribute is not found. Defaults to None. + do_print (bool, optional): Whether to print warning messages. Defaults to True. + default_is_none (bool, optional): Whether to treat a None value as a valid default. Defaults to False. + req_default (bool, optional): Whether to raise an error if no default value is provided. Defaults to False. + var_type (str, optional): The expected type of the attribute value. Defaults to "str". + min_int (int, optional): The minimum value for an integer attribute. Defaults to 0. + throw (bool, optional): Whether to raise an error if the attribute value is invalid. Defaults to False. + save (bool, optional): Whether to save the default value to the config if it is used. Defaults to True. + make_dirs (bool, optional): Whether to create directories for path attributes if they do not exist. Defaults to False. + + Returns: + any: The value of the attribute, or the default value if it is not found. + + Raises: + Failed: If the attribute value is invalid or a required default value is missing. + """ endline = "" if parent is not None: if subparent is not None: From 3210676062cc7fce4dcb2cbf51b4e5254ff187b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 May 2023 01:32:46 +0000 Subject: [PATCH 03/40] Bump ruamel-yaml from 0.17.27 to 0.17.28 Bumps [ruamel-yaml](https://sourceforge.net/p/ruamel-yaml/code/ci/default/tree) from 0.17.27 to 0.17.28. --- updated-dependencies: - dependency-name: ruamel-yaml dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 44d66f72..9edacd3d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,5 @@ pre-commit==3.3.2 qbittorrent-api==2023.4.47 requests==2.31.0 retrying==1.3.4 -ruamel.yaml==0.17.27 +ruamel.yaml==0.17.28 schedule==1.2.0 From 4edc31cdef5720240f123256bffa29dc26e78ab9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 May 2023 00:59:03 +0000 Subject: [PATCH 04/40] Bump qbittorrent-api from 2023.4.47 to 2023.5.48 Bumps [qbittorrent-api](https://github.com/rmartin16/qbittorrent-api) from 2023.4.47 to 2023.5.48. - [Release notes](https://github.com/rmartin16/qbittorrent-api/releases) - [Changelog](https://github.com/rmartin16/qbittorrent-api/blob/main/CHANGELOG.md) - [Commits](https://github.com/rmartin16/qbittorrent-api/compare/v2023.4.47...v2023.5.48) --- updated-dependencies: - dependency-name: qbittorrent-api dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9edacd3d..43cb8e23 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ bencodepy==0.9.5 flake8==6.0.0 GitPython==3.1.31 pre-commit==3.3.2 -qbittorrent-api==2023.4.47 +qbittorrent-api==2023.5.48 requests==2.31.0 retrying==1.3.4 ruamel.yaml==0.17.28 From b27c1c9245b03de43e1a8c9c0b87e886d02c093c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 May 2023 00:59:53 +0000 Subject: [PATCH 05/40] Bump ruamel-yaml from 0.17.28 to 0.17.30 Bumps [ruamel-yaml](https://sourceforge.net/p/ruamel-yaml/code/ci/default/tree) from 0.17.28 to 0.17.30. --- updated-dependencies: - dependency-name: ruamel-yaml dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 43cb8e23..3b2a5214 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,5 @@ pre-commit==3.3.2 qbittorrent-api==2023.5.48 requests==2.31.0 retrying==1.3.4 -ruamel.yaml==0.17.28 +ruamel.yaml==0.17.30 schedule==1.2.0 From fbf9cb59e9c56b1419541ecf1d4d42a63c53bc67 Mon Sep 17 00:00:00 2001 From: bobokun Date: Tue, 30 May 2023 21:26:54 -0400 Subject: [PATCH 06/40] Adds new command share_limits to update share limits based on tags/categories specified per group (Closes #88, Closes #306, Closes #259, Closes #308, Closes #137) --- CHANGELOG | 14 +- VERSION | 2 +- config/config.yml.sample | 106 +++++---- modules/apprise.py | 3 + modules/config.py | 160 ++++++++++++- modules/core/share_limits.py | 399 ++++++++++++++++++++++++++++++++ modules/core/tag_nohardlinks.py | 268 ++------------------- modules/core/tags.py | 18 +- modules/qbittorrent.py | 248 +------------------- qbit_manage.py | 23 +- 10 files changed, 677 insertions(+), 564 deletions(-) create mode 100644 modules/core/share_limits.py diff --git a/CHANGELOG b/CHANGELOG index 0d60abca..91b3f3b5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,8 +1,16 @@ # Requirements Updated -- Updates ruamel.yaml to 0.17.27 +- Updates ruamel.yaml to 0.17.30 +- Updates qbitorrent-api to 2023.5.48 + +# New Features +- Adds new command `share_limits`, `--share-limits` , `QBT_SHARE_LIMITS=True` to update share limits based on tags/categories specified per group (Closes #88, Closes #306, Closes #259, Closes #308, Closes #137) +- Adds new command `skip_qb_version_check`, `--skip-qb-version-check`, `QBT_SKIP_QB_VERSION_CHECK` to bypass qbitorrent compatibility check (unsupported - Thanks to @ftc2 #307) +# Breaking Changes +- `tag_nohardlinks` only updates/removes `noHL` tag. It does not modify or cleanup share_limits anymore. +- `tag_update` only adds tracker tags to torrent. It does not modify or cleanup share_limits anymore. +- Please remove any references to share_limits from your configuration in the tracker/nohardlinks section # Bug Fixes - Fixes #302 -- Adds a way to bypass qbt version check (unsupported - Thanks to @ftc2 #307) -**Full Changelog**: https://github.com/StuffAnThings/qbit_manage/compare/v3.6.3...v3.6.4 +**Full Changelog**: https://github.com/StuffAnThings/qbit_manage/compare/v3.6.3...v3.7.0 diff --git a/VERSION b/VERSION index 70253387..edd02d8d 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.6.4-develop1 +3.6.4-develop2 diff --git a/config/config.yml.sample b/config/config.yml.sample index 4eca5630..66d33cbe 100755 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -14,6 +14,8 @@ commands: tag_tracker_error: False rem_orphaned: False tag_nohardlinks: False + share_limits: False + skip_qb_version_check: False skip_cleanup: False qbt: @@ -26,6 +28,7 @@ settings: force_auto_tmm: False # Will force qBittorrent to enable Automatic Torrent Management for each torrent. tracker_error_tag: issue # Will set the tag of any torrents that do not have a working tracker. nohardlinks_tag: noHL # Will set the tag of any torrents with no hardlinks. + share_limits_suffix_tag: share_limit # Will add this suffix to the grouping separated by '.' to the tag of any torrents with share limits. ignoreTags_OnUpdate: # When running tag-update function, it will update torrent tags for a given torrent even if the torrent has at least one or more of the tags defined here. Otherwise torrents will not be tagged if tags exist. - noHL - issue @@ -68,14 +71,6 @@ tracker: # : # This is the keyword in the tracker url # Set tag name. Can be a list of tags or a single tag # tag: - # Will set the torrent Maximum share ratio until torrent is stopped from seeding/uploading. -2 means the global limit should be used, -1 means no limit. - # max_ratio: 5.0 - # Will set the torrent Maximum seeding time (min) until torrent is stopped from seeding. -2 means the global limit should be used, -1 means no limit. - # max_seeding_time: 129600 - # Will ensure that noHL torrents from this tracker are not deleted by cleanup variable if torrent has not yet met the minimum seeding time (min). - # min_seeding_time: 2000 - # Will limit the upload speed KiB/s (KiloBytes/second) (-1 means no limit) - # limit_upload_speed: 150 # Set this to the notifiarr react name. This is used to add indexer reactions to the notifications sent by Notifiarr # notifiarr: animebytes.tv: @@ -86,10 +81,6 @@ tracker: - Avistaz - tag2 - tag3 - max_ratio: 5.0 - max_seeding_time: 129600 - min_seeding_time: 30400 - limit_upload_speed: 150 notifiarr: avistaz beyond-hd: tag: [Beyond-HD, tag2, tag3] @@ -101,14 +92,11 @@ tracker: tag: CartoonChaos digitalcore: tag: DigitalCore - max_ratio: 5.0 notifiarr: digitalcore gazellegames: tag: GGn - limit_upload_speed: 150 hdts: tag: HDTorrents - max_seeding_time: 129600 landof.tv: tag: BroadcasTheNet notifiarr: broadcasthenet @@ -145,48 +133,66 @@ nohardlinks: - Beyond-HD - AnimeBytes - MaM - # cleanup var: WARNING!! Setting this as true Will remove and delete contents of any torrents that have a noHL tag and meets share limits - cleanup: false - # max_ratio var: Will set the torrent Maximum share ratio until torrent is stopped from seeding/uploading. - # Delete this key from a category's config to use the tracker's configured max_ratio. Will default to -1 if not specified for the category or tracker. - # Uses the larger value of the noHL Category or Tracker specific setting. - max_ratio: 4.0 - # max seeding time var: Will set the torrent Maximum seeding time (min) until torrent is stopped from seeding. - # Delete this key from a category's config to use the tracker's configured max_seeding_time. Will default to -1 if not specified for the category or tracker. - # Uses the larger value of the noHL Category or Tracker specific setting. - max_seeding_time: 86400 - # Limit Upload Speed var: Will limit the upload speed KiB/s (KiloBytes/second) (`-1` : No Limit) - limit_upload_speed: - # min seeding time var: Will prevent torrent deletion by cleanup variable if torrent has not yet minimum seeding time (min). - # Delete this key from a category's config to use the tracker's configured min_seeding_time. Will default to 0 if not specified for the category or tracker. - # Uses the larger value of the noHL Category or Tracker specific setting. - min_seeding_time: 43200 - # resume_torrent_after_untagging_noHL var: If a torrent was previously tagged as NoHL and now has hardlinks, this variable will resume your torrent after changing share limits - resume_torrent_after_untagging_noHL: false # Can have additional categories set with separate ratio/seeding times defined. series-completed: # exclude_tags var: Will exclude torrents with any of the following tags when searching through the category. exclude_tags: - Beyond-HD - BroadcasTheNet - # cleanup var: WARNING!! Setting this as true Will remove and delete contents of any torrents that have a noHL tag and meets share limits - cleanup: false - # max_ratio var: Will set the torrent Maximum share ratio until torrent is stopped from seeding/uploading. - # Delete this key from a category's config to use the tracker's configured max_ratio. Will default to -1 if not specified for the category or tracker. - # Uses the larger value of the noHL Category or Tracker specific setting. - max_ratio: 4.0 - # max seeding time var: Will set the torrent Maximum seeding time (min) until torrent is stopped from seeding. - # Delete this key from a category's config to use the tracker's configured max_seeding_time. Will default to -1 if not specified for the category or tracker. - # Uses the larger value of the noHL Category or Tracker specific setting. - max_seeding_time: 86400 - # Limit Upload Speed var: Will limit the upload speed KiB/s (KiloBytes/second) (`-1` : No Limit) - limit_upload_speed: - # min seeding time var: Will prevent torrent deletion by cleanup variable if torrent has not yet minimum seeding time (min). - # Delete this key from a category's config to use the tracker's configured min_seeding_time. Will default to 0 if not specified for the category or tracker. - # Uses the larger value of the noHL Category or Tracker specific setting. + +share_limits: +# Control how torrent share limits are set depending on the priority of your grouping +# This variable is mandatory and is a text defining the name of your grouping. This can be any string you want + noHL: + # priority: # This is the priority of your grouping. The lower the number the higher the priority + priority: 1 + # tags: # Filter the group based on one or more tags. Multiple tags are checked with an AND condition + tags: + - noHL + # exclude_tags: # Filter by excluding one or more tags. Multiple exclude_tags are checked with an AND condition + # This is useful to combine with the category filter to exclude one or more tags from an entire category + exclude_tags: + - Beyond-HD + # categories: # Filter by excluding one or more categories. Multiple exclude_tags 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 + categories: + - RadarrComplete + - SonarrComplete + # max_ratio : Will set the torrent Maximum share ratio until torrent is stopped from seeding/uploading. + # Delete this key from a category's config to use the tracker's configured max_ratio. Will default to -1 if not specified for the group. + max_ratio: 5.0 + # max_seeding_time : Will set the torrent Maximum seeding time (minutes) until torrent is stopped from seeding. + # Delete this key from a category's config to use the tracker's configured max_seeding_time. Will default to -1 if not specified for the group. + max_seeding_time: 129600 + # min_seeding_time : Will prevent torrent deletion by cleanup variable if torrent has not yet minimum seeding time (minutes). + # Delete this key from a category's config to use the tracker's configured min_seeding_time. Will default to 0 if not specified for the group. min_seeding_time: 43200 - # resume_torrent_after_untagging_noHL var: If a torrent was previously tagged as NoHL and now has hardlinks, this variable will resume your torrent after changing share limits - resume_torrent_after_untagging_noHL: false + # Limit Upload Speed : Will limit the upload speed KiB/s (KiloBytes/second) (`-1` : No Limit) + limit_upload_speed: 0 + # cleanup : WARNING!! Setting this as true Will remove and delete contents of any torrents that satisfies the share limits + cleanup: false + # resume_torrent_after_change : This variable will resume your torrent after changing share limits. Default is true + resume_torrent_after_change: true + # add_group_to_tag : This adds your grouping as a tag with a suffix defined in settings . Default is true + # Example: A grouping defined as noHL will have a tag set to noHL.share_limit (if using the default suffix) + add_group_to_tag: true + cross-seed: + priority: 2 + tags: cross-seed + max_seeding_time: 10200 + cleanup: false + PTP: + priority: 3 + tags: + - PassThePopcorn + max_ratio: 2.0 + max_seeding_time: 130000 + cleanup: false + default: + priority: 999 + max_ratio: -1 + max_seeding_time: -1 + cleanup: false recyclebin: # Recycle Bin method of deletion will move files into the recycle bin (Located in /root_dir/.RecycleBin) instead of directly deleting them in qbit diff --git a/modules/apprise.py b/modules/apprise.py index 0b7f59f3..5ae9c0ec 100755 --- a/modules/apprise.py +++ b/modules/apprise.py @@ -1,4 +1,6 @@ """Apprise notification class""" +import time + from modules import util from modules.util import Failed @@ -14,5 +16,6 @@ def __init__(self, config, params): logger.secret(self.api_url) self.notify_url = ",".join(params["notify_url"]) response = self.config.get(self.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}") diff --git a/modules/config.py b/modules/config.py index 0ccb4404..ec7f9d36 100755 --- a/modules/config.py +++ b/modules/config.py @@ -3,6 +3,7 @@ import re import stat import time +from collections import OrderedDict import requests from retrying import retry @@ -28,6 +29,7 @@ "tag_tracker_error", "rem_orphaned", "tag_nohardlinks", + "share_limits", "skip_cleanup", "skip_qb_version_check", "dry_run", @@ -82,6 +84,7 @@ def __init__(self, default_dir, args): logger.debug(f" --tag-tracker-error (QBT_TAG_TRACKER_ERROR): {self.commands['tag_tracker_error']}") logger.debug(f" --rem-orphaned (QBT_REM_ORPHANED): {self.commands['rem_orphaned']}") logger.debug(f" --tag-nohardlinks (QBT_TAG_NOHARDLINKS): {self.commands['tag_nohardlinks']}") + logger.debug(f" --share-limits (QBT_SHARE_LIMITS): {self.commands['share_limits']}") logger.debug(f" --skip-cleanup (QBT_SKIP_CLEANUP): {self.commands['skip_cleanup']}") logger.debug(f" --skip-qb-version-check (QBT_SKIP_QB_VERSION_CHECK): {self.commands['skip_qb_version_check']}") logger.debug(f" --dry-run (QBT_DRY_RUN): {self.commands['dry_run']}") @@ -136,6 +139,9 @@ def hooks(attr): self.data["webhooks"] = temp if "bhd" in self.data: self.data["bhd"] = self.data.pop("bhd") + if "share_limits" in self.data: + self.data["share_limits"] = self.data.pop("share_limits") + self.dry_run = self.commands["dry_run"] self.loglevel = "DRYRUN" if self.dry_run else "INFO" self.session = requests.Session() @@ -148,10 +154,14 @@ def hooks(attr): self.data, "tracker_error_tag", parent="settings", default="issue" ), "nohardlinks_tag": self.util.check_for_attribute(self.data, "nohardlinks_tag", parent="settings", default="noHL"), + "share_limits_suffix_tag": self.util.check_for_attribute( + self.data, "share_limits_suffix_tag", parent="settings", default="share_limit" + ), } self.tracker_error_tag = self.settings["tracker_error_tag"] self.nohardlinks_tag = self.settings["nohardlinks_tag"] + self.share_limits_suffix_tag = "." + self.settings["share_limits_suffix_tag"] default_ignore_tags = [self.nohardlinks_tag, self.tracker_error_tag, "cross-seed"] self.settings["ignoreTags_OnUpdate"] = self.util.check_for_attribute( @@ -167,6 +177,7 @@ def hooks(attr): "tag_tracker_error": None, "rem_orphaned": None, "tag_nohardlinks": None, + "share_limits": None, "cleanup_dirs": None, } @@ -333,7 +344,7 @@ def hooks(attr): var_type="int", min_int=-1, do_print=False, - default=0, + default=-1, save=False, ) self.nohardlinks[cat]["resume_torrent_after_untagging_noHL"] = self.util.check_for_attribute( @@ -357,6 +368,153 @@ def hooks(attr): self.notify(err, "Config") raise Failed(err) + # share limits + self.share_limits = None + if "share_limits" in self.data and self.commands["share_limits"]: + + def _sort_share_limits(share_limits): + sorted_limits = sorted( + share_limits.items(), key=lambda x: x[1].get("priority", float("inf")) if x[1] is not None else float("inf") + ) + priorities = set() + for key, value in sorted_limits: + if value is None: + value = {} + if "priority" in value: + priority = value["priority"] + if priority in priorities: + err = ( + f"Config Error: Duplicate priority '{priority}' found in share_limits " + f"for the grouping '{key}'. Priority must be a unique value and greater than or equal to 1" + ) + self.notify(err, "Config") + raise Failed(err) + else: + priority = max(priorities) + 1 + logger.warning( + f"Priority not defined for the grouping '{key}' in share_limits. " f"Setting priority to {priority}" + ) + value["priority"] = self.util.check_for_attribute( + self.data, + "priority", + parent="share_limits", + subparent=key, + var_type="float", + default=priority, + save=True, + ) + priorities.add(priority) + return OrderedDict(sorted_limits) + + self.share_limits = OrderedDict() + sorted_share_limits = _sort_share_limits(self.data["share_limits"]) + for group in sorted_share_limits: + self.share_limits[group] = {} + self.share_limits[group]["priority"] = sorted_share_limits[group]["priority"] + self.share_limits[group]["tags"] = self.util.check_for_attribute( + self.data, + "tags", + parent="share_limits", + subparent=group, + var_type="list", + default_is_none=True, + do_print=False, + save=False, + ) + self.share_limits[group]["exclude_tags"] = self.util.check_for_attribute( + self.data, + "exclude_tags", + parent="share_limits", + subparent=group, + var_type="list", + default_is_none=True, + do_print=False, + save=False, + ) + self.share_limits[group]["categories"] = self.util.check_for_attribute( + self.data, + "categories", + parent="share_limits", + subparent=group, + var_type="list", + default_is_none=True, + do_print=False, + save=False, + ) + self.share_limits[group]["cleanup"] = self.util.check_for_attribute( + self.data, "cleanup", parent="share_limits", subparent=group, var_type="bool", default=False, do_print=False + ) + self.share_limits[group]["max_ratio"] = self.util.check_for_attribute( + self.data, + "max_ratio", + parent="share_limits", + subparent=group, + var_type="float", + min_int=-2, + default=-1, + do_print=False, + save=False, + ) + self.share_limits[group]["max_seeding_time"] = self.util.check_for_attribute( + self.data, + "max_seeding_time", + parent="share_limits", + subparent=group, + var_type="int", + min_int=-2, + default=-1, + do_print=False, + save=False, + ) + self.share_limits[group]["min_seeding_time"] = self.util.check_for_attribute( + self.data, + "min_seeding_time", + parent="share_limits", + subparent=group, + var_type="int", + min_int=0, + default=0, + do_print=False, + save=False, + ) + self.share_limits[group]["limit_upload_speed"] = self.util.check_for_attribute( + self.data, + "limit_upload_speed", + parent="share_limits", + subparent=group, + var_type="int", + min_int=-1, + default=0, + do_print=False, + save=False, + ) + self.share_limits[group]["resume_torrent_after_change"] = self.util.check_for_attribute( + self.data, + "resume_torrent_after_change", + parent="share_limits", + subparent=group, + var_type="bool", + default=True, + do_print=False, + save=False, + ) + self.share_limits[group]["add_group_to_tag"] = self.util.check_for_attribute( + self.data, + "add_group_to_tag", + parent="share_limits", + subparent=group, + var_type="bool", + default=True, + do_print=False, + save=False, + ) + self.share_limits[group]["torrents"] = [] + else: + if self.commands["share_limits"]: + err = "Config Error: share_limits. No valid grouping found." + self.notify(err, "Config") + raise Failed(err) + # Add RecycleBin self.recyclebin = {} self.recyclebin["enabled"] = self.util.check_for_attribute( diff --git a/modules/core/share_limits.py b/modules/core/share_limits.py new file mode 100644 index 00000000..2e70ace8 --- /dev/null +++ b/modules/core/share_limits.py @@ -0,0 +1,399 @@ +import os +from datetime import timedelta + +from modules import util + +logger = util.logger + + +class ShareLimits: + def __init__(self, qbit_manager): + self.qbt = qbit_manager + self.config = qbit_manager.config + self.client = qbit_manager.client + self.stats_tagged = 0 # counter for the number of share limits changed + self.stats_deleted = 0 # counter for the number of torrents that \ + # meets the criteria for ratio limit/seed limit for deletion + self.stats_deleted_contents = 0 # counter for the number of torrents that \ + # meets the criteria for ratio limit/seed limit for deletion including contents \ + + self.tdel_dict = {} # dictionary to track the torrent names and content path that meet the deletion criteria + self.root_dir = qbit_manager.config.root_dir # root directory of torrents + self.remote_dir = qbit_manager.config.remote_dir # remote directory of torrents + self.share_limits_config = qbit_manager.config.share_limits # configuration of share limits + 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_suffix_tag = qbit_manager.config.share_limits_suffix_tag # suffix tag for share limits + self.group_tag = None # tag for the share limit group + + self.update_share_limits() + + def update_share_limits(self): + """Updates share limits for torrents based on grouping""" + logger.separator("Updating Share Limits based on priority", space=False, border=False) + torrent_list = self.qbt.get_torrents({"status_filter": "completed"}) + self.assign_torrents_to_group(torrent_list) + for group_name, group_config in self.share_limits_config.items(): + torrents = group_config["torrents"] + self.torrents_updated = [] + self.tdel_dict = {} + if torrents: + self.update_share_limits_for_group(group_name, group_config, torrents) + attr = { + "function": "share_limits", + "title": f"Updating Share Limits for {group_name}. Priority {group_config['priority']}", + "body": f"Updated {len(self.torrents_updated)} torrents.", + "grouping": group_name, + "torrent_list": self.torrents_updated, + "torrent_tag": self.group_tag, + "torrent_max_ratio": group_config["max_ratio"], + "torrent_max_seeding_time": group_config["max_seeding_time"], + "torrent_min_seeding_time": group_config["min_seeding_time"], + "torrent_limit_upload_speed": group_config["limit_upload_speed"], + } + self.config.send_notifications(attr) + if group_config["cleanup"] and len(self.tdel_dict) > 0: + self.cleanup_torrents_for_group(group_name, group_config["priority"]) + + def cleanup_torrents_for_group(self, group_name, priority): + """Deletes torrents that have reached the ratio/seed limit""" + logger.separator( + f"Cleaning up torrents that have reached ratio/seed limit for {group_name}. Priority {priority}", + space=False, + border=False, + ) + group_notifications = len(self.tdel_dict) > 10 + t_deleted = set() + t_deleted_and_contents = set() + for torrent_hash, torrent_dict in self.tdel_dict.items(): + torrent = torrent_dict["torrent"] + t_name = torrent.name + t_count = self.qbt.torrentinfo[t_name]["count"] + t_msg = self.qbt.torrentinfo[t_name]["msg"] + t_status = self.qbt.torrentinfo[t_name]["status"] + # Double check that the content path is the same before we delete anything + if torrent["content_path"].replace(self.root_dir, self.remote_dir) == torrent_dict["content_path"]: + tracker = self.qbt.get_tags(torrent.trackers) + 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(torrent_dict["body"], self.config.loglevel) + body += logger.print_line( + logger.insert_space("Cleanup: True [Meets Share Limits]", 8), + self.config.loglevel, + ) + attr = { + "function": "cleanup_share_limits", + "title": "Share limit removal", + "grouping": group_name, + "torrent_name": t_name, + "torrent_category": torrent.category, + "cleanup": "True", + "torrent_tracker": tracker["url"], + "notifiarr_indexer": tracker["notifiarr"], + } + if os.path.exists(torrent["content_path"].replace(self.root_dir, self.remote_dir)): + # Checks if any of the original torrents are working + if t_count > 1 and ("" in t_msg or 2 in t_status): + self.stats_deleted += 1 + attr["torrents_deleted_and_contents"] = False + t_deleted.add(t_name) + if not self.config.dry_run: + self.qbt.tor_delete_recycle(torrent, attr) + body += logger.print_line( + logger.insert_space("Deleted .torrent but NOT content files.", 8), + self.config.loglevel, + ) + else: + self.stats_deleted_contents += 1 + attr["torrents_deleted_and_contents"] = True + t_deleted_and_contents.add(t_name) + if not self.config.dry_run: + self.qbt.tor_delete_recycle(torrent, attr) + body += logger.print_line( + logger.insert_space("Deleted .torrent AND content files.", 8), self.config.loglevel + ) + else: + self.stats_deleted += 1 + attr["torrents_deleted_and_contents"] = False + t_deleted.add(t_name) + if not self.config.dry_run: + self.qbt.tor_delete_recycle(torrent, attr) + body += logger.print_line( + logger.insert_space("Deleted .torrent but NOT content files.", 8), self.config.loglevel + ) + attr["body"] = "\n".join(body) + if not group_notifications: + self.config.send_notifications(attr) + self.qbt.torrentinfo[t_name]["count"] -= 1 + if group_notifications: + if t_deleted: + attr = { + "function": "cleanup_share_limits", + "title": "Share limit removal - Deleted .torrent but NOT content files.", + "body": f"Deleted {self.stats_deleted} .torrents but NOT content files.", + "grouping": group_name, + "torrent_list": list(t_deleted), + "cleanup": True, + "torrents_deleted_and_contents": False, + } + self.config.send_notifications(attr) + if t_deleted_and_contents: + attr = { + "function": "cleanup_share_limits", + "title": "Share limit removal - Deleted .torrent AND content files.", + "body": f"Deleted {self.stats_deleted_contents} .torrents AND content files.", + "grouping": group_name, + "torrent_list": list(t_deleted_and_contents), + "cleanup": True, + "torrents_deleted_and_contents": True, + } + self.config.send_notifications(attr) + + def update_share_limits_for_group(self, group_name, group_config, torrents): + """Updates share limits for torrents in a group""" + logger.separator( + f"Updating Share Limits for [Group {group_name}] [Priority {group_config['priority']}]", space=False, border=False + ) + for torrent in torrents: + t_name = torrent.name + t_hash = torrent.hash + tracker = self.qbt.get_tags(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 + # Treat upload limit as -1 if it is set to 0 (unlimited) + torrent_upload_limit = -1 if torrent.up_limit == 0 else torrent.up_limit + if group_config["limit_upload_speed"] == 0: + group_config["limit_upload_speed"] = -1 + check_limit_upload_speed = group_config["limit_upload_speed"] != torrent_upload_limit + if ( + check_max_ratio or check_max_seeding_time or check_limit_upload_speed + ) and t_hash not in self.torrent_hash_checked: + if "MinSeedTimeNotReached" not in torrent.tags: + self.group_tag = f"{group_name}{self.share_limits_suffix_tag}" if group_config["add_group_to_tag"] else None + 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.trace(f"Torrent Category: {torrent.category}") + logger.trace(f"Torrent Tags: {torrent.tags}") + logger.trace(f"Grouping: {group_name}") + logger.trace(f"Config Max Ratio vs Torrent Max Ratio:{group_config['max_ratio']} vs {torrent.max_ratio}") + logger.trace( + "Config Max Seeding Time vs Torrent Max Seeding Time: " + f"{group_config['max_seeding_time']} vs {torrent.max_seeding_time}" + ) + logger.trace( + "Config Limit Upload Speed vs Torrent Limit Upload Speed: " + f"{group_config['limit_upload_speed']} vs {torrent.up_limit}" + ) + if self.group_tag: + logger.print_line(logger.insert_space(f"Added Tag: {self.group_tag}", 8), self.config.loglevel) + self.tag_and_update_share_limits_for_torrent(torrent, group_config) + self.stats_tagged += 1 + self.torrents_updated.append(t_name) + # Cleanup torrents if the torrent meets the criteria for deletion and cleanup is enabled + if group_config["cleanup"]: + tor_reached_seed_limit = self.has_reached_seed_limit( + torrent=torrent, + max_ratio=group_config["max_ratio"], + max_seeding_time=group_config["max_seeding_time"], + min_seeding_time=group_config["min_seeding_time"], + resume_torrent=group_config["resume_torrent_after_change"], + tracker=tracker["url"], + ) + if tor_reached_seed_limit: + if t_hash not in self.tdel_dict: + self.tdel_dict[t_hash] = {} + self.tdel_dict[t_hash]["torrent"] = torrent + self.tdel_dict[t_hash]["content_path"] = torrent["content_path"].replace(self.root_dir, self.remote_dir) + self.tdel_dict[t_hash]["body"] = tor_reached_seed_limit + else: + self.share_limits_config[group_name]["torrents"].remove(torrent) + self.torrent_hash_checked.append(t_hash) + + def tag_and_update_share_limits_for_torrent(self, torrent, group_config): + """Removes previous share limits tag, updates tag and share limits for a torrent, and resumes the torrent""" + # Remove previous share_limits tag + tags = util.get_list(torrent.tags) + for tag in tags: + if self.share_limits_suffix_tag in tag: + tags.remove(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( + torrent=torrent, + max_ratio=group_config["max_ratio"], + max_seeding_time=group_config["max_seeding_time"], + limit_upload_speed=group_config["limit_upload_speed"], + tags=self.group_tag, + ) + # Resume torrent if it was paused now that the share limit has changed + if torrent.state_enum.is_complete and group_config["resume_torrent_after_change"]: + if not self.config.dry_run: + torrent.resume() + + def assign_torrents_to_group(self, torrent_list): + """Assign torrents to a share limit group based on its tags and category""" + logger.info("Assigning torrents to share limit groups...") + for torrent in torrent_list: + tags = util.get_list(torrent.tags) + category = torrent.category or "" + grouping = self.get_share_limit_group(tags, category) + if grouping: + self.share_limits_config[grouping]["torrents"].append(torrent) + + def get_share_limit_group(self, tags, category): + """Get the share limit group based on the tags and category of the torrent""" + for group_name, group_config in self.share_limits_config.items(): + check_tags = self.check_tags(tags, group_config["tags"], group_config["exclude_tags"]) + check_category = self.check_category(category, group_config["categories"]) + + if check_tags and check_category: + return group_name + return None + + def check_tags(self, tags, include_tags, exclude_tags): + """Check if the torrent has the required tags and does not have the excluded tags""" + if include_tags: + if not set(include_tags).issubset(tags): + return False + if exclude_tags: + if set(exclude_tags).intersection(tags): + return False + return True + + def check_category(self, category, categories): + """Check if the torrent has the required category""" + if categories: + if category not in categories: + return False + return True + + def set_tags_and_limits( + self, torrent, max_ratio, max_seeding_time, limit_upload_speed=None, tags=None, restore=False, do_print=True + ): + """Set tags and limits for a torrent""" + body = [] + if limit_upload_speed: + if limit_upload_speed != -1: + 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: + 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: + 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): + 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): + 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 and tags not in torrent.tags: + torrent.add_tags(tags) + if limit_upload_speed: + if limit_upload_speed == -1: + torrent.set_upload_limit(-1) + else: + torrent.set_upload_limit(limit_upload_speed * 1024) + if not max_ratio: + 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): + """Check if torrent has reached seed limit""" + body = "" + + def _has_reached_min_seeding_time_limit(): + print_log = [] + if torrent.seeding_time >= min_seeding_time * 60: + if "MinSeedTimeNotReached" in torrent.tags: + torrent.remove_tags(tags="MinSeedTimeNotReached") + return True + else: + if "MinSeedTimeNotReached" not in 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 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 diff --git a/modules/core/tag_nohardlinks.py b/modules/core/tag_nohardlinks.py index 97d4495c..317c1eb0 100644 --- a/modules/core/tag_nohardlinks.py +++ b/modules/core/tag_nohardlinks.py @@ -1,5 +1,3 @@ -import os - from modules import util logger = util.logger @@ -12,12 +10,7 @@ def __init__(self, qbit_manager): self.client = qbit_manager.client self.stats_tagged = 0 # counter for the number of torrents that has no hardlinks self.stats_untagged = 0 # counter for number of torrents that previously had no hardlinks but now have hardlinks - self.stats_deleted = 0 # counter for the number of torrents that has no hardlinks and \ - # meets the criteria for ratio limit/seed limit for deletion - self.stats_deleted_contents = 0 # counter for the number of torrents that has no hardlinks and \ - # meets the criteria for ratio limit/seed limit for deletion including contents - self.tdel_dict = {} # dictionary to track the torrent names and content path that meet the deletion criteria self.root_dir = qbit_manager.config.root_dir self.remote_dir = qbit_manager.config.remote_dir self.nohardlinks = qbit_manager.config.nohardlinks @@ -25,110 +18,29 @@ def __init__(self, qbit_manager): self.tag_nohardlinks() - def add_tag_no_hl(self, torrent, tracker, category, max_ratio, max_seeding_time, add_tag=True): + def add_tag_no_hl(self, torrent, tracker, category): """Add tag nohardlinks_tag to torrents with no hardlinks""" body = [] body.append(logger.insert_space(f"Torrent Name: {torrent.name}", 3)) - if add_tag: - body.append(logger.insert_space(f"Added Tag: {self.nohardlinks_tag}", 6)) - title = "Tagging Torrents with No Hardlinks" - else: - title = "Changing Share Ratio of Torrents with No Hardlinks" + body.append(logger.insert_space(f"Added Tag: {self.nohardlinks_tag}", 6)) + title = "Tagging Torrents with No Hardlinks" body.append(logger.insert_space(f'Tracker: {tracker["url"]}', 8)) - body_tags_and_limits = self.qbt.set_tags_and_limits( - torrent, - max_ratio, - max_seeding_time, - self.nohardlinks[category]["limit_upload_speed"], - tags=self.nohardlinks_tag, - do_print=False, - ) - if body_tags_and_limits or add_tag: - self.stats_tagged += 1 - # Resume torrent if it was paused now that the share limit has changed - if torrent.state_enum.is_complete and self.nohardlinks[category]["resume_torrent_after_untagging_noHL"]: - if not self.config.dry_run: - torrent.resume() - body.extend(body_tags_and_limits) - for rcd in body: - logger.print_line(rcd, self.config.loglevel) - attr = { - "function": "tag_nohardlinks", - "title": title, - "body": "\n".join(body), - "torrent_name": torrent.name, - "torrent_category": torrent.category, - "torrent_tag": self.nohardlinks_tag, - "torrent_tracker": tracker["url"], - "notifiarr_indexer": tracker["notifiarr"], - "torrent_max_ratio": max_ratio, - "torrent_max_seeding_time": max_seeding_time, - "torrent_limit_upload_speed": self.nohardlinks[category]["limit_upload_speed"], - } - self.config.send_notifications(attr) - - def cleanup_tagged_torrents_with_no_hardlinks(self, category): - """Delete any tagged torrents that meet noHL criteria""" - # loop through torrent list again for cleanup purposes - if self.nohardlinks[category]["cleanup"]: - torrent_list = self.qbt.get_torrents({"category": category, "status_filter": "completed"}) - for torrent in torrent_list: - t_name = torrent.name - t_hash = torrent.hash - if t_hash in self.tdel_dict and self.nohardlinks_tag in torrent.tags: - t_count = self.qbt.torrentinfo[t_name]["count"] - t_msg = self.qbt.torrentinfo[t_name]["msg"] - t_status = self.qbt.torrentinfo[t_name]["status"] - # Double check that the content path is the same before we delete anything - if torrent["content_path"].replace(self.root_dir, self.remote_dir) == self.tdel_dict[t_hash]["content_path"]: - tracker = self.qbt.get_tags(torrent.trackers) - 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(self.tdel_dict[t_hash]["body"], self.config.loglevel) - body += logger.print_line( - logger.insert_space("Cleanup: True [No hardlinks found and meets Share Limits.]", 8), - self.config.loglevel, - ) - attr = { - "function": "cleanup_tag_nohardlinks", - "title": "Removing NoHL Torrents and meets Share Limits", - "torrent_name": t_name, - "torrent_category": torrent.category, - "cleanup": "True", - "torrent_tracker": tracker["url"], - "notifiarr_indexer": tracker["notifiarr"], - } - if os.path.exists(torrent["content_path"].replace(self.root_dir, self.remote_dir)): - # Checks if any of the original torrents are working - if t_count > 1 and ("" in t_msg or 2 in t_status): - self.stats_deleted += 1 - attr["torrents_deleted_and_contents"] = False - if not self.config.dry_run: - self.qbt.tor_delete_recycle(torrent, attr) - body += logger.print_line( - logger.insert_space("Deleted .torrent but NOT content files.", 8), - self.config.loglevel, - ) - else: - self.stats_deleted_contents += 1 - attr["torrents_deleted_and_contents"] = True - if not self.config.dry_run: - self.qbt.tor_delete_recycle(torrent, attr) - body += logger.print_line( - logger.insert_space("Deleted .torrent AND content files.", 8), self.config.loglevel - ) - else: - self.stats_deleted += 1 - attr["torrents_deleted_and_contents"] = False - if not self.config.dry_run: - self.qbt.tor_delete_recycle(torrent, attr) - body += logger.print_line( - logger.insert_space("Deleted .torrent but NOT content files.", 8), self.config.loglevel - ) - attr["body"] = "\n".join(body) - self.config.send_notifications(attr) - self.qbt.torrentinfo[t_name]["count"] -= 1 + if not self.config.dry_run: + torrent.add_tags(self.nohardlinks_tag) + self.stats_tagged += 1 + for rcd in body: + logger.print_line(rcd, self.config.loglevel) + attr = { + "function": "tag_nohardlinks", + "title": title, + "body": "\n".join(body), + "torrent_name": torrent.name, + "torrent_category": torrent.category, + "torrent_tag": self.nohardlinks_tag, + "torrent_tracker": tracker["url"], + "notifiarr_indexer": tracker["notifiarr"], + } + self.config.send_notifications(attr) def check_previous_nohardlinks_tagged_torrents(self, has_nohardlinks, torrent, tracker, category): """ @@ -145,28 +57,8 @@ def check_previous_nohardlinks_tagged_torrents(self, has_nohardlinks, torrent, t ) body += logger.print_line(logger.insert_space(f"Removed Tag: {self.nohardlinks_tag}", 6), self.config.loglevel) body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), self.config.loglevel) - body += logger.print_line( - f"{'Not Reverting' if self.config.dry_run else 'Reverting'} to tracker or Global share limits.", - self.config.loglevel, - ) - restore_max_ratio = tracker["max_ratio"] - restore_max_seeding_time = tracker["max_seeding_time"] - restore_limit_upload_speed = tracker["limit_upload_speed"] - if restore_max_ratio is None: - restore_max_ratio = -2 - if restore_max_seeding_time is None: - restore_max_seeding_time = -2 - if restore_limit_upload_speed is None: - restore_limit_upload_speed = -1 if not self.config.dry_run: torrent.remove_tags(tags=self.nohardlinks_tag) - body.extend( - self.qbt.set_tags_and_limits( - torrent, restore_max_ratio, restore_max_seeding_time, restore_limit_upload_speed, restore=True - ) - ) - if torrent.state_enum.is_complete and self.nohardlinks[category]["resume_torrent_after_untagging_noHL"]: - torrent.resume() attr = { "function": "untag_nohardlinks", "title": "Untagging Previous Torrents that now have hardlinks", @@ -176,9 +68,6 @@ def check_previous_nohardlinks_tagged_torrents(self, has_nohardlinks, torrent, t "torrent_tag": self.nohardlinks_tag, "torrent_tracker": tracker["url"], "notifiarr_indexer": tracker["notifiarr"], - "torrent_max_ratio": restore_max_ratio, - "torrent_max_seeding_time": restore_max_seeding_time, - "torrent_limit_upload_speed": restore_limit_upload_speed, } self.config.send_notifications(attr) @@ -211,120 +100,17 @@ def tag_nohardlinks(self): # Cleans up previously tagged nohardlinks_tag torrents that no longer have hardlinks if has_nohardlinks: tracker = self.qbt.get_tags(torrent.trackers) - # Determine min_seeding_time. - # If only tracker setting is set, use tracker's min_seeding_time - # If only nohardlinks category setting is set, use nohardlinks category's min_seeding_time - # If both tracker and nohardlinks category setting is set, use the larger of the two - # If neither set, use 0 (no limit) - min_seeding_time = 0 - logger.trace(f'tracker["min_seeding_time"] is {tracker["min_seeding_time"]}') - logger.trace(f'nohardlinks[category]["min_seeding_time"] is {nohardlinks[category]["min_seeding_time"]}') - if tracker["min_seeding_time"] is not None and nohardlinks[category]["min_seeding_time"] is not None: - if tracker["min_seeding_time"] >= nohardlinks[category]["min_seeding_time"]: - min_seeding_time = tracker["min_seeding_time"] - logger.trace(f'Using tracker["min_seeding_time"] {min_seeding_time}') - else: - min_seeding_time = nohardlinks[category]["min_seeding_time"] - logger.trace(f'Using nohardlinks[category]["min_seeding_time"] {min_seeding_time}') - elif nohardlinks[category]["min_seeding_time"]: - min_seeding_time = nohardlinks[category]["min_seeding_time"] - logger.trace(f'Using nohardlinks[category]["min_seeding_time"] {min_seeding_time}') - elif tracker["min_seeding_time"]: - min_seeding_time = tracker["min_seeding_time"] - logger.trace(f'Using tracker["min_seeding_time"] {min_seeding_time}') - else: - logger.trace(f"Using default min_seeding_time {min_seeding_time}") - # Determine max_ratio. - # If only tracker setting is set, use tracker's max_ratio - # If only nohardlinks category setting is set, use nohardlinks category's max_ratio - # If both tracker and nohardlinks category setting is set, use the larger of the two - # If neither set, use -1 (no limit) - max_ratio = -1 - logger.trace(f'tracker["max_ratio"] is {tracker["max_ratio"]}') - logger.trace(f'nohardlinks[category]["max_ratio"] is {nohardlinks[category]["max_ratio"]}') - if tracker["max_ratio"] is not None and nohardlinks[category]["max_ratio"] is not None: - if tracker["max_ratio"] >= nohardlinks[category]["max_ratio"]: - max_ratio = tracker["max_ratio"] - logger.trace(f'Using (tracker["max_ratio"]) {max_ratio}') - else: - max_ratio = nohardlinks[category]["max_ratio"] - logger.trace(f'Using (nohardlinks[category]["max_ratio"]) {max_ratio}') - elif nohardlinks[category]["max_ratio"]: - max_ratio = nohardlinks[category]["max_ratio"] - logger.trace(f'Using (nohardlinks[category]["max_ratio"]) {max_ratio}') - elif tracker["max_ratio"]: - max_ratio = tracker["max_ratio"] - logger.trace(f'Using (tracker["max_ratio"]) {max_ratio}') - else: - logger.trace(f"Using default (max_ratio) {max_ratio}") - # Determine max_seeding_time. - # If only tracker setting is set, use tracker's max_seeding_time - # If only nohardlinks category setting is set, use nohardlinks category's max_seeding_time - # If both tracker and nohardlinks category setting is set, use the larger of the two - # If neither set, use -1 (no limit) - max_seeding_time = -1 - logger.trace(f'tracker["max_seeding_time"] is {tracker["max_seeding_time"]}') - logger.trace(f'nohardlinks[category]["max_seeding_time"] is {nohardlinks[category]["max_seeding_time"]}') - if tracker["max_seeding_time"] is not None and nohardlinks[category]["max_seeding_time"] is not None: - if tracker["max_seeding_time"] >= nohardlinks[category]["max_seeding_time"]: - max_seeding_time = tracker["max_seeding_time"] - logger.trace(f'Using (tracker["max_seeding_time"]) {max_seeding_time}') - else: - max_seeding_time = nohardlinks[category]["max_seeding_time"] - logger.trace(f'Using (nohardlinks[category]["max_seeding_time"]) {max_seeding_time}') - elif nohardlinks[category]["max_seeding_time"]: - max_seeding_time = nohardlinks[category]["max_seeding_time"] - logger.trace(f'Using (nohardlinks[category]["max_seeding_time"]) {max_seeding_time}') - elif tracker["max_seeding_time"]: - max_seeding_time = tracker["max_seeding_time"] - logger.trace(f'Using (tracker["max_seeding_time"]) {max_seeding_time}') - else: - logger.trace(f"Using default (max_seeding_time) {max_seeding_time}") # Will only tag new torrents that don't have nohardlinks_tag tag if self.nohardlinks_tag not in torrent.tags: self.add_tag_no_hl( torrent=torrent, tracker=tracker, category=category, - max_ratio=max_ratio, - max_seeding_time=max_seeding_time, - add_tag=True, - ) - - # 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.qbt.has_reached_seed_limit( - torrent, - max_ratio, - max_seeding_time, - min_seeding_time, - nohardlinks[category]["resume_torrent_after_untagging_noHL"], - tracker["url"], ) - if tor_reach_seed_limit: - if torrent.hash not in self.tdel_dict: - self.tdel_dict[torrent.hash] = {} - self.tdel_dict[torrent.hash]["content_path"] = torrent["content_path"].replace( - self.root_dir, self.remote_dir - ) - self.tdel_dict[torrent.hash]["body"] = tor_reach_seed_limit - else: - # Updates torrent to see if "MinSeedTimeNotReached" tag has been added - torrent = self.qbt.get_torrents({"torrent_hashes": [torrent.hash]}).data[0] - # Checks to see if previously nohardlinks_tag share limits have changed. - self.add_tag_no_hl( - torrent=torrent, - tracker=tracker, - category=category, - max_ratio=max_ratio, - max_seeding_time=max_seeding_time, - add_tag=False, - ) self.check_previous_nohardlinks_tagged_torrents(has_nohardlinks, torrent, tracker, category) - self.cleanup_tagged_torrents_with_no_hardlinks(category) if self.stats_tagged >= 1: logger.print_line( - f"{'Did not Tag/set' if self.config.dry_run else 'Tag/set'} share limits for {self.stats_tagged} " + f"{'Did not Tag' if self.config.dry_run else 'Added Tag'} for {self.stats_tagged} " f".torrent{'s.' if self.stats_tagged > 1 else '.'}", self.config.loglevel, ) @@ -333,19 +119,7 @@ def tag_nohardlinks(self): if self.stats_untagged >= 1: logger.print_line( f"{'Did not delete' if self.config.dry_run else 'Deleted'} " - f"{self.nohardlinks_tag} tags / share limits for {self.stats_untagged} " + f"{self.nohardlinks_tag} tags for {self.stats_untagged} " f".torrent{'s.' if self.stats_untagged > 1 else '.'}", self.config.loglevel, ) - if self.stats_deleted >= 1: - logger.print_line( - f"{'Did not delete' if self.config.dry_run else 'Deleted'} {self.stats_deleted} " - f".torrent{'s' if self.stats_deleted > 1 else ''} but not content files.", - self.config.loglevel, - ) - if self.stats_deleted_contents >= 1: - logger.print_line( - f"{'Did not delete' if self.config.dry_run else 'Deleted'} {self.stats_deleted_contents} " - f".torrent{'s' if self.stats_deleted_contents > 1 else ''} AND content files.", - self.config.loglevel, - ) diff --git a/modules/core/tags.py b/modules/core/tags.py index 4f67cf17..029b9be8 100644 --- a/modules/core/tags.py +++ b/modules/core/tags.py @@ -9,6 +9,7 @@ def __init__(self, qbit_manager): self.config = qbit_manager.config self.client = qbit_manager.client self.stats = 0 + self.share_limits_suffix_tag = qbit_manager.config.share_limits_suffix_tag # suffix tag for share limits self.tags() @@ -17,7 +18,8 @@ def tags(self): ignore_tags = self.config.settings["ignoreTags_OnUpdate"] logger.separator("Updating Tags", space=False, border=False) for torrent in self.qbt.torrent_list: - check_tags = util.get_list(torrent.tags) + check_tags = [tag for tag in util.get_list(torrent.tags) if self.share_limits_suffix_tag not in tag] + if torrent.tags == "" or (len([trk for trk in check_tags if trk not in ignore_tags]) == 0): tracker = self.qbt.get_tags(torrent.trackers) if tracker["tag"]: @@ -29,15 +31,8 @@ def tags(self): self.config.loglevel, ) body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), self.config.loglevel) - body.extend( - self.qbt.set_tags_and_limits( - torrent, - tracker["max_ratio"], - tracker["max_seeding_time"], - tracker["limit_upload_speed"], - tracker["tag"], - ) - ) + 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 attr = { "function": "tag_update", @@ -48,9 +43,6 @@ def tags(self): "torrent_tag": ", ".join(tracker["tag"]), "torrent_tracker": tracker["url"], "notifiarr_indexer": tracker["notifiarr"], - "torrent_max_ratio": tracker["max_ratio"], - "torrent_max_seeding_time": tracker["max_seeding_time"], - "torrent_limit_upload_speed": tracker["limit_upload_speed"], } self.config.send_notifications(attr) if self.stats >= 1: diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index 7344f89a..37d33858 100755 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -1,7 +1,6 @@ """Qbittorrent Module""" import os import sys -from datetime import timedelta from qbittorrentapi import APIConnectionError from qbittorrentapi import Client @@ -25,7 +24,7 @@ class Qbt: SUPPORTED_VERSION = Version.latest_supported_app_version() MIN_SUPPORTED_VERSION = "v4.3.0" - TORRENT_DICT_COMMANDS = ["recheck", "cross_seed", "rem_unregistered", "tag_tracker_error", "tag_nohardlinks"] + TORRENT_DICT_COMMANDS = ["recheck", "cross_seed", "rem_unregistered", "tag_tracker_error", "tag_nohardlinks", "share_limits"] def __init__(self, config, params): self.config = config @@ -202,142 +201,11 @@ def get_torrents(self, params): """Get torrents from qBittorrent""" return self.client.torrents.info(**params) - def set_tags_and_limits( - self, torrent, max_ratio, max_seeding_time, limit_upload_speed=None, tags=None, restore=False, do_print=True - ): - """Set tags and limits for a torrent""" - body = [] - if limit_upload_speed: - if limit_upload_speed != -1: - 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: - 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: - 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): - 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): - 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: - torrent.add_tags(tags) - if limit_upload_speed: - if limit_upload_speed == -1: - torrent.set_upload_limit(-1) - else: - torrent.set_upload_limit(limit_upload_speed * 1024) - if not max_ratio: - 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): - """Check if torrent has reached seed limit""" - body = "" - - def _has_reached_min_seeding_time_limit(): - print_log = [] - if torrent.seeding_time >= min_seeding_time * 60: - if "MinSeedTimeNotReached" in torrent.tags: - torrent.remove_tags(tags="MinSeedTimeNotReached") - 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 get_tags(self, trackers): """Get tags from config file based on keyword""" urls = [x.url for x in trackers if x.url.startswith("http")] tracker = {} tracker["tag"] = None - tracker["max_ratio"] = None - tracker["min_seeding_time"] = None - tracker["max_seeding_time"] = None - tracker["limit_upload_speed"] = None tracker["notifiarr"] = None tracker["url"] = None tracker_other_tag = self.config.util.check_for_attribute( @@ -376,76 +244,6 @@ def get_tags(self, trackers): self.config.data["tracker"][tag_url]["tag"] = [tag_url] if isinstance(tracker["tag"], str): tracker["tag"] = [tracker["tag"]] - is_max_ratio_defined = self.config.data["tracker"].get("max_ratio") - is_max_seeding_time_defined = self.config.data["tracker"].get("max_seeding_time") - if is_max_ratio_defined or is_max_seeding_time_defined: - tracker["max_ratio"] = self.config.util.check_for_attribute( - self.config.data, - "max_ratio", - parent="tracker", - subparent=tag_url, - var_type="float", - min_int=-2, - do_print=False, - default=-1, - save=False, - ) - tracker["max_seeding_time"] = self.config.util.check_for_attribute( - self.config.data, - "max_seeding_time", - parent="tracker", - subparent=tag_url, - var_type="int", - min_int=-2, - do_print=False, - default=-1, - save=False, - ) - else: - tracker["max_ratio"] = self.config.util.check_for_attribute( - self.config.data, - "max_ratio", - parent="tracker", - subparent=tag_url, - var_type="float", - min_int=-2, - do_print=False, - default_is_none=True, - save=False, - ) - tracker["max_seeding_time"] = self.config.util.check_for_attribute( - self.config.data, - "max_seeding_time", - parent="tracker", - subparent=tag_url, - var_type="int", - min_int=-2, - do_print=False, - default_is_none=True, - save=False, - ) - tracker["min_seeding_time"] = self.config.util.check_for_attribute( - self.config.data, - "min_seeding_time", - parent="tracker", - subparent=tag_url, - var_type="int", - min_int=0, - do_print=False, - default=0, - save=False, - ) - tracker["limit_upload_speed"] = self.config.util.check_for_attribute( - self.config.data, - "limit_upload_speed", - parent="tracker", - subparent=tag_url, - var_type="int", - min_int=-1, - do_print=False, - default=0, - save=False, - ) tracker["notifiarr"] = self.config.util.check_for_attribute( self.config.data, "notifiarr", @@ -458,50 +256,6 @@ def get_tags(self, trackers): return tracker if tracker_other_tag: tracker["tag"] = tracker_other_tag - tracker["max_ratio"] = self.config.util.check_for_attribute( - self.config.data, - "max_ratio", - parent="tracker", - subparent="other", - var_type="float", - min_int=-2, - do_print=False, - default=-1, - save=False, - ) - tracker["min_seeding_time"] = self.config.util.check_for_attribute( - self.config.data, - "min_seeding_time", - parent="tracker", - subparent="other", - var_type="int", - min_int=0, - do_print=False, - default=-1, - save=False, - ) - tracker["max_seeding_time"] = self.config.util.check_for_attribute( - self.config.data, - "max_seeding_time", - parent="tracker", - subparent="other", - var_type="int", - min_int=-2, - do_print=False, - default=-1, - save=False, - ) - tracker["limit_upload_speed"] = self.config.util.check_for_attribute( - self.config.data, - "limit_upload_speed", - parent="tracker", - subparent="other", - var_type="int", - min_int=-1, - do_print=False, - default=0, - save=False, - ) tracker["notifiarr"] = self.config.util.check_for_attribute( self.config.data, "notifiarr", diff --git a/qbit_manage.py b/qbit_manage.py index 851a66a6..aa77768d 100755 --- a/qbit_manage.py +++ b/qbit_manage.py @@ -141,6 +141,16 @@ "When files get upgraded they no longer become linked with your media therefore will be tagged with a new tag noHL. " "You can then safely delete/remove these torrents to free up any extra space that is not being used by your media folder.", ) +parser.add_argument( + "-sl", + "--share-limits", + dest="share_limits", + action="store_true", + default=False, + help="Use this to help apply and manage your torrent share limits based on your tags/categories." + "This can apply a max ratio, seed time limits to your torrents or limit your torrent upload speed as well." + "Share limits are applied in the order of priority specified.", +) parser.add_argument( "-sc", "--skip-cleanup", @@ -237,6 +247,7 @@ def get_arg(env_str, default, arg_bool=False, arg_int=False): tag_tracker_error = get_arg("QBT_TAG_TRACKER_ERROR", args.tag_tracker_error, arg_bool=True) rem_orphaned = get_arg("QBT_REM_ORPHANED", args.rem_orphaned, arg_bool=True) tag_nohardlinks = get_arg("QBT_TAG_NOHARDLINKS", args.tag_nohardlinks, arg_bool=True) +share_limits = get_arg("QBT_SHARE_LIMITS", args.share_limits, arg_bool=True) skip_cleanup = get_arg("QBT_SKIP_CLEANUP", args.skip_cleanup, arg_bool=True) skip_qb_version_check = get_arg("QBT_SKIP_QB_VERSION_CHECK", args.skip_qb_version_check, arg_bool=True) dry_run = get_arg("QBT_DRY_RUN", args.dry_run, arg_bool=True) @@ -285,6 +296,7 @@ def get_arg(env_str, default, arg_bool=False, arg_int=False): "tag_tracker_error", "rem_orphaned", "tag_nohardlinks", + "share_limits", "skip_cleanup", "skip_qb_version_check", "dry_run", @@ -329,6 +341,7 @@ def get_arg(env_str, default, arg_bool=False, arg_int=False): from modules.core.recheck import ReCheck # noqa from modules.core.tag_nohardlinks import TagNoHardLinks # noqa from modules.core.remove_orphaned import RemoveOrphaned # noqa +from modules.core.share_limits import ShareLimits # noqa def my_except_hook(exctype, value, tbi): @@ -458,8 +471,13 @@ def finished_run(): stats["tagged"] += no_hardlinks.stats_tagged stats["tagged_noHL"] += no_hardlinks.stats_tagged stats["untagged_noHL"] += no_hardlinks.stats_untagged - stats["deleted"] += no_hardlinks.stats_deleted - stats["deleted_contents"] += no_hardlinks.stats_deleted_contents + + # Set Share Limits + if cfg.commands["share_limits"]: + share_limits = ShareLimits(qbit_manager) + stats["tagged"] += share_limits.stats_tagged + stats["deleted"] += share_limits.stats_deleted + stats["deleted_contents"] += share_limits.stats_deleted_contents # Remove Orphaned Files if cfg.commands["rem_orphaned"]: @@ -583,6 +601,7 @@ def calc_next_run(schd, write_out=False): logger.debug(f" --tag-tracker-error (QBT_TAG_TRACKER_ERROR): {tag_tracker_error}") logger.debug(f" --rem-orphaned (QBT_REM_ORPHANED): {rem_orphaned}") logger.debug(f" --tag-nohardlinks (QBT_TAG_NOHARDLINKS): {tag_nohardlinks}") + logger.debug(f" --share-limits (QBT_SHARE_LIMITS): {share_limits}") logger.debug(f" --skip-cleanup (QBT_SKIP_CLEANUP): {skip_cleanup}") logger.debug(f" --skip-qb-version-check (QBT_SKIP_QB_VERSION_CHECK): {skip_qb_version_check}") logger.debug(f" --dry-run (QBT_DRY_RUN): {dry_run}") From 0c6ebcd316860821053e69d69d38b960310be432 Mon Sep 17 00:00:00 2001 From: bobokun Date: Tue, 30 May 2023 21:39:49 -0400 Subject: [PATCH 07/40] update format --- config/config.yml.sample | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/config/config.yml.sample b/config/config.yml.sample index 66d33cbe..e44af977 100755 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -25,8 +25,8 @@ qbt: pass: "password" settings: - force_auto_tmm: False # Will force qBittorrent to enable Automatic Torrent Management for each torrent. - tracker_error_tag: issue # Will set the tag of any torrents that do not have a working tracker. + force_auto_tmm: False # Will force qBittorrent to enable Automatic Torrent Management for each torrent. + tracker_error_tag: issue # Will set the tag of any torrents that do not have a working tracker. nohardlinks_tag: noHL # Will set the tag of any torrents with no hardlinks. share_limits_suffix_tag: share_limit # Will add this suffix to the grouping separated by '.' to the tag of any torrents with share limits. ignoreTags_OnUpdate: # When running tag-update function, it will update torrent tags for a given torrent even if the torrent has at least one or more of the tags defined here. Otherwise torrents will not be tagged if tags exist. @@ -64,7 +64,6 @@ cat_change: movies-hd.cross-seed: movies-hd movies-uhd.cross-seed: movies-uhd - tracker: # Mandatory # Tag Parameters @@ -141,23 +140,23 @@ nohardlinks: - BroadcasTheNet share_limits: -# Control how torrent share limits are set depending on the priority of your grouping -# This variable is mandatory and is a text defining the name of your grouping. This can be any string you want + # Control how torrent share limits are set depending on the priority of your grouping + # This variable is mandatory and is a text defining the name of your grouping. This can be any string you want noHL: # priority: # This is the priority of your grouping. The lower the number the higher the priority priority: 1 # tags: # Filter the group based on one or more tags. Multiple tags are checked with an AND condition tags: - - noHL + - noHL # exclude_tags: # Filter by excluding one or more tags. Multiple exclude_tags are checked with an AND condition # This is useful to combine with the category filter to exclude one or more tags from an entire category exclude_tags: - - Beyond-HD + - Beyond-HD # categories: # Filter by excluding one or more categories. Multiple exclude_tags 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 categories: - - RadarrComplete - - SonarrComplete + - RadarrComplete + - SonarrComplete # max_ratio : Will set the torrent Maximum share ratio until torrent is stopped from seeding/uploading. # Delete this key from a category's config to use the tracker's configured max_ratio. Will default to -1 if not specified for the group. max_ratio: 5.0 @@ -184,7 +183,7 @@ share_limits: PTP: priority: 3 tags: - - PassThePopcorn + - PassThePopcorn max_ratio: 2.0 max_seeding_time: 130000 cleanup: false @@ -225,7 +224,7 @@ orphaned: - "**/@eaDir" - "/data/torrents/temp/**" - "**/*.!qB" - - '**/_unpackerred' + - "**/_unpackerred" apprise: # Apprise integration with webhooks From 580351f6d5c7b1b85570db69649cb9ab3639ba91 Mon Sep 17 00:00:00 2001 From: bobokun Date: Tue, 30 May 2023 21:42:57 -0400 Subject: [PATCH 08/40] Adds rate limit to Notifiarr --- modules/notifiarr.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/notifiarr.py b/modules/notifiarr.py index 868f47f6..734c36b0 100755 --- a/modules/notifiarr.py +++ b/modules/notifiarr.py @@ -1,3 +1,4 @@ +import time from json import JSONDecodeError from modules import util @@ -36,4 +37,6 @@ def __init__(self, config, params): def notification(self, json): """Send notification to Notifiarr""" params = {"qbit_client": self.config.data["qbt"]["host"], "instance": self.instance} - return self.config.get(f"{self.url}notification/qbitManage/", json=json, headers=self.header, params=params) + response = self.config.get(f"{self.url}notification/qbitManage/", json=json, headers=self.header, params=params) + time.sleep(1) # Pause for 1 second before sending the next request + return response From 58ecbcfe2e939f86abc5f5d1e01ecf4b671cee38 Mon Sep 17 00:00:00 2001 From: bobokun Date: Tue, 30 May 2023 22:38:46 -0400 Subject: [PATCH 09/40] update nohardlinks config --- config/config.yml.sample | 24 +++++---- modules/config.py | 107 ++++----------------------------------- 2 files changed, 22 insertions(+), 109 deletions(-) diff --git a/config/config.yml.sample b/config/config.yml.sample index e44af977..be587e00 100755 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -126,18 +126,20 @@ nohardlinks: # Tag Movies/Series that are not hard linked outside the root directory # Mandatory to fill out directory parameter above to use this function (root_dir/remote_dir) # This variable should be set to your category name of your completed movies/completed series in qbit. Acceptable variable can be any category you would like to tag if there are no hardlinks found - movies-completed: - # exclude_tags var: Will exclude torrents with any of the following tags when searching through the category. - exclude_tags: - - Beyond-HD - - AnimeBytes - - MaM + - movies-completed-4k + - series-completed-4k + - movies-completed: + # exclude_tags var: Will exclude torrents with any of the following tags when searching through the category. + exclude_tags: + - Beyond-HD + - AnimeBytes + - MaM # Can have additional categories set with separate ratio/seeding times defined. - series-completed: - # exclude_tags var: Will exclude torrents with any of the following tags when searching through the category. - exclude_tags: - - Beyond-HD - - BroadcasTheNet + - series-completed: + # exclude_tags var: Will exclude torrents with any of the following tags when searching through the category. + exclude_tags: + - Beyond-HD + - BroadcasTheNet share_limits: # Control how torrent share limits are set depending on the priority of your grouping diff --git a/modules/config.py b/modules/config.py index ec7f9d36..6d532f9d 100755 --- a/modules/config.py +++ b/modules/config.py @@ -263,105 +263,16 @@ def hooks(attr): if "nohardlinks" in self.data and self.commands["tag_nohardlinks"]: self.nohardlinks = {} for cat in self.data["nohardlinks"]: - if cat in list(self.data["cat"].keys()): - is_max_ratio_defined = self.data["nohardlinks"][cat].get("max_ratio") - is_max_seeding_time_defined = self.data["nohardlinks"][cat].get("max_seeding_time") + if isinstance(cat, dict): + cat_str = list(cat.keys())[0] + self.nohardlinks[cat_str] = {} + exclude_tags = cat[cat_str].get("exclude_tags", None) + if isinstance(exclude_tags, str): + exclude_tags = [exclude_tags] + self.nohardlinks[cat_str]["exclude_tags"] = exclude_tags + elif isinstance(cat, str): self.nohardlinks[cat] = {} - self.nohardlinks[cat]["exclude_tags"] = self.util.check_for_attribute( - self.data, - "exclude_tags", - parent="nohardlinks", - subparent=cat, - var_type="list", - default_is_none=True, - do_print=False, - ) - self.nohardlinks[cat]["cleanup"] = self.util.check_for_attribute( - self.data, "cleanup", parent="nohardlinks", subparent=cat, var_type="bool", default=False, do_print=False - ) - if is_max_ratio_defined or is_max_seeding_time_defined: - self.nohardlinks[cat]["max_ratio"] = self.util.check_for_attribute( - self.data, - "max_ratio", - parent="nohardlinks", - subparent=cat, - var_type="float", - min_int=-2, - do_print=False, - default=-1, - save=False, - ) - self.nohardlinks[cat]["max_seeding_time"] = self.util.check_for_attribute( - self.data, - "max_seeding_time", - parent="nohardlinks", - subparent=cat, - var_type="int", - min_int=-2, - do_print=False, - default=-1, - save=False, - ) - else: - self.nohardlinks[cat]["max_ratio"] = self.util.check_for_attribute( - self.data, - "max_ratio", - parent="nohardlinks", - subparent=cat, - var_type="float", - min_int=-2, - do_print=False, - default_is_none=True, - save=False, - ) - self.nohardlinks[cat]["max_seeding_time"] = self.util.check_for_attribute( - self.data, - "max_seeding_time", - parent="nohardlinks", - subparent=cat, - var_type="int", - min_int=-2, - do_print=False, - default_is_none=True, - save=False, - ) - self.nohardlinks[cat]["min_seeding_time"] = self.util.check_for_attribute( - self.data, - "min_seeding_time", - parent="nohardlinks", - subparent=cat, - var_type="int", - min_int=0, - do_print=False, - default=0, - save=False, - ) - self.nohardlinks[cat]["limit_upload_speed"] = self.util.check_for_attribute( - self.data, - "limit_upload_speed", - parent="nohardlinks", - subparent=cat, - var_type="int", - min_int=-1, - do_print=False, - default=-1, - save=False, - ) - self.nohardlinks[cat]["resume_torrent_after_untagging_noHL"] = self.util.check_for_attribute( - self.data, - "resume_torrent_after_untagging_noHL", - parent="nohardlinks", - subparent=cat, - var_type="bool", - default=True, - do_print=False, - save=False, - ) - else: - err = f"Config Error: Category {cat} is defined under nohardlinks attribute " - "but is not defined in the cat attribute." - self.notify(err, "Config") - raise Failed(err) + self.nohardlinks[cat]["exclude_tags"] = None else: if self.commands["tag_nohardlinks"]: err = "Config Error: nohardlinks attribute max_ratio not found" From 8e842761fc27f4fe4355fb6f5a7245f00c981089 Mon Sep 17 00:00:00 2001 From: bobokun Date: Tue, 30 May 2023 23:01:18 -0400 Subject: [PATCH 10/40] fix minor bug in nohardlink config --- VERSION | 2 +- modules/config.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/VERSION b/VERSION index edd02d8d..ace855be 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.6.4-develop2 +3.6.4-develop3 diff --git a/modules/config.py b/modules/config.py index 6d532f9d..ab2b6c51 100755 --- a/modules/config.py +++ b/modules/config.py @@ -260,22 +260,22 @@ def hooks(attr): # nohardlinks self.nohardlinks = None - if "nohardlinks" in self.data and self.commands["tag_nohardlinks"]: + if "nohardlinks" in self.data and self.commands["tag_nohardlinks"] and self.data["nohardlinks"] is not None: self.nohardlinks = {} for cat in self.data["nohardlinks"]: if isinstance(cat, dict): cat_str = list(cat.keys())[0] self.nohardlinks[cat_str] = {} - exclude_tags = cat[cat_str].get("exclude_tags", None) + exclude_tags = cat[cat_str].get("exclude_tags", []) if isinstance(exclude_tags, str): exclude_tags = [exclude_tags] self.nohardlinks[cat_str]["exclude_tags"] = exclude_tags elif isinstance(cat, str): self.nohardlinks[cat] = {} - self.nohardlinks[cat]["exclude_tags"] = None + self.nohardlinks[cat]["exclude_tags"] = [] else: if self.commands["tag_nohardlinks"]: - err = "Config Error: nohardlinks attribute max_ratio not found" + err = "Config Error: nohardlinks must be a list of categories" self.notify(err, "Config") raise Failed(err) From c341088adcd99bb58e5b586a392e50c9141d3573 Mon Sep 17 00:00:00 2001 From: bobokun Date: Wed, 31 May 2023 08:08:26 -0400 Subject: [PATCH 11/40] updates to config.yml.sample --- config/config.yml.sample | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/config/config.yml.sample b/config/config.yml.sample index be587e00..9427f9a2 100755 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -41,7 +41,6 @@ directory: # Must be set if you're running qbit_manage locally and qBittorrent/cross_seed is in a docker # recycle_bin var: # Path of the RecycleBin folder. Default location is set to remote_dir/.RecycleBin # torrents_dir var: # Path of the your qbittorrent torrents directory. Required for `save_torrents` attribute in recyclebin - cross_seed: "/your/path/here/" root_dir: "/data/torrents/" remote_dir: "/mnt/user/data/torrents/" @@ -150,11 +149,11 @@ share_limits: # tags: # Filter the group based on one or more tags. Multiple tags are checked with an AND condition tags: - noHL - # exclude_tags: # Filter by excluding one or more tags. Multiple exclude_tags are checked with an AND condition + # exclude_tags: # Filter by excluding one or more tags. Multiple exclude_tags are checked with an OR condition # This is useful to combine with the category filter to exclude one or more tags from an entire category exclude_tags: - Beyond-HD - # categories: # Filter by excluding one or more categories. Multiple exclude_tags are checked with an OR condition + # categories: # Filter by excluding 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 categories: - RadarrComplete From cf3cfc44702b64a3561dcacfb9e5cb2c964cf2bd Mon Sep 17 00:00:00 2001 From: bobokun Date: Wed, 31 May 2023 17:01:40 -0400 Subject: [PATCH 12/40] Updates include/exclude tags to be more flexible --- VERSION | 2 +- config/config.yml.sample | 19 +++++++++++++++---- modules/config.py | 28 ++++++++++++++++++++++++---- modules/core/share_limits.py | 27 ++++++++++++++++++++------- 4 files changed, 60 insertions(+), 16 deletions(-) diff --git a/VERSION b/VERSION index ace855be..03c481d7 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.6.4-develop3 +3.6.4-develop4 diff --git a/config/config.yml.sample b/config/config.yml.sample index 9427f9a2..2c500365 100755 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -146,12 +146,23 @@ share_limits: noHL: # priority: # This is the priority of your grouping. The lower the number the higher the priority priority: 1 - # tags: # Filter the group based on one or more tags. Multiple tags are checked with an AND condition - tags: + # include_all_tags: # Filter the group based on one or more tags. Multiple include_all_tags are checked with an AND condition + # All tags defined here must be present in the torrent for it to be included in this group + include_all_tags: + - noHL + # include_any_tags: # Filter the group based on one or more tags. Multiple include_any_tags are checked with an OR condition + # Any tags defined here must be present in the torrent for it to be included in this group + include_any_tags: - noHL - # exclude_tags: # Filter by excluding one or more tags. Multiple exclude_tags are checked with an OR condition + # exclude_all_tags: # Filter by excluding one or more tags. Multiple exclude_all_tags are checked with an AND condition + # This is useful to combine with the category filter to exclude one or more tags from an entire category + # All tags defined here must be present in the torrent for it to be excluded in this group + exclude_all_tags: + - Beyond-HD + # exclude_any_tags: # Filter by excluding one or more tags. Multiple exclude_any_tags are checked with an OR condition # This is useful to combine with the category filter to exclude one or more tags from an entire category - exclude_tags: + # Any tags defined here must be present in the torrent for it to be excluded in this group + exclude_any_tags: - Beyond-HD # categories: # Filter by excluding 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 diff --git a/modules/config.py b/modules/config.py index ab2b6c51..ecad9539 100755 --- a/modules/config.py +++ b/modules/config.py @@ -322,9 +322,9 @@ def _sort_share_limits(share_limits): for group in sorted_share_limits: self.share_limits[group] = {} self.share_limits[group]["priority"] = sorted_share_limits[group]["priority"] - self.share_limits[group]["tags"] = self.util.check_for_attribute( + self.share_limits[group]["include_all_tags"] = self.util.check_for_attribute( self.data, - "tags", + "include_all_tags", parent="share_limits", subparent=group, var_type="list", @@ -332,9 +332,29 @@ def _sort_share_limits(share_limits): do_print=False, save=False, ) - self.share_limits[group]["exclude_tags"] = self.util.check_for_attribute( + self.share_limits[group]["include_any_tags"] = self.util.check_for_attribute( self.data, - "exclude_tags", + "include_any_tags", + parent="share_limits", + subparent=group, + var_type="list", + default_is_none=True, + do_print=False, + save=False, + ) + self.share_limits[group]["exclude_all_tags"] = self.util.check_for_attribute( + self.data, + "exclude_all_tags", + parent="share_limits", + subparent=group, + var_type="list", + default_is_none=True, + do_print=False, + save=False, + ) + self.share_limits[group]["exclude_any_tags"] = self.util.check_for_attribute( + self.data, + "exclude_any_tags", parent="share_limits", subparent=group, var_type="list", diff --git a/modules/core/share_limits.py b/modules/core/share_limits.py index 2e70ace8..b67239bb 100644 --- a/modules/core/share_limits.py +++ b/modules/core/share_limits.py @@ -244,20 +244,33 @@ def assign_torrents_to_group(self, torrent_list): def get_share_limit_group(self, tags, category): """Get the share limit group based on the tags and category of the torrent""" for group_name, group_config in self.share_limits_config.items(): - check_tags = self.check_tags(tags, group_config["tags"], group_config["exclude_tags"]) + check_tags = self.check_tags( + tags=tags, + include_all_tags=group_config["include_all_tags"], + include_any_tags=group_config["include_any_tags"], + exclude_all_tags=group_config["exclude_all_tags"], + exclude_any_tags=group_config["exclude_any_tags"], + ) check_category = self.check_category(category, group_config["categories"]) if check_tags and check_category: return group_name return None - def check_tags(self, tags, include_tags, exclude_tags): - """Check if the torrent has the required tags and does not have the excluded tags""" - if include_tags: - if not set(include_tags).issubset(tags): + def check_tags(self, tags, include_all_tags=set(), include_any_tags=set(), exclude_all_tags=set(), exclude_any_tags=set()): + """Check if the torrent has the required tags""" + tags_set = set(tags) + if include_all_tags: + if not set(include_all_tags).issubset(tags_set): return False - if exclude_tags: - if set(exclude_tags).intersection(tags): + if include_any_tags: + if not set(include_any_tags).intersection(tags_set): + return False + if exclude_all_tags: + if set(exclude_all_tags).issubset(tags_set): + return False + if exclude_any_tags: + if set(exclude_any_tags).intersection(tags_set): return False return True From 97f09388f88be5efdbe9cedee0f76f0dcb637528 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 Jun 2023 01:03:29 +0000 Subject: [PATCH 13/40] Bump ruamel-yaml from 0.17.30 to 0.17.31 Bumps [ruamel-yaml](https://sourceforge.net/p/ruamel-yaml/code/ci/default/tree) from 0.17.30 to 0.17.31. --- updated-dependencies: - dependency-name: ruamel-yaml dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3b2a5214..e4f9fe06 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,5 @@ pre-commit==3.3.2 qbittorrent-api==2023.5.48 requests==2.31.0 retrying==1.3.4 -ruamel.yaml==0.17.30 +ruamel.yaml==0.17.31 schedule==1.2.0 From 9cd3949362d63e1be7cc1ddc4df5e18b15464056 Mon Sep 17 00:00:00 2001 From: bobokun Date: Thu, 1 Jun 2023 19:54:33 -0400 Subject: [PATCH 14/40] fixes share limits bug - Fixes rounding issue with upload limit - Check cleanup on every run - Don't send notification when no updates being made - Fix torrent removing grouping tag when tag changes --- VERSION | 2 +- modules/core/share_limits.py | 42 +++++++++++++++++++----------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/VERSION b/VERSION index 03c481d7..9aacf01f 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.6.4-develop4 +3.6.4-develop5 diff --git a/modules/core/share_limits.py b/modules/core/share_limits.py index b67239bb..7a1a9eb4 100644 --- a/modules/core/share_limits.py +++ b/modules/core/share_limits.py @@ -51,7 +51,8 @@ def update_share_limits(self): "torrent_min_seeding_time": group_config["min_seeding_time"], "torrent_limit_upload_speed": group_config["limit_upload_speed"], } - self.config.send_notifications(attr) + if len(self.torrents_updated) > 0: + self.config.send_notifications(attr) if group_config["cleanup"] and len(self.tdel_dict) > 0: self.cleanup_torrents_for_group(group_name, group_config["priority"]) @@ -162,7 +163,7 @@ def update_share_limits_for_group(self, group_name, group_config, torrents): check_max_ratio = group_config["max_ratio"] != torrent.max_ratio check_max_seeding_time = group_config["max_seeding_time"] != torrent.max_seeding_time # Treat upload limit as -1 if it is set to 0 (unlimited) - torrent_upload_limit = -1 if torrent.up_limit == 0 else torrent.up_limit + torrent_upload_limit = -1 if round(torrent.up_limit / 1024) == 0 else round(torrent.up_limit / 1024) if group_config["limit_upload_speed"] == 0: group_config["limit_upload_speed"] = -1 check_limit_upload_speed = group_config["limit_upload_speed"] != torrent_upload_limit @@ -183,31 +184,32 @@ def update_share_limits_for_group(self, group_name, group_config, torrents): ) logger.trace( "Config Limit Upload Speed vs Torrent Limit Upload Speed: " - f"{group_config['limit_upload_speed']} vs {torrent.up_limit}" + f"{group_config['limit_upload_speed']} vs {torrent_upload_limit}" ) if self.group_tag: logger.print_line(logger.insert_space(f"Added Tag: {self.group_tag}", 8), self.config.loglevel) self.tag_and_update_share_limits_for_torrent(torrent, group_config) self.stats_tagged += 1 self.torrents_updated.append(t_name) - # Cleanup torrents if the torrent meets the criteria for deletion and cleanup is enabled - if group_config["cleanup"]: - tor_reached_seed_limit = self.has_reached_seed_limit( - torrent=torrent, - max_ratio=group_config["max_ratio"], - max_seeding_time=group_config["max_seeding_time"], - min_seeding_time=group_config["min_seeding_time"], - resume_torrent=group_config["resume_torrent_after_change"], - tracker=tracker["url"], - ) - if tor_reached_seed_limit: - if t_hash not in self.tdel_dict: - self.tdel_dict[t_hash] = {} - self.tdel_dict[t_hash]["torrent"] = torrent - self.tdel_dict[t_hash]["content_path"] = torrent["content_path"].replace(self.root_dir, self.remote_dir) - self.tdel_dict[t_hash]["body"] = tor_reached_seed_limit else: self.share_limits_config[group_name]["torrents"].remove(torrent) + + # Cleanup torrents if the torrent meets the criteria for deletion and cleanup is enabled + if group_config["cleanup"]: + tor_reached_seed_limit = self.has_reached_seed_limit( + torrent=torrent, + max_ratio=group_config["max_ratio"], + max_seeding_time=group_config["max_seeding_time"], + min_seeding_time=group_config["min_seeding_time"], + resume_torrent=group_config["resume_torrent_after_change"], + tracker=tracker["url"], + ) + if tor_reached_seed_limit: + if t_hash not in self.tdel_dict: + self.tdel_dict[t_hash] = {} + self.tdel_dict[t_hash]["torrent"] = torrent + self.tdel_dict[t_hash]["content_path"] = torrent["content_path"].replace(self.root_dir, self.remote_dir) + self.tdel_dict[t_hash]["body"] = tor_reached_seed_limit self.torrent_hash_checked.append(t_hash) def tag_and_update_share_limits_for_torrent(self, torrent, group_config): @@ -216,7 +218,7 @@ def tag_and_update_share_limits_for_torrent(self, torrent, group_config): tags = util.get_list(torrent.tags) for tag in tags: if self.share_limits_suffix_tag in tag: - tags.remove(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 self.set_tags_and_limits( From f154792a88f272e9e5d2236d8b63153f186c00a7 Mon Sep 17 00:00:00 2001 From: bobokun Date: Thu, 1 Jun 2023 20:01:38 -0400 Subject: [PATCH 15/40] separate requirements dev --- requirements-dev.txt | 2 ++ requirements.txt | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 requirements-dev.txt diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 00000000..589bfee1 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +flake8==6.0.0 +pre-commit==3.3.2 diff --git a/requirements.txt b/requirements.txt index e4f9fe06..6d26c32a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,5 @@ bencodepy==0.9.5 -flake8==6.0.0 GitPython==3.1.31 -pre-commit==3.3.2 qbittorrent-api==2023.5.48 requests==2.31.0 retrying==1.3.4 From 527f7e77149ccb4f4952fbc45d94e6adf364a258 Mon Sep 17 00:00:00 2001 From: bakerboy448 <55419169+bakerboy448@users.noreply.github.com> Date: Fri, 2 Jun 2023 14:42:18 -0500 Subject: [PATCH 16/40] bump to v4 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 9aacf01f..6f77d310 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.6.4-develop5 +4.0.0-develop5 From b5384dc915a85163f0db0fb380635636438f0e8b Mon Sep 17 00:00:00 2001 From: bobokun Date: Fri, 2 Jun 2023 20:40:05 -0400 Subject: [PATCH 17/40] adds notification grouping to share_limits --- VERSION | 2 +- modules/core/share_limits.py | 3 ++- modules/webhooks.py | 2 ++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 6f77d310..b2ff95b8 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0.0-develop5 +4.0.0-develop6 diff --git a/modules/core/share_limits.py b/modules/core/share_limits.py index 7a1a9eb4..1bd48420 100644 --- a/modules/core/share_limits.py +++ b/modules/core/share_limits.py @@ -2,6 +2,7 @@ from datetime import timedelta from modules import util +from modules.webhooks import GROUP_NOTIFICATION_LIMIT logger = util.logger @@ -63,7 +64,7 @@ def cleanup_torrents_for_group(self, group_name, priority): space=False, border=False, ) - group_notifications = len(self.tdel_dict) > 10 + group_notifications = len(self.tdel_dict) > GROUP_NOTIFICATION_LIMIT t_deleted = set() t_deleted_and_contents = set() for torrent_hash, torrent_dict in self.tdel_dict.items(): diff --git a/modules/webhooks.py b/modules/webhooks.py index 6e747bc4..b91b218e 100755 --- a/modules/webhooks.py +++ b/modules/webhooks.py @@ -9,6 +9,8 @@ logger = util.logger +GROUP_NOTIFICATION_LIMIT = 10 + class Webhooks: """Class to handle webhooks.""" From e936e4526f14d1600ee88c5c1bd90f31cf941ba2 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sat, 3 Jun 2023 19:38:41 -0400 Subject: [PATCH 18/40] updates tags to use group notifications --- modules/core/share_limits.py | 16 +++++++---- modules/core/tags.py | 54 +++++++++++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/modules/core/share_limits.py b/modules/core/share_limits.py index 1bd48420..7ea52e16 100644 --- a/modules/core/share_limits.py +++ b/modules/core/share_limits.py @@ -45,7 +45,7 @@ def update_share_limits(self): "title": f"Updating Share Limits for {group_name}. Priority {group_config['priority']}", "body": f"Updated {len(self.torrents_updated)} torrents.", "grouping": group_name, - "torrent_list": self.torrents_updated, + "torrents": self.torrents_updated, "torrent_tag": self.group_tag, "torrent_max_ratio": group_config["max_ratio"], "torrent_max_seeding_time": group_config["max_seeding_time"], @@ -88,9 +88,9 @@ def cleanup_torrents_for_group(self, group_name, priority): "function": "cleanup_share_limits", "title": "Share limit removal", "grouping": group_name, - "torrent_name": t_name, + "torrents": [t_name], "torrent_category": torrent.category, - "cleanup": "True", + "cleanup": True, "torrent_tracker": tracker["url"], "notifiarr_indexer": tracker["notifiarr"], } @@ -135,8 +135,11 @@ def cleanup_torrents_for_group(self, group_name, priority): "title": "Share limit removal - Deleted .torrent but NOT content files.", "body": f"Deleted {self.stats_deleted} .torrents but NOT content files.", "grouping": group_name, - "torrent_list": list(t_deleted), + "torrents": list(t_deleted), + "torrent_category": None, "cleanup": True, + "torrent_tracker": None, + "notifiarr_indexer": None, "torrents_deleted_and_contents": False, } self.config.send_notifications(attr) @@ -146,8 +149,11 @@ def cleanup_torrents_for_group(self, group_name, priority): "title": "Share limit removal - Deleted .torrent AND content files.", "body": f"Deleted {self.stats_deleted_contents} .torrents AND content files.", "grouping": group_name, - "torrent_list": list(t_deleted_and_contents), + "torrents": list(t_deleted_and_contents), + "torrent_category": None, "cleanup": True, + "torrent_tracker": None, + "notifiarr_indexer": None, "torrents_deleted_and_contents": True, } self.config.send_notifications(attr) diff --git a/modules/core/tags.py b/modules/core/tags.py index 029b9be8..7decdb73 100644 --- a/modules/core/tags.py +++ b/modules/core/tags.py @@ -1,4 +1,5 @@ from modules import util +from modules.webhooks import GROUP_NOTIFICATION_LIMIT logger = util.logger @@ -10,8 +11,10 @@ def __init__(self, qbit_manager): self.client = qbit_manager.client self.stats = 0 self.share_limits_suffix_tag = qbit_manager.config.share_limits_suffix_tag # suffix tag for share limits - + self.torrents_updated = [] # List of torrents updated + self.notify_attr = [] # List of single torrent attributes to send to notifiarr self.tags() + self.notify() def tags(self): """Update tags for torrents""" @@ -23,9 +26,10 @@ def tags(self): if torrent.tags == "" or (len([trk for trk in check_tags if trk not in ignore_tags]) == 0): tracker = self.qbt.get_tags(torrent.trackers) if tracker["tag"]: + t_name = torrent.name self.stats += len(tracker["tag"]) body = [] - body += logger.print_line(logger.insert_space(f"Torrent Name: {torrent.name}", 3), self.config.loglevel) + body += logger.print_line(logger.insert_space(f"Torrent Name: {t_name}", 3), self.config.loglevel) body += logger.print_line( logger.insert_space(f'New Tag{"s" if len(tracker["tag"]) > 1 else ""}: {", ".join(tracker["tag"])}', 8), self.config.loglevel, @@ -38,16 +42,58 @@ def tags(self): "function": "tag_update", "title": "Updating Tags", "body": "\n".join(body), - "torrent_name": torrent.name, + "torrents": [t_name], "torrent_category": category, "torrent_tag": ", ".join(tracker["tag"]), "torrent_tracker": tracker["url"], "notifiarr_indexer": tracker["notifiarr"], } - self.config.send_notifications(attr) + self.notify_attr.append(attr) + self.torrents_updated.append(t_name) if self.stats >= 1: logger.print_line( f"{'Did not update' if self.config.dry_run else 'Updated'} {self.stats} new tags.", self.config.loglevel ) else: logger.print_line("No new torrents to tag.", self.config.loglevel) + + def notify(self): + """Send notifications""" + + def group_notifications_by_tag(self): + group_attr = {} + """Group notifications by tag""" + for attr in self.notify_attr: + tag = attr["torrent_tag"] + if tag not in group_attr: + group_attr[tag] = { + "torrent_tag": tag, + "torrents": [attr["torrents"][0]], + "torrent_tracker": attr["torrent_tracker"], + "notifiarr_indexer": attr["notifiarr_indexer"], + } + else: + group_attr[tag]["torrents"].append(attr["torrents"][0]) + return group_attr + + if len(self.torrents_updated) > GROUP_NOTIFICATION_LIMIT: + logger.trace( + f"Number of tags > {GROUP_NOTIFICATION_LIMIT}, grouping notifications by tag.", + ) + group_attr = group_notifications_by_tag(self) + for tag in group_attr: + attr = { + "function": "tag_update", + "title": f"Updating Tags for {tag}", + "body": f"Updated {len(group_attr[tag]['torrents'])} " + f"{'torrents' if len(group_attr[tag]['torrents']) > 1 else 'torrent'} with tag '{tag}'", + "torrents": group_attr[tag]["torrents"], + "torrent_category": None, + "torrent_tag": tag, + "torrent_tracker": group_attr[tag]["torrent_tracker"], + "notifiarr_indexer": group_attr[tag]["notifiarr_indexer"], + } + self.config.send_notifications(attr) + else: + for attr in self.notify_attr: + self.config.send_notifications(attr) From c013ff755bad5fbe9773edf63a1c89fc077b5b1d Mon Sep 17 00:00:00 2001 From: bobokun Date: Sat, 3 Jun 2023 20:03:38 -0400 Subject: [PATCH 19/40] updates categories to use group notifications --- modules/core/category.py | 50 +++++++++++++++++++++++++++++++++++++--- modules/core/tags.py | 3 ++- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/modules/core/category.py b/modules/core/category.py index 6cfcbec5..95e9207f 100644 --- a/modules/core/category.py +++ b/modules/core/category.py @@ -1,6 +1,7 @@ from qbittorrentapi import Conflict409Error from modules import util +from modules.webhooks import GROUP_NOTIFICATION_LIMIT logger = util.logger @@ -11,8 +12,11 @@ def __init__(self, qbit_manager): self.config = qbit_manager.config self.client = qbit_manager.client self.stats = 0 + self.torrents_updated = [] # List of torrents updated + self.notify_attr = [] # List of single torrent attributes to send to notifiarr self.category() + self.notify() def category(self): """Update category for torrents that don't have any category defined and returns total number categories updated""" @@ -40,6 +44,7 @@ def category(self): def update_cat(self, torrent, new_cat, cat_change): """Update category based on the torrent information""" tracker = self.qbt.get_tags(torrent.trackers) + t_name = torrent.name old_cat = torrent.category if not self.config.dry_run: try: @@ -55,7 +60,7 @@ def update_cat(self, torrent, new_cat, cat_change): self.client.torrent_categories.create_category(name=new_cat, save_path=torrent.save_path) torrent.set_category(category=new_cat) body = [] - body += logger.print_line(logger.insert_space(f"Torrent Name: {torrent.name}", 3), self.config.loglevel) + body += logger.print_line(logger.insert_space(f"Torrent Name: {t_name}", 3), self.config.loglevel) if cat_change: body += logger.print_line(logger.insert_space(f"Old Category: {old_cat}", 3), self.config.loglevel) title = "Moving Categories" @@ -67,10 +72,49 @@ def update_cat(self, torrent, new_cat, cat_change): "function": "cat_update", "title": title, "body": "\n".join(body), - "torrent_name": torrent.name, + "torrents": [t_name], "torrent_category": new_cat, "torrent_tracker": tracker["url"], "notifiarr_indexer": tracker["notifiarr"], } - self.config.send_notifications(attr) + self.notify_attr.append(attr) + self.torrents_updated.append(t_name) self.stats += 1 + + def notify(self): + """Send notifications""" + + def group_notifications_by_category(self): + group_attr = {} + """Group notifications by category""" + for attr in self.notify_attr: + category = attr["torrent_category"] + if category not in group_attr: + group_attr[category] = { + "torrent_category": category, + "torrents": [attr["torrents"][0]], + } + else: + group_attr[category]["torrents"].append(attr["torrents"][0]) + return group_attr + + if len(self.torrents_updated) > GROUP_NOTIFICATION_LIMIT: + logger.trace( + f"Number of torrents updated > {GROUP_NOTIFICATION_LIMIT}, grouping notifications by category.", + ) + group_attr = group_notifications_by_category(self) + for category in group_attr: + attr = { + "function": "cat_update", + "title": f"Updating Category for {category}", + "body": f"Updated {len(group_attr[category]['torrents'])} " + f"{'torrents' if len(group_attr[category]['torrents']) > 1 else 'torrent'} with category '{category}'", + "torrents": group_attr[category]["torrents"], + "torrent_category": category, + "torrent_tracker": None, + "notifiarr_indexer": None, + } + self.config.send_notifications(attr) + else: + for attr in self.notify_attr: + self.config.send_notifications(attr) diff --git a/modules/core/tags.py b/modules/core/tags.py index 7decdb73..98198386 100644 --- a/modules/core/tags.py +++ b/modules/core/tags.py @@ -13,6 +13,7 @@ def __init__(self, qbit_manager): self.share_limits_suffix_tag = qbit_manager.config.share_limits_suffix_tag # suffix tag for share limits self.torrents_updated = [] # List of torrents updated self.notify_attr = [] # List of single torrent attributes to send to notifiarr + self.tags() self.notify() @@ -78,7 +79,7 @@ def group_notifications_by_tag(self): if len(self.torrents_updated) > GROUP_NOTIFICATION_LIMIT: logger.trace( - f"Number of tags > {GROUP_NOTIFICATION_LIMIT}, grouping notifications by tag.", + f"Number of torrents updated > {GROUP_NOTIFICATION_LIMIT}, grouping notifications by tag.", ) group_attr = group_notifications_by_tag(self) for tag in group_attr: From b00c69a33ea7988808f05377aa91fe79a6e5e79a Mon Sep 17 00:00:00 2001 From: bobokun Date: Sat, 3 Jun 2023 20:14:10 -0400 Subject: [PATCH 20/40] rename torrent_name to torrents for notifications --- modules/core/cross_seed.py | 4 ++-- modules/core/recheck.py | 6 +++--- modules/core/remove_unregistered.py | 6 +++--- modules/core/tag_nohardlinks.py | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/modules/core/cross_seed.py b/modules/core/cross_seed.py index ffbd6152..601c0d02 100644 --- a/modules/core/cross_seed.py +++ b/modules/core/cross_seed.py @@ -56,7 +56,7 @@ def cross_seed(self): "function": "cross_seed", "title": "Adding New Cross-Seed Torrent", "body": "\n".join(body), - "torrent_name": t_name, + "torrents": [t_name], "torrent_category": category, "torrent_save_path": dest, "torrent_tag": "cross-seed", @@ -113,7 +113,7 @@ def cross_seed(self): "function": "tag_cross_seed", "title": "Tagging Cross-Seed Torrent", "body": body, - "torrent_name": t_name, + "torrents": [t_name], "torrent_category": t_cat, "torrent_tag": "cross-seed", "torrent_tracker": tracker, diff --git a/modules/core/recheck.py b/modules/core/recheck.py index 1fc2e5d4..acefeb15 100644 --- a/modules/core/recheck.py +++ b/modules/core/recheck.py @@ -36,7 +36,7 @@ def recheck(self): "function": "recheck", "title": "Resuming Torrent", "body": body, - "torrent_name": torrent.name, + "torrents": [torrent.name], "torrent_category": torrent.category, "torrent_tracker": tracker["url"], "notifiarr_indexer": tracker["notifiarr"], @@ -82,7 +82,7 @@ def recheck(self): "function": "recheck", "title": "Resuming Torrent", "body": body, - "torrent_name": torrent.name, + "torrents": [torrent.name], "torrent_category": torrent.category, "torrent_tracker": tracker["url"], "notifiarr_indexer": tracker["notifiarr"], @@ -105,7 +105,7 @@ def recheck(self): "function": "recheck", "title": "Rechecking Torrent", "body": body, - "torrent_name": torrent.name, + "torrents": [torrent.name], "torrent_category": torrent.category, "torrent_tracker": tracker["url"], "notifiarr_indexer": tracker["notifiarr"], diff --git a/modules/core/remove_unregistered.py b/modules/core/remove_unregistered.py index 40d283d4..24dd0812 100644 --- a/modules/core/remove_unregistered.py +++ b/modules/core/remove_unregistered.py @@ -58,7 +58,7 @@ def remove_previous_errors(self): "function": "untag_tracker_error", "title": "Untagging Tracker Error Torrent", "body": "\n".join(body), - "torrent_name": torrent.name, + "torrents": [torrent.name], "torrent_category": torrent.category, "torrent_tag": self.tag_error, "torrent_tracker": tracker["url"], @@ -165,7 +165,7 @@ def tag_tracker_error(self, msg, tracker, torrent): "function": "tag_tracker_error", "title": "Tag Tracker Error Torrents", "body": tor_error, - "torrent_name": self.t_name, + "torrents": [self.t_name], "torrent_category": self.t_cat, "torrent_tag": self.tag_error, "torrent_status": msg, @@ -185,7 +185,7 @@ def del_unregistered(self, msg, tracker, torrent): attr = { "function": "rem_unregistered", "title": "Removing Unregistered Torrents", - "torrent_name": self.t_name, + "torrents": [self.t_name], "torrent_category": self.t_cat, "torrent_status": msg, "torrent_tracker": tracker["url"], diff --git a/modules/core/tag_nohardlinks.py b/modules/core/tag_nohardlinks.py index 317c1eb0..8d783a97 100644 --- a/modules/core/tag_nohardlinks.py +++ b/modules/core/tag_nohardlinks.py @@ -34,7 +34,7 @@ def add_tag_no_hl(self, torrent, tracker, category): "function": "tag_nohardlinks", "title": title, "body": "\n".join(body), - "torrent_name": torrent.name, + "torrents": [torrent.name], "torrent_category": torrent.category, "torrent_tag": self.nohardlinks_tag, "torrent_tracker": tracker["url"], @@ -63,7 +63,7 @@ def check_previous_nohardlinks_tagged_torrents(self, has_nohardlinks, torrent, t "function": "untag_nohardlinks", "title": "Untagging Previous Torrents that now have hardlinks", "body": "\n".join(body), - "torrent_name": torrent.name, + "torrents": [torrent.name], "torrent_category": torrent.category, "torrent_tag": self.nohardlinks_tag, "torrent_tracker": tracker["url"], From 56e397da4b6d57bb36597da762b1d6f46fefb96f Mon Sep 17 00:00:00 2001 From: bobokun Date: Sat, 3 Jun 2023 20:15:13 -0400 Subject: [PATCH 21/40] update dev7 version - notification changes --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index b2ff95b8..e759c1ab 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0.0-develop6 +4.0.0-develop7 From 2ce622f6b9d2e7b1bce09e1777b964da466a9209 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sat, 3 Jun 2023 20:43:42 -0400 Subject: [PATCH 22/40] minor fixes --- modules/core/category.py | 19 +++++++++++++++---- modules/core/tags.py | 13 ++++++++++--- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/modules/core/category.py b/modules/core/category.py index 95e9207f..7f5be92e 100644 --- a/modules/core/category.py +++ b/modules/core/category.py @@ -74,6 +74,7 @@ def update_cat(self, torrent, new_cat, cat_change): "body": "\n".join(body), "torrents": [t_name], "torrent_category": new_cat, + "torrent_tag": ", ".join(tracker["tag"]), "torrent_tracker": tracker["url"], "notifiarr_indexer": tracker["notifiarr"], } @@ -92,7 +93,11 @@ def group_notifications_by_category(self): if category not in group_attr: group_attr[category] = { "torrent_category": category, + "body": attr["body"], "torrents": [attr["torrents"][0]], + "torrent_tag": attr["torrent_tag"], + "torrent_tracker": attr["torrent_tracker"], + "notifiarr_indexer": attr["notifiarr_indexer"], } else: group_attr[category]["torrents"].append(attr["torrents"][0]) @@ -104,15 +109,21 @@ def group_notifications_by_category(self): ) group_attr = group_notifications_by_category(self) for category in group_attr: + num_torrents_updated = len(group_attr[category]["torrents"]) + only_one_torrent_updated = num_torrents_updated == 1 + attr = { "function": "cat_update", "title": f"Updating Category for {category}", - "body": f"Updated {len(group_attr[category]['torrents'])} " - f"{'torrents' if len(group_attr[category]['torrents']) > 1 else 'torrent'} with category '{category}'", + "body": group_attr[category]["body"] + if only_one_torrent_updated + else f"Updated {num_torrents_updated} " + f"{'torrent' if only_one_torrent_updated else 'torrents'} with category '{category}'", "torrents": group_attr[category]["torrents"], "torrent_category": category, - "torrent_tracker": None, - "notifiarr_indexer": None, + "torrent_tag": group_attr[category]["torrent_tag"] if only_one_torrent_updated else None, + "torrent_tracker": group_attr[category]["torrent_tracker"] if only_one_torrent_updated else None, + "notifiarr_indexer": group_attr[category]["notifiarr_indexer"] if only_one_torrent_updated else None, } self.config.send_notifications(attr) else: diff --git a/modules/core/tags.py b/modules/core/tags.py index 98198386..cdd27562 100644 --- a/modules/core/tags.py +++ b/modules/core/tags.py @@ -69,7 +69,9 @@ def group_notifications_by_tag(self): if tag not in group_attr: group_attr[tag] = { "torrent_tag": tag, + "body": attr["body"], "torrents": [attr["torrents"][0]], + "torrent_category": attr["torrent_category"], "torrent_tracker": attr["torrent_tracker"], "notifiarr_indexer": attr["notifiarr_indexer"], } @@ -83,13 +85,18 @@ def group_notifications_by_tag(self): ) group_attr = group_notifications_by_tag(self) for tag in group_attr: + num_torrents_updated = len(group_attr[tag]["torrents"]) + only_one_torrent_updated = num_torrents_updated == 1 + attr = { "function": "tag_update", "title": f"Updating Tags for {tag}", - "body": f"Updated {len(group_attr[tag]['torrents'])} " - f"{'torrents' if len(group_attr[tag]['torrents']) > 1 else 'torrent'} with tag '{tag}'", + "body": group_attr[tag]["body"] + if only_one_torrent_updated + else f"Updated {num_torrents_updated} " + f"{'torrent' if only_one_torrent_updated else 'torrents'} with tag '{tag}'", "torrents": group_attr[tag]["torrents"], - "torrent_category": None, + "torrent_category": group_attr[tag]["torrent_category"] if only_one_torrent_updated else None, "torrent_tag": tag, "torrent_tracker": group_attr[tag]["torrent_tracker"], "notifiarr_indexer": group_attr[tag]["notifiarr_indexer"], From 38c8ab9c116cc889d8f7901a7d77e9385115593a Mon Sep 17 00:00:00 2001 From: bobokun Date: Sat, 3 Jun 2023 23:24:56 -0400 Subject: [PATCH 23/40] refactor webhook to group notifications --- modules/core/category.py | 51 +-------------------------------- modules/core/tags.py | 51 +-------------------------------- modules/webhooks.py | 62 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 100 deletions(-) diff --git a/modules/core/category.py b/modules/core/category.py index 7f5be92e..527e01a6 100644 --- a/modules/core/category.py +++ b/modules/core/category.py @@ -1,7 +1,6 @@ from qbittorrentapi import Conflict409Error from modules import util -from modules.webhooks import GROUP_NOTIFICATION_LIMIT logger = util.logger @@ -16,7 +15,7 @@ def __init__(self, qbit_manager): self.notify_attr = [] # List of single torrent attributes to send to notifiarr self.category() - self.notify() + self.config.webhooks_factory.notify(self.torrents_updated, self.notify_attr, group_by="category") def category(self): """Update category for torrents that don't have any category defined and returns total number categories updated""" @@ -81,51 +80,3 @@ def update_cat(self, torrent, new_cat, cat_change): self.notify_attr.append(attr) self.torrents_updated.append(t_name) self.stats += 1 - - def notify(self): - """Send notifications""" - - def group_notifications_by_category(self): - group_attr = {} - """Group notifications by category""" - for attr in self.notify_attr: - category = attr["torrent_category"] - if category not in group_attr: - group_attr[category] = { - "torrent_category": category, - "body": attr["body"], - "torrents": [attr["torrents"][0]], - "torrent_tag": attr["torrent_tag"], - "torrent_tracker": attr["torrent_tracker"], - "notifiarr_indexer": attr["notifiarr_indexer"], - } - else: - group_attr[category]["torrents"].append(attr["torrents"][0]) - return group_attr - - if len(self.torrents_updated) > GROUP_NOTIFICATION_LIMIT: - logger.trace( - f"Number of torrents updated > {GROUP_NOTIFICATION_LIMIT}, grouping notifications by category.", - ) - group_attr = group_notifications_by_category(self) - for category in group_attr: - num_torrents_updated = len(group_attr[category]["torrents"]) - only_one_torrent_updated = num_torrents_updated == 1 - - attr = { - "function": "cat_update", - "title": f"Updating Category for {category}", - "body": group_attr[category]["body"] - if only_one_torrent_updated - else f"Updated {num_torrents_updated} " - f"{'torrent' if only_one_torrent_updated else 'torrents'} with category '{category}'", - "torrents": group_attr[category]["torrents"], - "torrent_category": category, - "torrent_tag": group_attr[category]["torrent_tag"] if only_one_torrent_updated else None, - "torrent_tracker": group_attr[category]["torrent_tracker"] if only_one_torrent_updated else None, - "notifiarr_indexer": group_attr[category]["notifiarr_indexer"] if only_one_torrent_updated else None, - } - self.config.send_notifications(attr) - else: - for attr in self.notify_attr: - self.config.send_notifications(attr) diff --git a/modules/core/tags.py b/modules/core/tags.py index cdd27562..540571f8 100644 --- a/modules/core/tags.py +++ b/modules/core/tags.py @@ -1,5 +1,4 @@ from modules import util -from modules.webhooks import GROUP_NOTIFICATION_LIMIT logger = util.logger @@ -15,7 +14,7 @@ def __init__(self, qbit_manager): self.notify_attr = [] # List of single torrent attributes to send to notifiarr self.tags() - self.notify() + self.config.webhooks_factory.notify(self.torrents_updated, self.notify_attr, group_by="tag") def tags(self): """Update tags for torrents""" @@ -57,51 +56,3 @@ def tags(self): ) else: logger.print_line("No new torrents to tag.", self.config.loglevel) - - def notify(self): - """Send notifications""" - - def group_notifications_by_tag(self): - group_attr = {} - """Group notifications by tag""" - for attr in self.notify_attr: - tag = attr["torrent_tag"] - if tag not in group_attr: - group_attr[tag] = { - "torrent_tag": tag, - "body": attr["body"], - "torrents": [attr["torrents"][0]], - "torrent_category": attr["torrent_category"], - "torrent_tracker": attr["torrent_tracker"], - "notifiarr_indexer": attr["notifiarr_indexer"], - } - else: - group_attr[tag]["torrents"].append(attr["torrents"][0]) - return group_attr - - if len(self.torrents_updated) > GROUP_NOTIFICATION_LIMIT: - logger.trace( - f"Number of torrents updated > {GROUP_NOTIFICATION_LIMIT}, grouping notifications by tag.", - ) - group_attr = group_notifications_by_tag(self) - for tag in group_attr: - num_torrents_updated = len(group_attr[tag]["torrents"]) - only_one_torrent_updated = num_torrents_updated == 1 - - attr = { - "function": "tag_update", - "title": f"Updating Tags for {tag}", - "body": group_attr[tag]["body"] - if only_one_torrent_updated - else f"Updated {num_torrents_updated} " - f"{'torrent' if only_one_torrent_updated else 'torrents'} with tag '{tag}'", - "torrents": group_attr[tag]["torrents"], - "torrent_category": group_attr[tag]["torrent_category"] if only_one_torrent_updated else None, - "torrent_tag": tag, - "torrent_tracker": group_attr[tag]["torrent_tracker"], - "notifiarr_indexer": group_attr[tag]["notifiarr_indexer"], - } - self.config.send_notifications(attr) - else: - for attr in self.notify_attr: - self.config.send_notifications(attr) diff --git a/modules/webhooks.py b/modules/webhooks.py index b91b218e..6db8be8f 100755 --- a/modules/webhooks.py +++ b/modules/webhooks.py @@ -165,3 +165,65 @@ def function_hooks(self, webhook, json): """Send a webhook to notify that a function has completed.""" if self.function_webhooks: self._request(webhook, json) + + def notify(self, torrents_updated=[], payload={}, group_by=""): + if len(torrents_updated) > GROUP_NOTIFICATION_LIMIT: + logger.trace( + f"Number of torrents updated > {GROUP_NOTIFICATION_LIMIT}, grouping notifications" + f"{f' by {group_by}' if group_by else ''}", + ) + if group_by == "category": + group_attr = group_notifications_by_key(payload, "torrent_category") + elif group_by == "tag": + group_attr = group_notifications_by_key(payload, "torrent_tag") + + # group notifications by grouping attribute + for group in group_attr: + num_torrents_updated = len(group_attr[group]["torrents"]) + only_one_torrent_updated = num_torrents_updated == 1 + + attr = { + "function": group_attr[group]["function"], + "title": f"{group_attr[group]['title']} for {group}", + "body": group_attr[group]["body"] + if only_one_torrent_updated + else f"Updated {num_torrents_updated} " + f"{'torrent' if only_one_torrent_updated else 'torrents'} with {group_by} '{group}'", + "torrents": group_attr[group]["torrents"], + } + if group_by == "category": + attr["torrent_category"] = group + attr["torrent_tag"] = group_attr[group]["torrent_tag"] if only_one_torrent_updated else None + attr["torrent_tracker"] = group_attr[group]["torrent_tracker"] if only_one_torrent_updated else None + attr["notifiarr_indexer"] = group_attr[group]["notifiarr_indexer"] if only_one_torrent_updated else None + elif group_by == "tag": + attr["torrent_tag"] = group + attr["torrent_category"] = group_attr[group]["torrent_category"] if only_one_torrent_updated else None + attr["torrent_tracker"] = group_attr[group]["torrent_tracker"] + attr["notifiarr_indexer"] = group_attr[group]["notifiarr_indexer"] + + self.config.send_notifications(attr) + else: + for attr in payload: + self.config.send_notifications(attr) + + +def group_notifications_by_key(payload, key): + """Group notifications by key""" + group_attr = {} + for attr in payload: + group = attr[key] + if group not in group_attr: + group_attr[group] = { + "function": attr["function"], + "title": attr["title"], + "body": attr["body"], + "torrent_category": attr["torrent_category"], + "torrent_tag": attr["torrent_tag"], + "torrents": [attr["torrents"][0]], + "torrent_tracker": attr["torrent_tracker"], + "notifiarr_indexer": attr["notifiarr_indexer"], + } + else: + group_attr[group]["torrents"].append(attr["torrents"][0]) + return group_attr From 40eea41a29b3c34e31d1f3510f1e5523466f3fa3 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sun, 4 Jun 2023 07:44:17 -0400 Subject: [PATCH 24/40] fix cleanup bug --- VERSION | 2 +- modules/qbittorrent.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/VERSION b/VERSION index e759c1ab..47c3fcce 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0.0-develop7 +4.0.0-develop8 diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index 37d33858..ce55de35 100755 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -327,18 +327,18 @@ def tor_delete_recycle(self, torrent, info): # Create recycle bin if not exists torrent_path = os.path.join(recycle_path, "torrents") torrents_json_path = os.path.join(recycle_path, "torrents_json") - + torrent_name = info["torrents"][0] os.makedirs(recycle_path, exist_ok=True) if self.config.recyclebin["save_torrents"]: if os.path.isdir(torrent_path) is False: os.makedirs(torrent_path) if os.path.isdir(torrents_json_path) is False: os.makedirs(torrents_json_path) - torrent_json_file = os.path.join(torrents_json_path, f"{info['torrent_name']}.json") + torrent_json_file = os.path.join(torrents_json_path, f"{torrent_name}.json") torrent_json = util.load_json(torrent_json_file) if not torrent_json: logger.info(f"Saving Torrent JSON file to {torrent_json_file}") - torrent_json["torrent_name"] = info["torrent_name"] + torrent_json["torrent_name"] = torrent_name torrent_json["category"] = info["torrent_category"] else: logger.info(f"Adding {info['torrent_tracker']} to existing {os.path.basename(torrent_json_file)}") From 99eebb7c10b2f840ed091b1b84b662179e6ced6d Mon Sep 17 00:00:00 2001 From: bobokun Date: Sun, 4 Jun 2023 10:31:06 -0400 Subject: [PATCH 25/40] Updated run_end stats to include share limits --- VERSION | 2 +- modules/webhooks.py | 2 ++ qbit_manage.py | 8 ++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 47c3fcce..9b1df857 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0.0-develop8 +4.0.0-develop9 diff --git a/modules/webhooks.py b/modules/webhooks.py index 6db8be8f..1791ec28 100755 --- a/modules/webhooks.py +++ b/modules/webhooks.py @@ -141,6 +141,8 @@ def end_time_hooks(self, start_time, end_time, run_time, next_run, stats, body): "orphaned_files_found": stats["orphaned"], "torrents_tagged_no_hardlinks": stats["tagged_noHL"], "torrents_untagged_no_hardlinks": stats["untagged_noHL"], + "torrents_updated_share_limits": stats["updated_share_limits"], + "torrents_cleaned_share_limits": stats["cleaned_share_limits"], "files_deleted_from_recyclebin": stats["recycle_emptied"], "files_deleted_from_orphaned": stats["orphaned_emptied"], }, diff --git a/qbit_manage.py b/qbit_manage.py index aa77768d..a6e0dd9f 100755 --- a/qbit_manage.py +++ b/qbit_manage.py @@ -408,6 +408,8 @@ def start(): "untagged_tracker_error": 0, "tagged_noHL": 0, "untagged_noHL": 0, + "updated_share_limits": 0, + "cleaned_share_limits": 0, } def finished_run(): @@ -476,8 +478,10 @@ def finished_run(): if cfg.commands["share_limits"]: share_limits = ShareLimits(qbit_manager) stats["tagged"] += share_limits.stats_tagged + stats["updated_share_limits"] += share_limits.stats_tagged stats["deleted"] += share_limits.stats_deleted stats["deleted_contents"] += share_limits.stats_deleted_contents + stats["cleaned_share_limits"] += share_limits.stats_deleted + share_limits.stats_deleted_contents # Remove Orphaned Files if cfg.commands["rem_orphaned"]: @@ -515,6 +519,10 @@ def finished_run(): stats_summary.append(f"Total {cfg.nohardlinks_tag} Torrents Tagged: {stats['tagged_noHL']}") if stats["untagged_noHL"] > 0: stats_summary.append(f"Total {cfg.nohardlinks_tag} Torrents untagged: {stats['untagged_noHL']}") + if stats["updated_share_limits"] > 0: + stats_summary.append(f"Total Share Limits Updated: {stats['updated_share_limits']}") + if stats["cleaned_share_limits"] > 0: + stats_summary.append(f"Total Torrents Removed from Meeting Share Limits: {stats['cleaned_share_limits']}") if stats["recycle_emptied"] > 0: stats_summary.append(f"Total Files Deleted from Recycle Bin: {stats['recycle_emptied']}") if stats["orphaned_emptied"] > 0: From aa86f3f2c9c7c4cbd5d069c1f73f46c1cf394c0d Mon Sep 17 00:00:00 2001 From: bobokun Date: Sun, 4 Jun 2023 12:33:15 -0400 Subject: [PATCH 26/40] update sample --- config/config.yml.sample | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/config.yml.sample b/config/config.yml.sample index 2c500365..eb810c59 100755 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -189,12 +189,13 @@ share_limits: add_group_to_tag: true cross-seed: priority: 2 - tags: cross-seed + include_all_tags: + - cross-seed max_seeding_time: 10200 cleanup: false PTP: priority: 3 - tags: + include_all_tags: - PassThePopcorn max_ratio: 2.0 max_seeding_time: 130000 From bf5d0621ef7f479a458743def7978463b9755ddf Mon Sep 17 00:00:00 2001 From: bobokun Date: Sun, 4 Jun 2023 14:06:00 -0400 Subject: [PATCH 27/40] update config sample description --- config/config.yml.sample | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/config/config.yml.sample b/config/config.yml.sample index eb810c59..6e9f9644 100755 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -142,6 +142,8 @@ nohardlinks: share_limits: # Control how torrent share limits are set depending on the priority of your grouping + # Each torrent will be matched with the share limit group with the highest priority that meets the group filter criteria. + # Each torrent can only be matched with one share limit group # This variable is mandatory and is a text defining the name of your grouping. This can be any string you want noHL: # priority: # This is the priority of your grouping. The lower the number the higher the priority @@ -170,13 +172,13 @@ share_limits: - RadarrComplete - SonarrComplete # max_ratio : Will set the torrent Maximum share ratio until torrent is stopped from seeding/uploading. - # Delete this key from a category's config to use the tracker's configured max_ratio. Will default to -1 if not specified for the group. + # Will default to -1 (no limit) if not specified for the group. max_ratio: 5.0 # max_seeding_time : Will set the torrent Maximum seeding time (minutes) until torrent is stopped from seeding. - # Delete this key from a category's config to use the tracker's configured max_seeding_time. Will default to -1 if not specified for the group. + # Will default to -1 (no limit) if not specified for the group. max_seeding_time: 129600 # min_seeding_time : Will prevent torrent deletion by cleanup variable if torrent has not yet minimum seeding time (minutes). - # Delete this key from a category's config to use the tracker's configured min_seeding_time. Will default to 0 if not specified for the group. + # Will default to 0 if not specified for the group. min_seeding_time: 43200 # Limit Upload Speed : Will limit the upload speed KiB/s (KiloBytes/second) (`-1` : No Limit) limit_upload_speed: 0 @@ -274,6 +276,7 @@ webhooks: tag_tracker_error: notifiarr rem_orphaned: notifiarr tag_nohardlinks: notifiarr + share_limits: notifiarr cleanup_dirs: notifiarr bhd: From 3f42ee5262030fd7e98a74828305b665034ca358 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sun, 4 Jun 2023 14:32:52 -0400 Subject: [PATCH 28/40] updates cross seed to use group notifiactions --- VERSION | 2 +- modules/core/cross_seed.py | 15 ++++++++++++--- modules/webhooks.py | 34 +++++++++++++++++++--------------- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/VERSION b/VERSION index 9b1df857..249b4645 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0.0-develop9 +4.0.0-develop10 diff --git a/modules/core/cross_seed.py b/modules/core/cross_seed.py index 601c0d02..7baef976 100644 --- a/modules/core/cross_seed.py +++ b/modules/core/cross_seed.py @@ -15,6 +15,9 @@ def __init__(self, qbit_manager): self.stats_added = 0 self.stats_tagged = 0 + self.torrents_updated = [] # List of torrents added by cross-seed + self.notify_attr = [] # List of single torrent attributes to send to notifiarr + self.cross_seed() def cross_seed(self): @@ -62,7 +65,8 @@ def cross_seed(self): "torrent_tag": "cross-seed", "torrent_tracker": t_tracker, } - self.config.send_notifications(attr) + self.notify_attr.append(attr) + self.torrents_updated.append(t_name) self.stats_added += 1 if not self.config.dry_run: self.client.torrents.add( @@ -95,6 +99,10 @@ def cross_seed(self): else: logger.print_line(error, "WARNING") self.config.notify(error, "cross-seed", False) + + self.config.webhooks_factory.notify(self.torrents_updated, self.notify_attr, group_by="category") + self.torrents_updated = [] + self.notify_attr = [] # Tag missing cross-seed torrents tags for torrent in self.qbt.torrent_list: t_name = torrent.name @@ -118,10 +126,11 @@ def cross_seed(self): "torrent_tag": "cross-seed", "torrent_tracker": tracker, } - self.config.send_notifications(attr) + self.notify_attr.append(attr) + self.torrents_updated.append(t_name) if not self.config.dry_run: torrent.add_tags(tags="cross-seed") - + self.config.webhooks_factory.notify(self.torrents_updated, self.notify_attr, group_by="category") numcategory = Counter(categories) for cat in numcategory: if numcategory[cat] > 0: diff --git a/modules/webhooks.py b/modules/webhooks.py index 1791ec28..2e43f4a8 100755 --- a/modules/webhooks.py +++ b/modules/webhooks.py @@ -195,14 +195,18 @@ def notify(self, torrents_updated=[], payload={}, group_by=""): } if group_by == "category": attr["torrent_category"] = group - attr["torrent_tag"] = group_attr[group]["torrent_tag"] if only_one_torrent_updated else None - attr["torrent_tracker"] = group_attr[group]["torrent_tracker"] if only_one_torrent_updated else None - attr["notifiarr_indexer"] = group_attr[group]["notifiarr_indexer"] if only_one_torrent_updated else None + attr["torrent_tag"] = group_attr[group].get("torrent_tag") if only_one_torrent_updated else None + attr["torrent_tracker"] = group_attr[group].get("torrent_tracker") if only_one_torrent_updated else None + attr["notifiarr_indexer"] = group_attr[group].get("notifiarr_indexer") if only_one_torrent_updated else None elif group_by == "tag": attr["torrent_tag"] = group - attr["torrent_category"] = group_attr[group]["torrent_category"] if only_one_torrent_updated else None - attr["torrent_tracker"] = group_attr[group]["torrent_tracker"] - attr["notifiarr_indexer"] = group_attr[group]["notifiarr_indexer"] + attr["torrent_category"] = group_attr[group].get("torrent_category") if only_one_torrent_updated else None + attr["torrent_tracker"] = group_attr[group].get("torrent_tracker") + attr["notifiarr_indexer"] = group_attr[group].get("notifiarr_indexer") + + for extra_attr in payload: + if extra_attr not in attr: + attr[extra_attr] = payload[extra_attr] self.config.send_notifications(attr) else: @@ -217,15 +221,15 @@ def group_notifications_by_key(payload, key): group = attr[key] if group not in group_attr: group_attr[group] = { - "function": attr["function"], - "title": attr["title"], - "body": attr["body"], - "torrent_category": attr["torrent_category"], - "torrent_tag": attr["torrent_tag"], - "torrents": [attr["torrents"][0]], - "torrent_tracker": attr["torrent_tracker"], - "notifiarr_indexer": attr["notifiarr_indexer"], + "function": attr.get("function"), + "title": attr.get("title"), + "body": attr.get("body"), + "torrent_category": attr.get("torrent_category"), + "torrent_tag": attr.get("torrent_tag"), + "torrents": [attr.get("torrents", [None])[0]], + "torrent_tracker": attr.get("torrent_tracker"), + "notifiarr_indexer": attr.get("notifiarr_indexer"), } else: - group_attr[group]["torrents"].append(attr["torrents"][0]) + group_attr[group]["torrents"].append(attr.get("torrents", [None])[0]) return group_attr From 29751d1809dd9dd7ce7ddea7f6160cfb8377dc38 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sun, 4 Jun 2023 14:57:29 -0400 Subject: [PATCH 29/40] add pre-commit to increase version --- .pre-commit-config.yaml | 8 +++++++ VERSION | 2 +- scripts/pre-commit/increase_version.sh | 30 ++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100755 scripts/pre-commit/increase_version.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c54df6cc..92c8631f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -51,3 +51,11 @@ repos: hooks: - id: flake8 args: [--config=.flake8] + - repo: local + hooks: + - id: increase-version + name: Increase version if branch contains "develop" + entry: ./scripts/pre-commit/increase_version.sh + language: script + pass_filenames: false + stages: [commit] diff --git a/VERSION b/VERSION index 249b4645..19cf3986 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0.0-develop10 +4.0.0-develop11 diff --git a/scripts/pre-commit/increase_version.sh b/scripts/pre-commit/increase_version.sh new file mode 100755 index 00000000..2909597a --- /dev/null +++ b/scripts/pre-commit/increase_version.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Check if the VERSION file is staged for modification +if git diff --cached --name-only | grep -q "VERSION"; then + echo "The VERSION file is already modified. Skipping version update." + exit 0 +fi + +# Read the current version from the VERSION file +current_version=$(cat VERSION) +echo "Current version: $current_version" +# Check if "develop" is not present in the version string +if [[ $current_version != *"develop"* ]]; then + echo "The word 'develop' is not present in the version string." + exit 0 +fi + +# Extract the version number after "develop" +version_number=$(echo "$current_version" | grep -oP '(?<=develop)\d+') + +# Increment the version number +new_version_number=$((version_number + 1)) + +# Replace the old version number with the new one +new_version=$(echo "$current_version" | sed "s/develop$version_number/develop$new_version_number/") + +# Update the VERSION file +echo "$new_version" > VERSION + +echo "Version updated to: $new_version" From f56c1e3220d85313fec34fff1db7b204661d7196 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sun, 4 Jun 2023 15:13:31 -0400 Subject: [PATCH 30/40] adds recheck to use group notifications --- VERSION | 2 +- modules/core/recheck.py | 44 +++++++++++++++++++++++++++-------------- modules/webhooks.py | 7 +++++++ 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/VERSION b/VERSION index 19cf3986..72f4c65b 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0.0-develop11 +4.0.0-develop12 diff --git a/modules/core/recheck.py b/modules/core/recheck.py index acefeb15..2ac70320 100644 --- a/modules/core/recheck.py +++ b/modules/core/recheck.py @@ -13,7 +13,14 @@ def __init__(self, qbit_manager): self.stats_resumed = 0 self.stats_rechecked = 0 + self.torrents_updated_recheck = [] # List of torrents updated + self.notify_attr_recheck = [] # List of single torrent attributes to send to notifiarr + self.torrents_updated_resume = [] # List of torrents updated + self.notify_attr_resume = [] # List of single torrent attributes to send to notifiarr + self.recheck() + self.config.webhooks_factory.notify(self.torrents_updated_resume, self.notify_attr_resume, group_by="tag") + self.config.webhooks_factory.notify(self.torrents_updated_recheck, self.notify_attr_recheck, group_by="tag") def recheck(self): """Function used to recheck paused torrents sorted by size and resume torrents that are completed""" @@ -24,30 +31,34 @@ def recheck(self): if torrent_list: for torrent in torrent_list: tracker = self.qbt.get_tags(torrent.trackers) + t_name = torrent.name + t_category = torrent.category # Resume torrent if completed if torrent.progress == 1: if torrent.max_ratio < 0 and torrent.max_seeding_time < 0: self.stats_resumed += 1 body = logger.print_line( - f"{'Not Resuming' if self.config.dry_run else 'Resuming'} [{tracker['tag']}] - {torrent.name}", + f"{'Not Resuming' if self.config.dry_run else 'Resuming'} [{tracker['tag']}] - {t_name}", self.config.loglevel, ) attr = { "function": "recheck", "title": "Resuming Torrent", "body": body, - "torrents": [torrent.name], - "torrent_category": torrent.category, + "torrents": [t_name], + "torrent_tag": tracker["tag"], + "torrent_category": t_category, "torrent_tracker": tracker["url"], "notifiarr_indexer": tracker["notifiarr"], } - self.config.send_notifications(attr) + self.torrents_updated_resume.append(t_name) + self.notify_attr_resume.append(attr) if not self.config.dry_run: torrent.resume() else: # Check to see if torrent meets AutoTorrentManagement criteria logger.debug("DEBUG: Torrent to see if torrent meets AutoTorrentManagement Criteria") - logger.debug(logger.insert_space(f"- Torrent Name: {torrent.name}", 2)) + logger.debug(logger.insert_space(f"- Torrent Name: {t_name}", 2)) logger.debug( logger.insert_space(f"-- Ratio vs Max Ratio: {torrent.ratio:.2f} < {torrent.max_ratio:.2f}", 4) ) @@ -74,42 +85,45 @@ def recheck(self): ): self.stats_resumed += 1 body = logger.print_line( - f"{'Not Resuming' if self.config.dry_run else 'Resuming'} [{tracker['tag']}] - " - f"{torrent.name}", + f"{'Not Resuming' if self.config.dry_run else 'Resuming'} [{tracker['tag']}] - " f"{t_name}", self.config.loglevel, ) attr = { "function": "recheck", "title": "Resuming Torrent", "body": body, - "torrents": [torrent.name], - "torrent_category": torrent.category, + "torrents": [t_name], + "torrent_tag": tracker["tag"], + "torrent_category": t_category, "torrent_tracker": tracker["url"], "notifiarr_indexer": tracker["notifiarr"], } - self.config.send_notifications(attr) + self.torrents_updated_resume.append(t_name) + self.notify_attr_resume.append(attr) if not self.config.dry_run: torrent.resume() # Recheck elif ( torrent.progress == 0 - and self.qbt.torrentinfo[torrent.name]["is_complete"] + and self.qbt.torrentinfo[t_name]["is_complete"] and not torrent.state_enum.is_checking ): self.stats_rechecked += 1 body = logger.print_line( - f"{'Not Rechecking' if self.config.dry_run else 'Rechecking'} [{tracker['tag']}] - {torrent.name}", + f"{'Not Rechecking' if self.config.dry_run else 'Rechecking'} [{tracker['tag']}] - {t_name}", self.config.loglevel, ) attr = { "function": "recheck", "title": "Rechecking Torrent", "body": body, - "torrents": [torrent.name], - "torrent_category": torrent.category, + "torrents": [t_name], + "torrent_tag": tracker["tag"], + "torrent_category": t_category, "torrent_tracker": tracker["url"], "notifiarr_indexer": tracker["notifiarr"], } - self.config.send_notifications(attr) + self.torrents_updated_recheck.append(t_name) + self.notify_attr_recheck.append(attr) if not self.config.dry_run: torrent.recheck() diff --git a/modules/webhooks.py b/modules/webhooks.py index 2e43f4a8..984dd266 100755 --- a/modules/webhooks.py +++ b/modules/webhooks.py @@ -178,6 +178,8 @@ def notify(self, torrents_updated=[], payload={}, group_by=""): group_attr = group_notifications_by_key(payload, "torrent_category") elif group_by == "tag": group_attr = group_notifications_by_key(payload, "torrent_tag") + elif group_by == "tracker": + group_attr = group_notifications_by_key(payload, "torrent_tracker") # group notifications by grouping attribute for group in group_attr: @@ -203,6 +205,11 @@ def notify(self, torrents_updated=[], payload={}, group_by=""): attr["torrent_category"] = group_attr[group].get("torrent_category") if only_one_torrent_updated else None attr["torrent_tracker"] = group_attr[group].get("torrent_tracker") attr["notifiarr_indexer"] = group_attr[group].get("notifiarr_indexer") + elif group_by == "tracker": + attr["torrent_tracker"] = group + attr["torrent_category"] = group_attr[group].get("torrent_category") if only_one_torrent_updated else None + attr["torrent_tag"] = group_attr[group].get("torrent_tag") if only_one_torrent_updated else None + attr["notifiarr_indexer"] = group_attr[group].get("notifiarr_indexer") for extra_attr in payload: if extra_attr not in attr: From bde5edfec8212ac2c71de9b2e5a6697d981f0c55 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sun, 4 Jun 2023 15:33:27 -0400 Subject: [PATCH 31/40] adds tag_nohl to use group notifications --- VERSION | 2 +- modules/core/tag_nohardlinks.py | 15 +++++++++++++-- scripts/pre-commit/increase_version.sh | 3 +++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 72f4c65b..c04d4c3e 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0.0-develop12 +4.0.0-develop13 diff --git a/modules/core/tag_nohardlinks.py b/modules/core/tag_nohardlinks.py index 8d783a97..21af9155 100644 --- a/modules/core/tag_nohardlinks.py +++ b/modules/core/tag_nohardlinks.py @@ -16,8 +16,17 @@ def __init__(self, qbit_manager): self.nohardlinks = qbit_manager.config.nohardlinks self.nohardlinks_tag = qbit_manager.config.nohardlinks_tag + self.torrents_updated_tagged = [] # List of torrents updated + self.notify_attr_tagged = [] # List of single torrent attributes to send to notifiarr + + self.torrents_updated_untagged = [] # List of torrents updated + self.notify_attr_untagged = [] # List of single torrent attributes to send to notifiarr + self.tag_nohardlinks() + self.config.webhooks_factory.notify(self.torrents_updated_tagged, self.notify_attr_tagged, group_by="tag") + self.config.webhooks_factory.notify(self.torrents_updated_untagged, self.notify_attr_untagged, group_by="tag") + def add_tag_no_hl(self, torrent, tracker, category): """Add tag nohardlinks_tag to torrents with no hardlinks""" body = [] @@ -40,7 +49,8 @@ def add_tag_no_hl(self, torrent, tracker, category): "torrent_tracker": tracker["url"], "notifiarr_indexer": tracker["notifiarr"], } - self.config.send_notifications(attr) + self.torrents_updated_tagged.append(torrent.name) + self.notify_attr_tagged.append(attr) def check_previous_nohardlinks_tagged_torrents(self, has_nohardlinks, torrent, tracker, category): """ @@ -69,7 +79,8 @@ def check_previous_nohardlinks_tagged_torrents(self, has_nohardlinks, torrent, t "torrent_tracker": tracker["url"], "notifiarr_indexer": tracker["notifiarr"], } - self.config.send_notifications(attr) + self.torrents_updated_untagged.append(torrent.name) + self.notify_attr_untagged.append(attr) def tag_nohardlinks(self): """Tag torrents with no hardlinks""" diff --git a/scripts/pre-commit/increase_version.sh b/scripts/pre-commit/increase_version.sh index 2909597a..c49632fb 100755 --- a/scripts/pre-commit/increase_version.sh +++ b/scripts/pre-commit/increase_version.sh @@ -15,6 +15,9 @@ if [[ $current_version != *"develop"* ]]; then exit 0 fi +# Get the version number from the HEAD commit +current_version=$(git show HEAD:VERSION 2>/dev/null) + # Extract the version number after "develop" version_number=$(echo "$current_version" | grep -oP '(?<=develop)\d+') From 3be193636c3556147760e3e8afc5f516038657f7 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sun, 4 Jun 2023 15:43:13 -0400 Subject: [PATCH 32/40] update pre-commit increase version logic --- VERSION | 2 +- scripts/pre-commit/increase_version.sh | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index c04d4c3e..71fe40d8 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0.0-develop13 +4.0.0-develop14 diff --git a/scripts/pre-commit/increase_version.sh b/scripts/pre-commit/increase_version.sh index c49632fb..5ce7754d 100755 --- a/scripts/pre-commit/increase_version.sh +++ b/scripts/pre-commit/increase_version.sh @@ -1,5 +1,13 @@ #!/bin/bash +staged_changes=$(git diff-index --cached HEAD | wc -l | awk '{print $1}') + +# Check if there are any changes staged for commit +if [ "$staged_changes" -eq 0 ]; then + echo "There are no changes staged for commit. Skipping version update." + exit 0 +fi + # Check if the VERSION file is staged for modification if git diff --cached --name-only | grep -q "VERSION"; then echo "The VERSION file is already modified. Skipping version update." From 44f69c7e552e419bfd78cb79445938a6cecc00a2 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sun, 4 Jun 2023 16:10:36 -0400 Subject: [PATCH 33/40] updates remove_unreg to use group notifications --- VERSION | 2 +- modules/core/remove_unregistered.py | 35 ++++++++++++++++++++++++----- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/VERSION b/VERSION index 71fe40d8..8bac0ea2 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0.0-develop14 +4.0.0-develop15 diff --git a/modules/core/remove_unregistered.py b/modules/core/remove_unregistered.py index 24dd0812..f030304e 100644 --- a/modules/core/remove_unregistered.py +++ b/modules/core/remove_unregistered.py @@ -39,8 +39,12 @@ def __init__(self, qbit_manager): def remove_previous_errors(self): """Removes any previous torrents that were tagged as an error but are now working.""" + torrents_updated = [] + notify_attr = [] + for torrent in self.qbt.torrentvalid: check_tags = util.get_list(torrent.tags) + t_name = torrent.name # Remove any error torrents Tags that are no longer unreachable. if self.tag_error in check_tags: tracker = self.qbt.get_tags(torrent.trackers) @@ -49,7 +53,7 @@ def remove_previous_errors(self): body += logger.print_line( f"Previous Tagged {self.tag_error} torrent currently has a working tracker.", self.config.loglevel ) - body += logger.print_line(logger.insert_space(f"Torrent Name: {torrent.name}", 3), self.config.loglevel) + body += logger.print_line(logger.insert_space(f"Torrent Name: {t_name}", 3), self.config.loglevel) body += logger.print_line(logger.insert_space(f"Removed Tag: {self.tag_error}", 4), self.config.loglevel) body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), self.config.loglevel) if not self.config.dry_run: @@ -58,13 +62,16 @@ def remove_previous_errors(self): "function": "untag_tracker_error", "title": "Untagging Tracker Error Torrent", "body": "\n".join(body), - "torrents": [torrent.name], + "torrents": [t_name], "torrent_category": torrent.category, "torrent_tag": self.tag_error, "torrent_tracker": tracker["url"], "notifiarr_indexer": tracker["notifiarr"], } - self.config.send_notifications(attr) + torrents_updated.append(t_name) + notify_attr.append(attr) + + self.config.webhooks_factory.notify(torrents_updated, notify_attr, group_by="tag") def check_for_unregistered_torrents_using_bhd_api(self, tracker, msg_up, torrent_hash): """ @@ -82,6 +89,12 @@ def check_for_unregistered_torrents_using_bhd_api(self, tracker, msg_up, torrent return False def process_torrent_issues(self): + """Process torrent issues.""" + self.torrents_updated_issue = [] # List of torrents updated + self.notify_attr_issue = [] # List of single torrent attributes to send to notifiarr + self.torrents_updated_unreg = [] # List of torrents updated + self.notify_attr_unreg = [] # List of single torrent attributes to send to notifiarr + for torrent in self.qbt.torrentissue: self.t_name = torrent.name self.t_cat = self.qbt.torrentinfo[self.t_name]["Category"] @@ -97,7 +110,10 @@ def process_torrent_issues(self): if TrackerStatus(trk.status) == TrackerStatus.NOT_WORKING: # Tag any error torrents if self.cfg_tag_error and self.tag_error not in check_tags: - self.tag_tracker_error(msg, tracker, torrent) + 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 if self.cfg_rem_unregistered: if list_in_text(msg_up, TorrentMessages.UNREGISTERED_MSGS) and not list_in_text( @@ -121,6 +137,10 @@ def rem_unregistered(self): """Remove torrents with unregistered trackers.""" self.remove_previous_errors() self.process_torrent_issues() + + self.config.webhooks_factory.notify(self.torrents_updated_issue, self.notify_attr_issue, group_by="tag") + self.config.webhooks_factory.notify(self.torrents_updated_unreg, self.notify_attr_unreg, group_by="tag") + if self.cfg_rem_unregistered: if self.stats_deleted >= 1 or self.stats_deleted_contents >= 1: if self.stats_deleted >= 1: @@ -172,7 +192,8 @@ def tag_tracker_error(self, msg, tracker, torrent): "torrent_tracker": tracker["url"], "notifiarr_indexer": tracker["notifiarr"], } - self.config.send_notifications(attr) + self.torrents_updated_issue.append(self.t_name) + self.notify_attr_issue.append(attr) if not self.config.dry_run: torrent.add_tags(tags=self.tag_error) @@ -188,6 +209,7 @@ def del_unregistered(self, msg, tracker, torrent): "torrents": [self.t_name], "torrent_category": self.t_cat, "torrent_status": msg, + "torrent_tag": tracker["tag"], "torrent_tracker": tracker["url"], "notifiarr_indexer": tracker["notifiarr"], } @@ -212,5 +234,6 @@ def del_unregistered(self, msg, tracker, torrent): body += logger.print_line(logger.insert_space("Deleted .torrent AND content files.", 8), self.config.loglevel) self.stats_deleted_contents += 1 attr["body"] = "\n".join(body) - self.config.send_notifications(attr) + self.torrents_updated_unreg.append(self.t_name) + self.notify_attr_unreg.append(attr) self.qbt.torrentinfo[self.t_name]["count"] -= 1 From dbf8b6a87fcb4a7889c7eec06a03db02d2d09893 Mon Sep 17 00:00:00 2001 From: bobokun Date: Sun, 4 Jun 2023 16:42:01 -0400 Subject: [PATCH 34/40] fixes bug in webhooks --- VERSION | 2 +- modules/webhooks.py | 15 +-------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/VERSION b/VERSION index 8bac0ea2..3ca2550b 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0.0-develop15 +4.0.0-develop16 diff --git a/modules/webhooks.py b/modules/webhooks.py index 984dd266..dcbd14d2 100755 --- a/modules/webhooks.py +++ b/modules/webhooks.py @@ -211,10 +211,6 @@ def notify(self, torrents_updated=[], payload={}, group_by=""): attr["torrent_tag"] = group_attr[group].get("torrent_tag") if only_one_torrent_updated else None attr["notifiarr_indexer"] = group_attr[group].get("notifiarr_indexer") - for extra_attr in payload: - if extra_attr not in attr: - attr[extra_attr] = payload[extra_attr] - self.config.send_notifications(attr) else: for attr in payload: @@ -227,16 +223,7 @@ def group_notifications_by_key(payload, key): for attr in payload: group = attr[key] if group not in group_attr: - group_attr[group] = { - "function": attr.get("function"), - "title": attr.get("title"), - "body": attr.get("body"), - "torrent_category": attr.get("torrent_category"), - "torrent_tag": attr.get("torrent_tag"), - "torrents": [attr.get("torrents", [None])[0]], - "torrent_tracker": attr.get("torrent_tracker"), - "notifiarr_indexer": attr.get("notifiarr_indexer"), - } + group_attr[group] = attr else: group_attr[group]["torrents"].append(attr.get("torrents", [None])[0]) return group_attr From 24eb05bd899df6b7ee32e9fd9671b047f5d5cee9 Mon Sep 17 00:00:00 2001 From: bobokun Date: Mon, 5 Jun 2023 10:36:43 -0400 Subject: [PATCH 35/40] adjust skip_qb_version_check to not notify if true --- VERSION | 2 +- modules/qbittorrent.py | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/VERSION b/VERSION index 3ca2550b..03503223 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0.0-develop16 +4.0.0-develop17 diff --git a/modules/qbittorrent.py b/modules/qbittorrent.py index ce55de35..e8394f5d 100755 --- a/modules/qbittorrent.py +++ b/modules/qbittorrent.py @@ -57,16 +57,14 @@ def __init__(self, config, params): + f"Please downgrade your qBittorrent version to {self.SUPPORTED_VERSION} to use qbit_manage." ) if ex: - self.config.notify(ex, "Qbittorrent") - logger.print_line(ex, "CRITICAL") if self.config.commands["skip_qb_version_check"]: - logger.print_line( - "Continuing because qBittorrent version check is bypassed... Please do not ask for support!" - ) + ex += "\n[BYPASS]: Continuing because qBittorrent version check is bypassed... Please do not ask for support!" + logger.print_line(ex, "WARN") else: + self.config.notify(ex, "Qbittorrent") + logger.print_line(ex, "CRITICAL") sys.exit(0) - else: - logger.info("Qbt Connection Successful") + logger.info("Qbt Connection Successful") except LoginFailed as exc: ex = "Qbittorrent Error: Failed to login. Invalid username/password." self.config.notify(ex, "Qbittorrent") From 81574917e819fa132826a1d3a92b2c57e7e1de72 Mon Sep 17 00:00:00 2001 From: bakerboy448 <55419169+bakerboy448@users.noreply.github.com> Date: Mon, 5 Jun 2023 12:55:28 -0500 Subject: [PATCH 36/40] add orphaned data config details --- config/config.yml.sample | 1 + 1 file changed, 1 insertion(+) diff --git a/config/config.yml.sample b/config/config.yml.sample index 6e9f9644..40b8e5de 100755 --- a/config/config.yml.sample +++ b/config/config.yml.sample @@ -41,6 +41,7 @@ directory: # Must be set if you're running qbit_manage locally and qBittorrent/cross_seed is in a docker # recycle_bin var: # Path of the RecycleBin folder. Default location is set to remote_dir/.RecycleBin # torrents_dir var: # Path of the your qbittorrent torrents directory. Required for `save_torrents` attribute in recyclebin + # orphaned_dir var: # Path of the the Orphaned Data folder. This is similar to RecycleBin, but only for orphaned data. cross_seed: "/your/path/here/" root_dir: "/data/torrents/" remote_dir: "/mnt/user/data/torrents/" From 9572f68e4b83a64eb7443ad54ebf660c44123901 Mon Sep 17 00:00:00 2001 From: bobokun Date: Mon, 5 Jun 2023 15:27:49 -0400 Subject: [PATCH 37/40] fixes bug in group notifications adds trace logs for share limits --- VERSION | 2 +- modules/core/remove_unregistered.py | 2 +- modules/core/share_limits.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 03503223..341c3a9c 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0.0-develop17 +4.0.0-develop18 diff --git a/modules/core/remove_unregistered.py b/modules/core/remove_unregistered.py index f030304e..d11d6f47 100644 --- a/modules/core/remove_unregistered.py +++ b/modules/core/remove_unregistered.py @@ -209,7 +209,7 @@ def del_unregistered(self, msg, tracker, torrent): "torrents": [self.t_name], "torrent_category": self.t_cat, "torrent_status": msg, - "torrent_tag": tracker["tag"], + "torrent_tag": ", ".join(tracker["tag"]), "torrent_tracker": tracker["url"], "notifiarr_indexer": tracker["notifiarr"], } diff --git a/modules/core/share_limits.py b/modules/core/share_limits.py index 7ea52e16..cb31bdcb 100644 --- a/modules/core/share_limits.py +++ b/modules/core/share_limits.py @@ -247,6 +247,7 @@ def assign_torrents_to_group(self, torrent_list): tags = util.get_list(torrent.tags) category = torrent.category or "" grouping = self.get_share_limit_group(tags, category) + logger.trace(f"Torrent: {torrent.name} [{torrent.hash}] - Share Limit Group: {grouping}") if grouping: self.share_limits_config[grouping]["torrents"].append(torrent) From 0a85c8c5d48895fa05e67a898b3a9316da89beb6 Mon Sep 17 00:00:00 2001 From: bobokun Date: Mon, 5 Jun 2023 16:40:46 -0400 Subject: [PATCH 38/40] better trace logging in share_limits --- VERSION | 2 +- modules/core/share_limits.py | 36 ++++++++++++++++++++---------------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/VERSION b/VERSION index 341c3a9c..cb3b9208 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0.0-develop18 +4.0.0-develop19 diff --git a/modules/core/share_limits.py b/modules/core/share_limits.py index cb31bdcb..e0e7e7e0 100644 --- a/modules/core/share_limits.py +++ b/modules/core/share_limits.py @@ -174,25 +174,29 @@ def update_share_limits_for_group(self, group_name, group_config, torrents): if group_config["limit_upload_speed"] == 0: group_config["limit_upload_speed"] = -1 check_limit_upload_speed = group_config["limit_upload_speed"] != torrent_upload_limit - if ( - check_max_ratio or check_max_seeding_time or check_limit_upload_speed - ) and t_hash not in self.torrent_hash_checked: + hash_not_prev_checked = t_hash not in self.torrent_hash_checked + logger.trace(f"Torrent: {t_name} [Hash: {t_hash}]") + logger.trace(f"Torrent Category: {torrent.category}") + logger.trace(f"Torrent Tags: {torrent.tags}") + logger.trace(f"Grouping: {group_name}") + logger.trace(f"Config Max Ratio vs Torrent Max Ratio:{group_config['max_ratio']} vs {torrent.max_ratio}") + logger.trace(f"check_max_ratio: {check_max_ratio}") + logger.trace( + "Config Max Seeding Time vs Torrent Max Seeding Time: " + f"{group_config['max_seeding_time']} vs {torrent.max_seeding_time}" + ) + logger.trace(f"check_max_seeding_time: {check_max_seeding_time}") + logger.trace( + "Config Limit Upload Speed vs Torrent Limit Upload Speed: " + f"{group_config['limit_upload_speed']} vs {torrent_upload_limit}" + ) + logger.trace(f"check_limit_upload_speed: {check_limit_upload_speed}") + logger.trace(f"hash_not_prev_checked: {hash_not_prev_checked}") + if (check_max_ratio or check_max_seeding_time or check_limit_upload_speed) and hash_not_prev_checked: if "MinSeedTimeNotReached" not in torrent.tags: self.group_tag = f"{group_name}{self.share_limits_suffix_tag}" if group_config["add_group_to_tag"] else None 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.trace(f"Torrent Category: {torrent.category}") - logger.trace(f"Torrent Tags: {torrent.tags}") - logger.trace(f"Grouping: {group_name}") - logger.trace(f"Config Max Ratio vs Torrent Max Ratio:{group_config['max_ratio']} vs {torrent.max_ratio}") - logger.trace( - "Config Max Seeding Time vs Torrent Max Seeding Time: " - f"{group_config['max_seeding_time']} vs {torrent.max_seeding_time}" - ) - logger.trace( - "Config Limit Upload Speed vs Torrent Limit Upload Speed: " - f"{group_config['limit_upload_speed']} vs {torrent_upload_limit}" - ) if self.group_tag: logger.print_line(logger.insert_space(f"Added Tag: {self.group_tag}", 8), self.config.loglevel) self.tag_and_update_share_limits_for_torrent(torrent, group_config) @@ -247,7 +251,7 @@ def assign_torrents_to_group(self, torrent_list): tags = util.get_list(torrent.tags) category = torrent.category or "" grouping = self.get_share_limit_group(tags, category) - logger.trace(f"Torrent: {torrent.name} [{torrent.hash}] - Share Limit Group: {grouping}") + logger.trace(f"Torrent: {torrent.name} [Hash: {torrent.hash}] - Share Limit Group: {grouping}") if grouping: self.share_limits_config[grouping]["torrents"].append(torrent) From 148a1a2176e5df44d132af89f3c82919bafb95d4 Mon Sep 17 00:00:00 2001 From: bobokun Date: Mon, 5 Jun 2023 22:38:05 -0400 Subject: [PATCH 39/40] increase log size and backup count --- VERSION | 2 +- modules/core/share_limits.py | 2 -- modules/logs.py | 6 +++--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/VERSION b/VERSION index cb3b9208..977caffe 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0.0-develop19 +4.0.0-develop20 diff --git a/modules/core/share_limits.py b/modules/core/share_limits.py index e0e7e7e0..2e5b6159 100644 --- a/modules/core/share_limits.py +++ b/modules/core/share_limits.py @@ -202,8 +202,6 @@ def update_share_limits_for_group(self, group_name, group_config, torrents): self.tag_and_update_share_limits_for_torrent(torrent, group_config) self.stats_tagged += 1 self.torrents_updated.append(t_name) - else: - self.share_limits_config[group_name]["torrents"].remove(torrent) # Cleanup torrents if the torrent meets the criteria for deletion and cleanup is enabled if group_config["cleanup"]: diff --git a/modules/logs.py b/modules/logs.py index b425544e..d634c2ec 100755 --- a/modules/logs.py +++ b/modules/logs.py @@ -68,9 +68,9 @@ def clear_errors(self): """Clear saved errors""" self.saved_errors = [] - def _get_handler(self, log_file, count=3): + def _get_handler(self, log_file, count=5): """Get handler for log file""" - max_bytes = 1024 * 1024 * 2 + max_bytes = 1024 * 1024 * 10 _handler = RotatingFileHandler(log_file, delay=True, mode="w", maxBytes=max_bytes, backupCount=count, encoding="utf-8") self._formatter(handler=_handler) # if os.path.isfile(log_file): @@ -88,7 +88,7 @@ def _formatter(self, handler=None, border=True, log_only=False, space=False): def add_main_handler(self): """Add main handler to logger""" - self.main_handler = self._get_handler(self.main_log, count=9) + self.main_handler = self._get_handler(self.main_log, count=19) self.main_handler.addFilter(fmt_filter) self._logger.addHandler(self.main_handler) From 65086a6b7dae6acc85d7d82b57213aff773d43ae Mon Sep 17 00:00:00 2001 From: bobokun Date: Tue, 6 Jun 2023 17:46:05 -0400 Subject: [PATCH 40/40] v4.0.0 --- CHANGELOG | 21 ++++++++++++++------- VERSION | 2 +- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 91b3f3b5..bcf014ed 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,16 +1,23 @@ # Requirements Updated -- Updates ruamel.yaml to 0.17.30 +- Updates ruamel.yaml to 0.17.31 - Updates qbitorrent-api to 2023.5.48 +- Separate out dev requirements into requirements-dev.txt +# Breaking Changes +- `tag_nohardlinks` only updates/removes `noHL` tag. **It does not modify or cleanup share_limits anymore.** +- `tag_update` only adds tracker tags to torrent. **It does not modify or cleanup share_limits anymore.** +- Please remove any references to share_limits from your configuration in the tracker/nohardlinks section +- Migration guide can be followed here: [V4 Migration Guide](https://github.com/StuffAnThings/qbit_manage/wiki/v4-Migration-Guide) +- Webhook payloads changed (See [webhooks](https://github.com/StuffAnThings/qbit_manage/wiki/Config-Setup#webhooks) for updated payload) # New Features - Adds new command `share_limits`, `--share-limits` , `QBT_SHARE_LIMITS=True` to update share limits based on tags/categories specified per group (Closes #88, Closes #306, Closes #259, Closes #308, Closes #137) +- See [Config Setup - share_limits](https://github.com/StuffAnThings/qbit_manage/wiki/Config-Setup#share_limits) for more details - Adds new command `skip_qb_version_check`, `--skip-qb-version-check`, `QBT_SKIP_QB_VERSION_CHECK` to bypass qbitorrent compatibility check (unsupported - Thanks to @ftc2 #307) -# Breaking Changes -- `tag_nohardlinks` only updates/removes `noHL` tag. It does not modify or cleanup share_limits anymore. -- `tag_update` only adds tracker tags to torrent. It does not modify or cleanup share_limits anymore. -- Please remove any references to share_limits from your configuration in the tracker/nohardlinks section - +- Updates to webhook notifications to group notifications when a function updates more than 10 Torrents. +- Adds new webhooks for `share_limits` +- Adds rate limit to webhook notifications (1 msg/sec) # Bug Fixes - Fixes #302 +- Fixes #317 -**Full Changelog**: https://github.com/StuffAnThings/qbit_manage/compare/v3.6.3...v3.7.0 +**Full Changelog**: https://github.com/StuffAnThings/qbit_manage/compare/v3.6.4...v4.0.0 diff --git a/VERSION b/VERSION index 977caffe..fcdb2e10 100755 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.0.0-develop20 +4.0.0