feat(tags): add support for tagging private torrents

Introduces a new `private_tag` setting to automatically tag torrents
identified as private.

Changes include:
- Added `is_torrent_private` method to check torrent privacy status via
  attributes or tracker messages
- Updated tagging logic to apply the configured private tag
- Added `private_tag` to config, docs, and Web UI schema
- Bumped version to 4.6.5-develop11
- Adds [FR]: Allow tagging by "Private" tracker or otherwise
Fixes #883
This commit is contained in:
bobokun 2025-11-29 07:33:48 -05:00 committed by bobokun
parent 5b9d4d806e
commit 1570c01b4c
7 changed files with 257 additions and 150 deletions

View file

@ -1 +1 @@
4.6.5-develop10
4.6.5-develop11

View file

@ -33,6 +33,7 @@ settings:
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.
stalled_tag: stalledDL # Will set the tag of any torrents stalled downloading.
private_tag: null # Will set the tag of any torrents that are private. (Set to null to disable)
share_limits_tag: ~share_limit # Will add this tag when applying share limits to provide an easy way to filter torrents by share limit group/priority for each torrent
share_limits_min_seeding_time_tag: MinSeedTimeNotReached # Tag to be added to torrents that have not yet reached the minimum seeding time
share_limits_min_num_seeds_tag: MinSeedsNotMet # Tag to be added to torrents that have not yet reached the minimum number of seeds

View file

@ -65,6 +65,7 @@ This section defines any settings defined in the configuration.
| `force_auto_tmm_ignore_tags` | Torrents with these tags will be ignored when force_auto_tmm is enabled. | | <center></center> |
| `tracker_error_tag` | Define the tag of any torrents that do not have a working tracker. (Used in `--tag-tracker-error`) | issue | <center></center> |
| `nohardlinks_tag` | Define the tag of any torrents that don't have hardlinks (Used in `--tag-nohardlinks`) | noHL | <center></center> |
| `private_tag` | Define the tag of any torrents that are private. | None | <center></center> |
| `share_limits_tag` | Will add this tag when applying share limits to provide an easy way to filter torrents by share limit group/priority for each torrent. For example, if you have a share-limit group `cross-seed` with a priority of 2 and the default share_limits_tag `~share_limits` would add the tag `~share_limit_2.cross-seed` (Used in `--share-limits`) | ~share_limit | <center></center> |
| `share_limits_min_seeding_time_tag` | Will add this tag when applying share limits to torrents that have not yet reached the minimum seeding time (Used in `--share-limits`) | MinSeedTimeNotReached | <center></center> |
| `share_limits_min_num_seeds_tag` | Will add this tag when applying share limits to torrents that have not yet reached the minimum number of seeds (Used in `--share-limits`) | MinSeedsNotMet | <center></center> |
@ -306,13 +307,15 @@ Provide webhook notifications based on event triggers
Payload will be sent on any errors
```yaml
{
"function": "run_error", // Webhook Trigger keyword
"title": str, // Title of the Payload
"body": str, // Error Message of the Payload
"critical": bool, // Critical Error
"type": str // severity of error
}
{ "function": "run_error", ? // Webhook Trigger keyword
"title"
: str, ? // Title of the Payload
"body"
: str, ? // Error Message of the Payload
"critical"
: bool, ? // Critical Error
"type"
: str // severity of error }
```
### **Run Start Notifications**
@ -320,16 +323,21 @@ Payload will be sent on any errors
Payload will be sent at the start of the run
```yaml
{
"function": "run_start", // Webhook Trigger keyword
"title": str, // Title of the Payload
"body": str, // Message of the Payload
"start_time": str, // Time Run is started Format "YYYY-mm-dd HH:MM:SS"
"dry_run": bool, // Dry-Run
"web_api_used": bool, // Indicates whether the run was initiated via the Web API (true) or not (false).
"commands": list, // List of commands that that will be ran
"execution_options": list // List of eecution options selected
}
{ "function": "run_start", ? // Webhook Trigger keyword
"title"
: str, ? // Title of the Payload
"body"
: str, ? // Message of the Payload
"start_time"
: str, ? // Time Run is started Format "YYYY-mm-dd HH:MM:SS"
"dry_run"
: bool, ? // Dry-Run
"web_api_used"
: bool, ? // Indicates whether the run was initiated via the Web API (true) or not (false).
"commands"
: list, ? // List of commands that that will be ran
"execution_options"
: list // List of eecution options selected }
```
### **Run End Notifications**
@ -370,16 +378,21 @@ Payload will be sent at the end of the run
Payload will be sent when rechecking/resuming a torrent that is paused
```yaml
{
"function": "recheck", // Webhook Trigger keyword
"title": str, // Title of the Payload
"body": str, // Message of the Payload
"torrents": [str], // List of Torrent Names
"torrent_tag": str, // Torrent Tags
"torrent_category": str, // Torrent Category
"torrent_tracker": str, // Torrent Tracker URL
"notifiarr_indexer": str, // Notifiarr React name/id for indexer
}
{ "function": "recheck", ? // Webhook Trigger keyword
"title"
: str, ? // Title of the Payload
"body"
: str, ? // Message of the Payload
"torrents"
: [str], ? // List of Torrent Names
"torrent_tag"
: str, ? // Torrent Tags
"torrent_category"
: str, ? // Torrent Category
"torrent_tracker"
: str, ? // Torrent Tracker URL
"notifiarr_indexer"
: str, // Notifiarr React name/id for indexer }
```
### **Category Update Notifications**
@ -387,16 +400,21 @@ Payload will be sent when rechecking/resuming a torrent that is paused
Payload will be sent when updating torrents with missing category
```yaml
{
"function": "cat_update", // Webhook Trigger keyword
"title": str, // Title of the Payload
"body": str, // Message of the Payload
"torrents": [str], // List of Torrent Names
"torrent_category": str, // New Torrent Category
"torrent_tag": str, // Torrent Tags
"torrent_tracker": str, // Torrent Tracker URL
"notifiarr_indexer": str, // Notifiarr React name/id for indexer
}
{ "function": "cat_update", ? // Webhook Trigger keyword
"title"
: str, ? // Title of the Payload
"body"
: str, ? // Message of the Payload
"torrents"
: [str], ? // List of Torrent Names
"torrent_category"
: str, ? // New Torrent Category
"torrent_tag"
: str, ? // Torrent Tags
"torrent_tracker"
: str, ? // Torrent Tracker URL
"notifiarr_indexer"
: str, // Notifiarr React name/id for indexer }
```
### **Tag Update Notifications**
@ -404,16 +422,21 @@ Payload will be sent when updating torrents with missing category
Payload will be sent when updating torrents with missing tag
```yaml
{
"function": "tag_update", // Webhook Trigger keyword
"title": str, // Title of the Payload
"body": str, // Message of the Payload
"torrents": [str], // List of Torrent Names
"torrent_category": str, // Torrent Category
"torrent_tag": str, // New Torrent Tag
"torrent_tracker": str, // Torrent Tracker URL
"notifiarr_indexer": str, // Notifiarr React name/id for indexer
}
{ "function": "tag_update", ? // Webhook Trigger keyword
"title"
: str, ? // Title of the Payload
"body"
: str, ? // Message of the Payload
"torrents"
: [str], ? // List of Torrent Names
"torrent_category"
: str, ? // Torrent Category
"torrent_tag"
: str, ? // New Torrent Tag
"torrent_tracker"
: str, ? // Torrent Tracker URL
"notifiarr_indexer"
: str, // Notifiarr React name/id for indexer }
```
### **Remove Unregistered Torrents Notifications**
@ -421,18 +444,25 @@ Payload will be sent when updating torrents with missing tag
Payload will be sent when Unregistered Torrents are found
```yaml
{
"function": "rem_unregistered", // Webhook Trigger keyword
"title": str, // Title of the Payload
"body": str, // Message of the Payload
"torrents": [str], // List of Torrent Names
"torrent_category": str, // Torrent Category
"torrent_status": str, // Torrent Tracker Status message
"torrent_tag": str, // Torrent Tags
"torrent_tracker": str, // Torrent Tracker URL
"notifiarr_indexer": str, // Notifiarr React name/id for indexer
"torrents_deleted_and_contents": bool, // Deleted Torrents and contents or Deleted just the torrent
}
{ "function": "rem_unregistered", ? // Webhook Trigger keyword
"title"
: str, ? // Title of the Payload
"body"
: str, ? // Message of the Payload
"torrents"
: [str], ? // List of Torrent Names
"torrent_category"
: str, ? // Torrent Category
"torrent_status"
: str, ? // Torrent Tracker Status message
"torrent_tag"
: str, ? // Torrent Tags
"torrent_tracker"
: str, ? // Torrent Tracker URL
"notifiarr_indexer"
: str, ? // Notifiarr React name/id for indexer
"torrents_deleted_and_contents"
: bool, // Deleted Torrents and contents or Deleted just the torrent }
```
### **Tag Tracker Error Notifications**
@ -440,30 +470,41 @@ Payload will be sent when Unregistered Torrents are found
Payload will be sent when trackers with errors are tagged/untagged
```yaml
{
"function": "tag_tracker_error", // Webhook Trigger keyword
"title": str, // Title of the Payload
"body": str, // Message of the Payload
"torrents": [str], // List of Torrent Names
"torrent_category": str, // Torrent Category
"torrent_tag": "issue", // Tag Added
"torrent_status": str, // Torrent Tracker Status message
"torrent_tracker": str, // Torrent Tracker URL
"notifiarr_indexer": str, // Notifiarr React name/id for indexer
}
{ "function": "tag_tracker_error", ? // Webhook Trigger keyword
"title"
: str, ? // Title of the Payload
"body"
: str, ? // Message of the Payload
"torrents"
: [str], ? // List of Torrent Names
"torrent_category"
: str, ? // Torrent Category
"torrent_tag"
: "issue", ? // Tag Added
"torrent_status"
: str, ? // Torrent Tracker Status message
"torrent_tracker"
: str, ? // Torrent Tracker URL
"notifiarr_indexer"
: str, // Notifiarr React name/id for indexer }
```
```yaml
{
"function": "untag_tracker_error", // Webhook Trigger keyword
"title": str, // Title of the Payload
"body": str, // Message of the Payload
"torrents": [str], // List of Torrent Names
"torrent_category": str, // Torrent Category
"torrent_tag": str, // Tag Added
"torrent_tracker": str, // Torrent Tracker URL
"notifiarr_indexer": str, // Notifiarr React name/id for indexer
}
{ "function": "untag_tracker_error", ? // Webhook Trigger keyword
"title"
: str, ? // Title of the Payload
"body"
: str, ? // Message of the Payload
"torrents"
: [str], ? // List of Torrent Names
"torrent_category"
: str, ? // Torrent Category
"torrent_tag"
: str, ? // Tag Added
"torrent_tracker"
: str, ? // Torrent Tracker URL
"notifiarr_indexer"
: str, // Notifiarr React name/id for indexer }
```
### **Remove Orphaned Files Notifications**
@ -471,14 +512,17 @@ Payload will be sent when trackers with errors are tagged/untagged
Payload will be sent when Orphaned Files are found and moved into the orphaned folder
```yaml
{
"function": "rem_orphaned", // Webhook Trigger keyword
"title": str, // Title of the Payload
"body": str, // Message of the Payload
"orphaned_files": list, // List of orphaned files
"orphaned_directory": str, // Folder path where orphaned files will be moved to
"total_orphaned_files": int, // Total number of orphaned files found
}
{ "function": "rem_orphaned", ? // Webhook Trigger keyword
"title"
: str, ? // Title of the Payload
"body"
: str, ? // Message of the Payload
"orphaned_files"
: list, ? // List of orphaned files
"orphaned_directory"
: str, ? // Folder path where orphaned files will be moved to
"total_orphaned_files"
: int, // Total number of orphaned files found }
```
### **Tag No Hardlinks Notifications**
@ -486,31 +530,41 @@ Payload will be sent when Orphaned Files are found and moved into the orphaned f
Payload will be sent when no hard links are found for any files in a particular torrent
```yaml
{
"function": "tag_nohardlinks", // Webhook Trigger keyword
"title": str, // Title of the Payload
"body": str, // Message of the Payload
"torrents": [str], // List of Torrent Names
"torrent_category": str, // Torrent Category
"torrent_tag": 'noHL', // Add `noHL` to Torrent Tags
"torrent_tracker": str, // Torrent Tracker URL
"notifiarr_indexer": str, // Notifiarr React name/id for indexer
}
{ "function": "tag_nohardlinks", ? // Webhook Trigger keyword
"title"
: str, ? // Title of the Payload
"body"
: str, ? // Message of the Payload
"torrents"
: [str], ? // List of Torrent Names
"torrent_category"
: str, ? // Torrent Category
"torrent_tag"
: "noHL", ? // Add `noHL` to Torrent Tags
"torrent_tracker"
: str, ? // Torrent Tracker URL
"notifiarr_indexer"
: str, // Notifiarr React name/id for indexer }
```
Payload will be sent when hard links are found for any torrents that were previously tagged with `noHL`
```yaml
{
"function": "untag_nohardlinks", // Webhook Trigger keyword
"title": str, // Title of the Payload
"body": str, // Message of the Payload
"torrents": [str], // List of Torrent Names
"torrent_category": str, // Torrent Category
"torrent_tag": 'noHL', // Remove `noHL` from Torrent Tags
"torrent_tracker": str, // Torrent Tracker URL
"notifiarr_indexer": str, // Notifiarr React name/id for indexer
}
{ "function": "untag_nohardlinks", ? // Webhook Trigger keyword
"title"
: str, ? // Title of the Payload
"body"
: str, ? // Message of the Payload
"torrents"
: [str], ? // List of Torrent Names
"torrent_category"
: str, ? // Torrent Category
"torrent_tag"
: "noHL", ? // Remove `noHL` from Torrent Tags
"torrent_tracker"
: str, ? // Torrent Tracker URL
"notifiarr_indexer"
: str, // Notifiarr React name/id for indexer }
```
### **Share Limits Notifications**
@ -518,35 +572,49 @@ Payload will be sent when hard links are found for any torrents that were previo
Payload will be sent when Share Limits are updated for a specific group
```yaml
{
"function": "share_limits", // Webhook Trigger keyword
"title": str, // Title of the Payload
"body": str, // Message of the Payload
"grouping": str, // Share Limit group name
"torrents": [str], // List of Torrent Names
"torrent_tag": str, // Torrent Tags
"torrent_max_ratio": float, // Set the Max Ratio Share Limit
"torrent_max_seeding_time": int, // Set the Max Seeding Time (minutes) Share Limit
"torrent_min_seeding_time": int, // Set the Min Seeding Time (minutes) Share Limit
"torrent_limit_upload_speed": int // Set the the torrent upload speed limit (kB/s)
}
{ "function": "share_limits", ? // Webhook Trigger keyword
"title"
: str, ? // Title of the Payload
"body"
: str, ? // Message of the Payload
"grouping"
: str, ? // Share Limit group name
"torrents"
: [str], ? // List of Torrent Names
"torrent_tag"
: str, ? // Torrent Tags
"torrent_max_ratio"
: float, ? // Set the Max Ratio Share Limit
"torrent_max_seeding_time"
: int, ? // Set the Max Seeding Time (minutes) Share Limit
"torrent_min_seeding_time"
: int, ? // Set the Min Seeding Time (minutes) Share Limit
"torrent_limit_upload_speed"
: int // Set the the torrent upload speed limit (kB/s) }
```
Payload will be sent when `cleanup` flag is set to true and torrent meets share limit criteria.
```yaml
{
"function": "cleanup_share_limits", // Webhook Trigger keyword
"title": str, // Title of the Payload
"body": str, // Message of the Payload
"grouping": str, // Share Limit group name
"torrents": [str], // List of Torrent Names
"torrent_category": str, // Torrent Category
"cleanup": True, // Cleanup flag
"torrent_tracker": str, // Torrent Tracker URL
"notifiarr_indexer": str, // Notifiarr React name/id for indexer
"torrents_deleted_and_contents": bool, // Deleted Torrents and contents or Deleted just the torrent
}
{ "function": "cleanup_share_limits", ? // Webhook Trigger keyword
"title"
: str, ? // Title of the Payload
"body"
: str, ? // Message of the Payload
"grouping"
: str, ? // Share Limit group name
"torrents"
: [str], ? // List of Torrent Names
"torrent_category"
: str, ? // Torrent Category
"cleanup"
: True, ? // Cleanup flag
"torrent_tracker"
: str, ? // Torrent Tracker URL
"notifiarr_indexer"
: str, ? // Notifiarr React name/id for indexer
"torrents_deleted_and_contents"
: bool, // Deleted Torrents and contents or Deleted just the torrent }
```
### **Cleanup directories Notifications**
@ -554,13 +622,17 @@ Payload will be sent when `cleanup` flag is set to true and torrent meets share
Payload will be sent when files are deleted/cleaned up from the various folders
```yaml
{
"function": "cleanup_dirs", // Webhook Trigger keyword
"location": str, // Location of the folder that is being cleaned
"title": str, // Title of the Payload
"body": str, // Message of the Payload
"files": list, // List of files that were deleted from the location
"empty_after_x_days": int, // Number of days that the files will be kept in the location
"size_in_bytes": int, // Total number of bytes deleted from the location
}
{ "function": "cleanup_dirs", ? // Webhook Trigger keyword
"location"
: str, ? // Location of the folder that is being cleaned
"title"
: str, ? // Title of the Payload
"body"
: str, ? // Message of the Payload
"files"
: list, ? // List of files that were deleted from the location
"empty_after_x_days"
: int, ? // Number of days that the files will be kept in the location
"size_in_bytes"
: int, // Total number of bytes deleted from the location }
```

View file

@ -302,6 +302,7 @@ class Config:
),
"nohardlinks_tag": self.util.check_for_attribute(self.data, "nohardlinks_tag", parent="settings", default="noHL"),
"stalled_tag": self.util.check_for_attribute(self.data, "stalled_tag", parent="settings", default="stalledDL"),
"private_tag": self.util.check_for_attribute(self.data, "private_tag", parent="settings", default_is_none=True),
"share_limits_tag": self.util.check_for_attribute(
self.data, "share_limits_tag", parent="settings", default=share_limits_tag
),
@ -352,6 +353,7 @@ class Config:
self.tracker_error_tag = self.settings["tracker_error_tag"]
self.nohardlinks_tag = self.settings["nohardlinks_tag"]
self.stalled_tag = self.settings["stalled_tag"]
self.private_tag = self.settings["private_tag"]
self.share_limits_tag = self.settings["share_limits_tag"]
self.share_limits_custom_tags = []
self.share_limits_min_seeding_time_tag = self.settings["share_limits_min_seeding_time_tag"]
@ -365,6 +367,7 @@ class Config:
self.share_limits_min_num_seeds_tag,
self.share_limits_last_active_tag,
self.share_limits_tag,
self.private_tag,
]
# "Migrate settings from v4.0.0 to v4.0.1 and beyond. Convert 'share_limits_suffix_tag' to 'share_limits_tag'"
if "share_limits_suffix_tag" in self.data["settings"]:

View file

@ -17,6 +17,7 @@ class Tags:
self.torrents_updated = [] # List of torrents updated
self.notify_attr = [] # List of single torrent attributes to send to notifiarr
self.stalled_tag = qbit_manager.config.stalled_tag
self.private_tag = qbit_manager.config.private_tag
self.tag_stalled_torrents = self.config.settings["tag_stalled_torrents"]
self.tags()
@ -53,23 +54,29 @@ class Tags:
and torrent.state == "stalledDL"
and not util.is_tag_in_torrent(self.stalled_tag, torrent.tags)
)
or (
self.private_tag
and not util.is_tag_in_torrent(self.private_tag, torrent.tags)
and self.qbt.is_torrent_private(torrent)
)
):
stalled = False
tags_to_add = tracker["tag"].copy()
if self.tag_stalled_torrents and torrent.state == "stalledDL":
stalled = True
tracker["tag"].append(self.stalled_tag)
if tracker["tag"] or stalled:
tags_to_add.append(self.stalled_tag)
if self.private_tag and self.qbt.is_torrent_private(torrent):
tags_to_add.append(self.private_tag)
if tags_to_add:
t_name = torrent.name
self.stats += len(tracker["tag"])
self.stats += len(tags_to_add)
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"New Tag{'s' if len(tracker['tag']) > 1 else ''}: {', '.join(tracker['tag'])}", 8),
logger.insert_space(f"New Tag{'s' if len(tags_to_add) > 1 else ''}: {', '.join(tags_to_add)}", 8),
self.config.loglevel,
)
body += logger.print_line(logger.insert_space(f"Tracker: {tracker['url']}", 8), self.config.loglevel)
if not self.config.dry_run:
torrent.add_tags(tracker["tag"])
torrent.add_tags(tags_to_add)
category = self.qbt.get_category(torrent.save_path)[0] if torrent.category == "" else torrent.category
attr = {
"function": "tag_update",
@ -77,7 +84,7 @@ class Tags:
"body": "\n".join(body),
"torrents": [t_name],
"torrent_category": category,
"torrent_tag": ", ".join(tracker["tag"]),
"torrent_tag": ", ".join(tags_to_add),
"torrent_tracker": tracker["url"],
"notifiarr_indexer": tracker["notifiarr"],
}

View file

@ -299,6 +299,23 @@ class Qbt:
"""Get tracker urls from torrent"""
return tuple(x.url for x in trackers if x.url.startswith(("http", "udp", "ws")))
def is_torrent_private(self, torrent):
"""Checks if torrent is private"""
if hasattr(torrent, "private") and torrent.private:
return True
if hasattr(torrent, "private") and not torrent.private:
return False
if isinstance(torrent, str):
torrent_hash = torrent
else:
torrent_hash = torrent.hash
torrent_trackers = self.client.torrents_trackers(torrent_hash)
for tracker in torrent_trackers:
if "private" in tracker["msg"].lower() or "private" in tracker["url"].lower():
return True
return False
def get_tags(self, urls):
"""Get tags from config file based on keyword"""
urls = list(urls)

View file

@ -44,6 +44,13 @@ export const settingsSchema = {
description: 'The tag to apply to torrents that are stalled during download.',
default: 'stalledDL'
},
{
name: 'private_tag',
type: 'text',
label: 'Private Tag',
description: 'The tag to apply to private torrents.',
default: null
},
{
name: 'share_limits_tag',
type: 'text',