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)
This commit is contained in:
bobokun 2023-05-30 21:26:54 -04:00
parent fe56320ec3
commit fbf9cb59e9
No known key found for this signature in database
GPG key ID: B73932169607D927
10 changed files with 677 additions and 564 deletions

View file

@ -1,8 +1,16 @@
# Requirements Updated # 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 # Bug Fixes
- Fixes #302 - 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

View file

@ -1 +1 @@
3.6.4-develop1 3.6.4-develop2

View file

@ -14,6 +14,8 @@ commands:
tag_tracker_error: False tag_tracker_error: False
rem_orphaned: False rem_orphaned: False
tag_nohardlinks: False tag_nohardlinks: False
share_limits: False
skip_qb_version_check: False
skip_cleanup: False skip_cleanup: False
qbt: qbt:
@ -26,6 +28,7 @@ settings:
force_auto_tmm: False # Will force qBittorrent to enable Automatic Torrent Management for each torrent. 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. 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. 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. 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 - noHL
- issue - issue
@ -68,14 +71,6 @@ tracker:
# <Tracker URL Keyword>: # <MANDATORY> This is the keyword in the tracker url # <Tracker URL Keyword>: # <MANDATORY> This is the keyword in the tracker url
# <MANDATORY> Set tag name. Can be a list of tags or a single tag # <MANDATORY> Set tag name. Can be a list of tags or a single tag
# tag: <Tag Name> # tag: <Tag Name>
# <OPTIONAL> 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
# <OPTIONAL> 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
# <OPTIONAL> 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
# <OPTIONAL> Will limit the upload speed KiB/s (KiloBytes/second) (-1 means no limit)
# limit_upload_speed: 150
# <OPTIONAL> Set this to the notifiarr react name. This is used to add indexer reactions to the notifications sent by Notifiarr # <OPTIONAL> Set this to the notifiarr react name. This is used to add indexer reactions to the notifications sent by Notifiarr
# notifiarr: <notifiarr indexer> # notifiarr: <notifiarr indexer>
animebytes.tv: animebytes.tv:
@ -86,10 +81,6 @@ tracker:
- Avistaz - Avistaz
- tag2 - tag2
- tag3 - tag3
max_ratio: 5.0
max_seeding_time: 129600
min_seeding_time: 30400
limit_upload_speed: 150
notifiarr: avistaz notifiarr: avistaz
beyond-hd: beyond-hd:
tag: [Beyond-HD, tag2, tag3] tag: [Beyond-HD, tag2, tag3]
@ -101,14 +92,11 @@ tracker:
tag: CartoonChaos tag: CartoonChaos
digitalcore: digitalcore:
tag: DigitalCore tag: DigitalCore
max_ratio: 5.0
notifiarr: digitalcore notifiarr: digitalcore
gazellegames: gazellegames:
tag: GGn tag: GGn
limit_upload_speed: 150
hdts: hdts:
tag: HDTorrents tag: HDTorrents
max_seeding_time: 129600
landof.tv: landof.tv:
tag: BroadcasTheNet tag: BroadcasTheNet
notifiarr: broadcasthenet notifiarr: broadcasthenet
@ -145,48 +133,66 @@ nohardlinks:
- Beyond-HD - Beyond-HD
- AnimeBytes - AnimeBytes
- MaM - MaM
# <OPTIONAL> 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
# <OPTIONAL> 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
# <OPTIONAL> 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
# <OPTIONAL> Limit Upload Speed var: Will limit the upload speed KiB/s (KiloBytes/second) (`-1` : No Limit)
limit_upload_speed:
# <OPTIONAL> 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
# <OPTIONAL> 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. # Can have additional categories set with separate ratio/seeding times defined.
series-completed: series-completed:
# <OPTIONAL> exclude_tags var: Will exclude torrents with any of the following tags when searching through the category. # <OPTIONAL> exclude_tags var: Will exclude torrents with any of the following tags when searching through the category.
exclude_tags: exclude_tags:
- Beyond-HD - Beyond-HD
- BroadcasTheNet - BroadcasTheNet
# <OPTIONAL> 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 share_limits:
# <OPTIONAL> max_ratio var: Will set the torrent Maximum share ratio until torrent is stopped from seeding/uploading. # Control how torrent share limits are set depending on the priority of your grouping
# 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. # This variable is mandatory and is a text defining the name of your grouping. This can be any string you want
# Uses the larger value of the noHL Category or Tracker specific setting. noHL:
max_ratio: 4.0 # <MANDATORY> priority: <int/float> # This is the priority of your grouping. The lower the number the higher the priority
# <OPTIONAL> max seeding time var: Will set the torrent Maximum seeding time (min) until torrent is stopped from seeding. priority: 1
# 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. # <OPTIONAL> tags: <list> # Filter the group based on one or more tags. Multiple tags are checked with an AND condition
# Uses the larger value of the noHL Category or Tracker specific setting. tags:
max_seeding_time: 86400 - noHL
# <OPTIONAL> Limit Upload Speed var: Will limit the upload speed KiB/s (KiloBytes/second) (`-1` : No Limit) # <OPTIONAL> exclude_tags: <list> # Filter by excluding one or more tags. Multiple exclude_tags are checked with an AND condition
limit_upload_speed: # This is useful to combine with the category filter to exclude one or more tags from an entire category
# <OPTIONAL> min seeding time var: Will prevent torrent deletion by cleanup variable if torrent has not yet minimum seeding time (min). exclude_tags:
# 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. - Beyond-HD
# Uses the larger value of the noHL Category or Tracker specific setting. # <OPTIONAL> categories: <list> # 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
# <OPTIONAL> max_ratio <float>: 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
# <OPTIONAL> max_seeding_time <int>: 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
# <OPTIONAL> min_seeding_time <int>: 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 min_seeding_time: 43200
# <OPTIONAL> 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 # <OPTIONAL> Limit Upload Speed <int>: Will limit the upload speed KiB/s (KiloBytes/second) (`-1` : No Limit)
resume_torrent_after_untagging_noHL: false limit_upload_speed: 0
# <OPTIONAL> cleanup <bool>: WARNING!! Setting this as true Will remove and delete contents of any torrents that satisfies the share limits
cleanup: false
# <OPTIONAL> resume_torrent_after_change <bool>: This variable will resume your torrent after changing share limits. Default is true
resume_torrent_after_change: true
# <OPTIONAL> add_group_to_tag <bool>: 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: 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 # Recycle Bin method of deletion will move files into the recycle bin (Located in /root_dir/.RecycleBin) instead of directly deleting them in qbit

View file

@ -1,4 +1,6 @@
"""Apprise notification class""" """Apprise notification class"""
import time
from modules import util from modules import util
from modules.util import Failed from modules.util import Failed
@ -14,5 +16,6 @@ class Apprise:
logger.secret(self.api_url) logger.secret(self.api_url)
self.notify_url = ",".join(params["notify_url"]) self.notify_url = ",".join(params["notify_url"])
response = self.config.get(self.api_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: if response.status_code != 200:
raise Failed(f"Apprise Error: Unable to connect to Apprise using {self.api_url}") raise Failed(f"Apprise Error: Unable to connect to Apprise using {self.api_url}")

View file

@ -3,6 +3,7 @@ import os
import re import re
import stat import stat
import time import time
from collections import OrderedDict
import requests import requests
from retrying import retry from retrying import retry
@ -28,6 +29,7 @@ COMMANDS = [
"tag_tracker_error", "tag_tracker_error",
"rem_orphaned", "rem_orphaned",
"tag_nohardlinks", "tag_nohardlinks",
"share_limits",
"skip_cleanup", "skip_cleanup",
"skip_qb_version_check", "skip_qb_version_check",
"dry_run", "dry_run",
@ -82,6 +84,7 @@ class Config:
logger.debug(f" --tag-tracker-error (QBT_TAG_TRACKER_ERROR): {self.commands['tag_tracker_error']}") 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" --rem-orphaned (QBT_REM_ORPHANED): {self.commands['rem_orphaned']}")
logger.debug(f" --tag-nohardlinks (QBT_TAG_NOHARDLINKS): {self.commands['tag_nohardlinks']}") 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-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" --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']}") logger.debug(f" --dry-run (QBT_DRY_RUN): {self.commands['dry_run']}")
@ -136,6 +139,9 @@ class Config:
self.data["webhooks"] = temp self.data["webhooks"] = temp
if "bhd" in self.data: if "bhd" in self.data:
self.data["bhd"] = self.data.pop("bhd") 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.dry_run = self.commands["dry_run"]
self.loglevel = "DRYRUN" if self.dry_run else "INFO" self.loglevel = "DRYRUN" if self.dry_run else "INFO"
self.session = requests.Session() self.session = requests.Session()
@ -148,10 +154,14 @@ class Config:
self.data, "tracker_error_tag", parent="settings", default="issue" self.data, "tracker_error_tag", parent="settings", default="issue"
), ),
"nohardlinks_tag": self.util.check_for_attribute(self.data, "nohardlinks_tag", parent="settings", default="noHL"), "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.tracker_error_tag = self.settings["tracker_error_tag"]
self.nohardlinks_tag = self.settings["nohardlinks_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"] default_ignore_tags = [self.nohardlinks_tag, self.tracker_error_tag, "cross-seed"]
self.settings["ignoreTags_OnUpdate"] = self.util.check_for_attribute( self.settings["ignoreTags_OnUpdate"] = self.util.check_for_attribute(
@ -167,6 +177,7 @@ class Config:
"tag_tracker_error": None, "tag_tracker_error": None,
"rem_orphaned": None, "rem_orphaned": None,
"tag_nohardlinks": None, "tag_nohardlinks": None,
"share_limits": None,
"cleanup_dirs": None, "cleanup_dirs": None,
} }
@ -333,7 +344,7 @@ class Config:
var_type="int", var_type="int",
min_int=-1, min_int=-1,
do_print=False, do_print=False,
default=0, default=-1,
save=False, save=False,
) )
self.nohardlinks[cat]["resume_torrent_after_untagging_noHL"] = self.util.check_for_attribute( self.nohardlinks[cat]["resume_torrent_after_untagging_noHL"] = self.util.check_for_attribute(
@ -357,6 +368,153 @@ class Config:
self.notify(err, "Config") self.notify(err, "Config")
raise Failed(err) 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 # Add RecycleBin
self.recyclebin = {} self.recyclebin = {}
self.recyclebin["enabled"] = self.util.check_for_attribute( self.recyclebin["enabled"] = self.util.check_for_attribute(

View file

@ -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

View file

@ -1,5 +1,3 @@
import os
from modules import util from modules import util
logger = util.logger logger = util.logger
@ -12,12 +10,7 @@ class TagNoHardLinks:
self.client = qbit_manager.client self.client = qbit_manager.client
self.stats_tagged = 0 # counter for the number of torrents that has no hardlinks 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_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.root_dir = qbit_manager.config.root_dir
self.remote_dir = qbit_manager.config.remote_dir self.remote_dir = qbit_manager.config.remote_dir
self.nohardlinks = qbit_manager.config.nohardlinks self.nohardlinks = qbit_manager.config.nohardlinks
@ -25,31 +18,16 @@ class TagNoHardLinks:
self.tag_nohardlinks() 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""" """Add tag nohardlinks_tag to torrents with no hardlinks"""
body = [] body = []
body.append(logger.insert_space(f"Torrent Name: {torrent.name}", 3)) 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)) body.append(logger.insert_space(f"Added Tag: {self.nohardlinks_tag}", 6))
title = "Tagging Torrents with No Hardlinks" title = "Tagging Torrents with No Hardlinks"
else:
title = "Changing Share Ratio of Torrents with No Hardlinks"
body.append(logger.insert_space(f'Tracker: {tracker["url"]}', 8)) body.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: if not self.config.dry_run:
torrent.resume() torrent.add_tags(self.nohardlinks_tag)
body.extend(body_tags_and_limits) self.stats_tagged += 1
for rcd in body: for rcd in body:
logger.print_line(rcd, self.config.loglevel) logger.print_line(rcd, self.config.loglevel)
attr = { attr = {
@ -61,75 +39,9 @@ class TagNoHardLinks:
"torrent_tag": self.nohardlinks_tag, "torrent_tag": self.nohardlinks_tag,
"torrent_tracker": tracker["url"], "torrent_tracker": tracker["url"],
"notifiarr_indexer": tracker["notifiarr"], "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) 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
def check_previous_nohardlinks_tagged_torrents(self, has_nohardlinks, torrent, tracker, category): def check_previous_nohardlinks_tagged_torrents(self, has_nohardlinks, torrent, tracker, category):
""" """
Checks for any previous torrents that were tagged with the nohardlinks tag and have since had hardlinks added. Checks for any previous torrents that were tagged with the nohardlinks tag and have since had hardlinks added.
@ -145,28 +57,8 @@ class TagNoHardLinks:
) )
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"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(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: if not self.config.dry_run:
torrent.remove_tags(tags=self.nohardlinks_tag) 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 = { attr = {
"function": "untag_nohardlinks", "function": "untag_nohardlinks",
"title": "Untagging Previous Torrents that now have hardlinks", "title": "Untagging Previous Torrents that now have hardlinks",
@ -176,9 +68,6 @@ class TagNoHardLinks:
"torrent_tag": self.nohardlinks_tag, "torrent_tag": self.nohardlinks_tag,
"torrent_tracker": tracker["url"], "torrent_tracker": tracker["url"],
"notifiarr_indexer": tracker["notifiarr"], "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) self.config.send_notifications(attr)
@ -211,120 +100,17 @@ class TagNoHardLinks:
# Cleans up previously tagged nohardlinks_tag torrents that no longer have hardlinks # Cleans up previously tagged nohardlinks_tag torrents that no longer have hardlinks
if has_nohardlinks: if has_nohardlinks:
tracker = self.qbt.get_tags(torrent.trackers) 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 # Will only tag new torrents that don't have nohardlinks_tag tag
if self.nohardlinks_tag not in torrent.tags: if self.nohardlinks_tag not in torrent.tags:
self.add_tag_no_hl( self.add_tag_no_hl(
torrent=torrent, torrent=torrent,
tracker=tracker, tracker=tracker,
category=category, 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.check_previous_nohardlinks_tagged_torrents(has_nohardlinks, torrent, tracker, category)
self.cleanup_tagged_torrents_with_no_hardlinks(category)
if self.stats_tagged >= 1: if self.stats_tagged >= 1:
logger.print_line( 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 '.'}", f".torrent{'s.' if self.stats_tagged > 1 else '.'}",
self.config.loglevel, self.config.loglevel,
) )
@ -333,19 +119,7 @@ class TagNoHardLinks:
if self.stats_untagged >= 1: if self.stats_untagged >= 1:
logger.print_line( logger.print_line(
f"{'Did not delete' if self.config.dry_run else 'Deleted'} " 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 '.'}", f".torrent{'s.' if self.stats_untagged > 1 else '.'}",
self.config.loglevel, 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,
)

View file

@ -9,6 +9,7 @@ class Tags:
self.config = qbit_manager.config self.config = qbit_manager.config
self.client = qbit_manager.client self.client = qbit_manager.client
self.stats = 0 self.stats = 0
self.share_limits_suffix_tag = qbit_manager.config.share_limits_suffix_tag # suffix tag for share limits
self.tags() self.tags()
@ -17,7 +18,8 @@ class Tags:
ignore_tags = self.config.settings["ignoreTags_OnUpdate"] ignore_tags = self.config.settings["ignoreTags_OnUpdate"]
logger.separator("Updating Tags", space=False, border=False) logger.separator("Updating Tags", space=False, border=False)
for torrent in self.qbt.torrent_list: 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): 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) tracker = self.qbt.get_tags(torrent.trackers)
if tracker["tag"]: if tracker["tag"]:
@ -29,15 +31,8 @@ class Tags:
self.config.loglevel, self.config.loglevel,
) )
body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), self.config.loglevel) body += logger.print_line(logger.insert_space(f'Tracker: {tracker["url"]}', 8), self.config.loglevel)
body.extend( if not self.config.dry_run:
self.qbt.set_tags_and_limits( torrent.add_tags(tracker["tag"])
torrent,
tracker["max_ratio"],
tracker["max_seeding_time"],
tracker["limit_upload_speed"],
tracker["tag"],
)
)
category = self.qbt.get_category(torrent.save_path) if torrent.category == "" else torrent.category category = self.qbt.get_category(torrent.save_path) if torrent.category == "" else torrent.category
attr = { attr = {
"function": "tag_update", "function": "tag_update",
@ -48,9 +43,6 @@ class Tags:
"torrent_tag": ", ".join(tracker["tag"]), "torrent_tag": ", ".join(tracker["tag"]),
"torrent_tracker": tracker["url"], "torrent_tracker": tracker["url"],
"notifiarr_indexer": tracker["notifiarr"], "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) self.config.send_notifications(attr)
if self.stats >= 1: if self.stats >= 1:

View file

@ -1,7 +1,6 @@
"""Qbittorrent Module""" """Qbittorrent Module"""
import os import os
import sys import sys
from datetime import timedelta
from qbittorrentapi import APIConnectionError from qbittorrentapi import APIConnectionError
from qbittorrentapi import Client from qbittorrentapi import Client
@ -25,7 +24,7 @@ class Qbt:
SUPPORTED_VERSION = Version.latest_supported_app_version() SUPPORTED_VERSION = Version.latest_supported_app_version()
MIN_SUPPORTED_VERSION = "v4.3.0" 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): def __init__(self, config, params):
self.config = config self.config = config
@ -202,142 +201,11 @@ class Qbt:
"""Get torrents from qBittorrent""" """Get torrents from qBittorrent"""
return self.client.torrents.info(**params) 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): def get_tags(self, trackers):
"""Get tags from config file based on keyword""" """Get tags from config file based on keyword"""
urls = [x.url for x in trackers if x.url.startswith("http")] urls = [x.url for x in trackers if x.url.startswith("http")]
tracker = {} tracker = {}
tracker["tag"] = None 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["notifiarr"] = None
tracker["url"] = None tracker["url"] = None
tracker_other_tag = self.config.util.check_for_attribute( tracker_other_tag = self.config.util.check_for_attribute(
@ -376,76 +244,6 @@ class Qbt:
self.config.data["tracker"][tag_url]["tag"] = [tag_url] self.config.data["tracker"][tag_url]["tag"] = [tag_url]
if isinstance(tracker["tag"], str): if isinstance(tracker["tag"], str):
tracker["tag"] = [tracker["tag"]] 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( tracker["notifiarr"] = self.config.util.check_for_attribute(
self.config.data, self.config.data,
"notifiarr", "notifiarr",
@ -458,50 +256,6 @@ class Qbt:
return tracker return tracker
if tracker_other_tag: if tracker_other_tag:
tracker["tag"] = 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( tracker["notifiarr"] = self.config.util.check_for_attribute(
self.config.data, self.config.data,
"notifiarr", "notifiarr",

View file

@ -141,6 +141,16 @@ parser.add_argument(
"When files get upgraded they no longer become linked with your media therefore will be tagged with a new tag noHL. " "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.", "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( parser.add_argument(
"-sc", "-sc",
"--skip-cleanup", "--skip-cleanup",
@ -237,6 +247,7 @@ rem_unregistered = get_arg("QBT_REM_UNREGISTERED", args.rem_unregistered, arg_bo
tag_tracker_error = get_arg("QBT_TAG_TRACKER_ERROR", args.tag_tracker_error, arg_bool=True) 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) 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) 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_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) 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) dry_run = get_arg("QBT_DRY_RUN", args.dry_run, arg_bool=True)
@ -285,6 +296,7 @@ for v in [
"tag_tracker_error", "tag_tracker_error",
"rem_orphaned", "rem_orphaned",
"tag_nohardlinks", "tag_nohardlinks",
"share_limits",
"skip_cleanup", "skip_cleanup",
"skip_qb_version_check", "skip_qb_version_check",
"dry_run", "dry_run",
@ -329,6 +341,7 @@ from modules.core.cross_seed import CrossSeed # noqa
from modules.core.recheck import ReCheck # noqa from modules.core.recheck import ReCheck # noqa
from modules.core.tag_nohardlinks import TagNoHardLinks # noqa from modules.core.tag_nohardlinks import TagNoHardLinks # noqa
from modules.core.remove_orphaned import RemoveOrphaned # noqa from modules.core.remove_orphaned import RemoveOrphaned # noqa
from modules.core.share_limits import ShareLimits # noqa
def my_except_hook(exctype, value, tbi): def my_except_hook(exctype, value, tbi):
@ -458,8 +471,13 @@ def start():
stats["tagged"] += no_hardlinks.stats_tagged stats["tagged"] += no_hardlinks.stats_tagged
stats["tagged_noHL"] += no_hardlinks.stats_tagged stats["tagged_noHL"] += no_hardlinks.stats_tagged
stats["untagged_noHL"] += no_hardlinks.stats_untagged 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 # Remove Orphaned Files
if cfg.commands["rem_orphaned"]: if cfg.commands["rem_orphaned"]:
@ -583,6 +601,7 @@ if __name__ == "__main__":
logger.debug(f" --tag-tracker-error (QBT_TAG_TRACKER_ERROR): {tag_tracker_error}") 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" --rem-orphaned (QBT_REM_ORPHANED): {rem_orphaned}")
logger.debug(f" --tag-nohardlinks (QBT_TAG_NOHARDLINKS): {tag_nohardlinks}") 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-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" --skip-qb-version-check (QBT_SKIP_QB_VERSION_CHECK): {skip_qb_version_check}")
logger.debug(f" --dry-run (QBT_DRY_RUN): {dry_run}") logger.debug(f" --dry-run (QBT_DRY_RUN): {dry_run}")