Skip to content

Commit fc8d0a9

Browse files
committed
feat(torrents): support new share limit parameters
1 parent 3c5cf3b commit fc8d0a9

20 files changed

Lines changed: 228 additions & 26 deletions

File tree

‎documentation/docs/advanced/compatibility.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ qui automatically detects the features available on each qBittorrent instance an
2626
| **Path Autocomplete** | 5.0.0+ (Web API 2.11.2+) | Autocomplete suggestions for path inputs when adding torrents or creating .torrent files |
2727
| **External IP Reporting (IPv4/IPv6)** | 5.1.0+ (Web API 2.11.3+) | Exposes `last_external_address_v4` / `_v6` fields |
2828
| **Tracker Health Status** | 5.1.0+ (Web API 2.11.4+) | Automatically detects unregistered torrents and tracker issues |
29+
| **Share Limit Action & Mode** | 5.2.0+ (Web API 2.15.3+) | Per-torrent action when share limits are reached (stop, remove, remove with content, enable super seeding) and matching mode (any vs all) |
2930

3031
:::note
3132
Hybrid and v2 torrent creation requires a qBittorrent build that links against libtorrent v2. Builds compiled with libtorrent 1.x ignore the `format` parameter.

‎documentation/docs/features/automations.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,30 @@ Set ratio limit and/or seeding time limit. Each field supports these modes:
436436

437437
Torrents stop seeding when any enabled limit is reached.
438438

439+
#### Share Limit Action & Mode (qBittorrent 5.2+)
440+
441+
On qBittorrent 5.2 and newer (Web API 2.15.3+), two additional per-torrent settings are available:
442+
443+
**When limits are reached** controls what happens when a torrent's share limits are hit:
444+
445+
| Option | Value | Description |
446+
| ----------------------- | ----- | ------------------------------------------- |
447+
| Default (use global) | -1 | Follow qBittorrent's global setting |
448+
| Stop torrent | 0 | Pause the torrent |
449+
| Remove torrent | 1 | Remove from client, keep files |
450+
| Enable super seeding | 2 | Switch to super seeding mode |
451+
| Remove with content | 3 | Remove from client and delete files |
452+
453+
**Limits matching mode** controls whether the action triggers when *any* limit is reached or only when *all* limits are reached:
454+
455+
| Option | Value | Description |
456+
| ----------------------- | ----- | ------------------------------------------- |
457+
| Default (use global) | -1 | Follow qBittorrent's global setting |
458+
| Match any limit | 0 | Trigger when any single limit is reached |
459+
| Match all limits | 1 | Trigger only when all limits are reached |
460+
461+
These fields appear in both the torrent share limit dialog and the automation workflow editor when the connected qBittorrent instance supports them. On older instances, the fields are hidden and only the classic ratio/seeding time limits are sent.
462+
439463
### Pause
440464

441465
Pause matching torrents. Only pauses if not already stopped.

‎go.mod‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ require (
1111
github.com/andybalholm/brotli v1.2.1
1212
github.com/autobrr/autobrr v1.77.0
1313
github.com/autobrr/go-mediainfo v0.3.1
14-
github.com/autobrr/go-qbittorrent v1.15.1-0.20260506110806-0f6206131e7a
14+
github.com/autobrr/go-qbittorrent v1.15.1-0.20260511072703-cba7da3ac836
1515
github.com/avast/retry-go v3.0.0+incompatible
1616
github.com/cespare/xxhash/v2 v2.3.0
1717
github.com/coreos/go-oidc/v3 v3.18.0

‎go.sum‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ github.com/autobrr/autobrr v1.77.0 h1:0SNa9g2mk5Qe92vHOt0eJJz7VHB515R/BZyy1fCzik
6161
github.com/autobrr/autobrr v1.77.0/go.mod h1:GstQT2UumxQE54ECtjl5jwdzenaGFzvWxVNEVoLWH4E=
6262
github.com/autobrr/go-mediainfo v0.3.1 h1:4gT1qoIKp4e+9X7PA/YO53lEkylY+GubFhCl8u8o1R4=
6363
github.com/autobrr/go-mediainfo v0.3.1/go.mod h1:xVXgHDAYYpIeGQCQCdTBpoiY/O3tojJfflK+rrbzclQ=
64-
github.com/autobrr/go-qbittorrent v1.15.1-0.20260506110806-0f6206131e7a h1:bWzZDGbwP72DxYSP9+ab68bmEYxf2FKV3LsklFrNt2s=
65-
github.com/autobrr/go-qbittorrent v1.15.1-0.20260506110806-0f6206131e7a/go.mod h1:W/24JQyQI+u5KntK1qzkBC/7amOTBPpV+q5e3C+kCkY=
64+
github.com/autobrr/go-qbittorrent v1.15.1-0.20260511072703-cba7da3ac836 h1:sgOAVRXsaMnL9f8kaQgFNQPU0xQCE0MzH9qUuN46qec=
65+
github.com/autobrr/go-qbittorrent v1.15.1-0.20260511072703-cba7da3ac836/go.mod h1:W/24JQyQI+u5KntK1qzkBC/7amOTBPpV+q5e3C+kCkY=
6666
github.com/autobrr/rls v0.8.0 h1:Odz78o5nfrO4hsQ17qiuyKYtiG97+Cs5nrD9TFk8/lI=
6767
github.com/autobrr/rls v0.8.0/go.mod h1:11YJWgNe5H+UQrfFaNGjnkEy0OEsMFnmV4IL2tRKpPk=
6868
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=

‎internal/api/handlers/capabilities.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type InstanceCapabilitiesResponse struct {
2525
SupportsPathAutocomplete bool `json:"supportsPathAutocomplete"`
2626
SupportsFreeSpacePathSource bool `json:"supportsFreeSpacePathSource"`
2727
SupportsSetRSSFeedURL bool `json:"supportsSetRSSFeedURL"`
28+
SupportsShareLimits bool `json:"supportsShareLimits"`
2829
WebAPIVersion string `json:"webAPIVersion,omitempty"`
2930
}
3031

@@ -45,6 +46,7 @@ func NewInstanceCapabilitiesResponse(client *internalqbittorrent.Client) Instanc
4546
SupportsPathAutocomplete: client.SupportsPathAutocomplete(),
4647
SupportsFreeSpacePathSource: runtime.GOOS != osWindows,
4748
SupportsSetRSSFeedURL: client.SupportsSetRSSFeedURL(),
49+
SupportsShareLimits: client.SupportsShareLimits(),
4850
}
4951

5052
if version := client.GetWebAPIVersion(); version != "" {

‎internal/api/handlers/torrents.go‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1064,6 +1064,8 @@ type BulkActionRequest struct {
10641064
RatioLimit float64 `json:"ratioLimit,omitempty"` // For setShareLimit action
10651065
SeedingTimeLimit int64 `json:"seedingTimeLimit,omitempty"` // For setShareLimit action
10661066
InactiveSeedingTimeLimit int64 `json:"inactiveSeedingTimeLimit,omitempty"` // For setShareLimit action
1067+
ShareLimitAction string `json:"shareLimitAction,omitempty"` // For setShareLimit action (qBittorrent 5.2+)
1068+
ShareLimitsMode string `json:"shareLimitsMode,omitempty"` // For setShareLimit action (qBittorrent 5.2+)
10671069
UploadLimit int64 `json:"uploadLimit,omitempty"` // For setUploadLimit action (KB/s)
10681070
DownloadLimit int64 `json:"downloadLimit,omitempty"` // For setDownloadLimit action (KB/s)
10691071
Location string `json:"location,omitempty"` // For setLocation action
@@ -1584,7 +1586,7 @@ func (h *TorrentsHandler) executeBulkActionForInstance(ctx context.Context, inst
15841586
case "forceStart":
15851587
return h.syncManager.SetForceStart(ctx, instanceID, hashes, req.Enable)
15861588
case "setShareLimit":
1587-
return h.syncManager.SetTorrentShareLimit(ctx, instanceID, hashes, req.RatioLimit, req.SeedingTimeLimit, req.InactiveSeedingTimeLimit)
1589+
return h.syncManager.SetTorrentShareLimit(ctx, instanceID, hashes, req.RatioLimit, req.SeedingTimeLimit, req.InactiveSeedingTimeLimit, req.ShareLimitAction, req.ShareLimitsMode)
15881590
case "setUploadLimit":
15891591
return h.syncManager.SetTorrentUploadLimit(ctx, instanceID, hashes, req.UploadLimit)
15901592
case "setDownloadLimit":

‎internal/models/automation.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,8 @@ type ShareLimitsAction struct {
884884
Enabled bool `json:"enabled"`
885885
RatioLimit *float64 `json:"ratioLimit,omitempty"`
886886
SeedingTimeMinutes *int64 `json:"seedingTimeMinutes,omitempty"`
887+
ShareLimitAction *string `json:"shareLimitAction,omitempty"`
888+
ShareLimitsMode *string `json:"shareLimitsMode,omitempty"`
887889
Condition *RuleCondition `json:"condition,omitempty"`
888890
}
889891

‎internal/qbittorrent/client.go‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ var (
3131
torrentTmpPathMinVersion = semver.MustParse("2.8.4")
3232
pathAutocompleteMinVersion = semver.MustParse("2.11.2")
3333
rssSetFeedURLMinVersion = semver.MustParse("2.9.1")
34+
shareLimitsMinVersion = semver.MustParse("2.15.3")
3435
)
3536

3637
type Client struct {
@@ -50,6 +51,7 @@ type Client struct {
5051
supportsPathAutocomplete bool
5152
trackerIncludeSupported bool
5253
supportsSetRSSFeedURL bool
54+
supportsShareLimits bool
5355
lastHealthCheck time.Time
5456
isHealthy bool
5557
syncManager *qbt.SyncManager
@@ -268,6 +270,7 @@ func (c *Client) applyCapabilitiesLocked(version string) {
268270
c.supportsTorrentTmpPath = !v.LessThan(torrentTmpPathMinVersion)
269271
c.supportsPathAutocomplete = !v.LessThan(pathAutocompleteMinVersion)
270272
c.supportsSetRSSFeedURL = !v.LessThan(rssSetFeedURLMinVersion)
273+
c.supportsShareLimits = !v.LessThan(shareLimitsMinVersion)
271274
}
272275

273276
// UpdateWithPeersData triggers a sync on the peer manager to keep it warm after intercepting peer data
@@ -384,6 +387,12 @@ func (c *Client) SupportsSetRSSFeedURL() bool {
384387
return c.supportsSetRSSFeedURL
385388
}
386389

390+
func (c *Client) SupportsShareLimits() bool {
391+
c.mu.RLock()
392+
defer c.mu.RUnlock()
393+
return c.supportsShareLimits
394+
}
395+
387396
func (c *Client) GetWebAPIVersion() string {
388397
c.mu.RLock()
389398
defer c.mu.RUnlock()

‎internal/qbittorrent/sync_manager.go‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5636,8 +5636,9 @@ func (sm *SyncManager) GetActiveTrackers(ctx context.Context, instanceID int) (m
56365636
return trackerMap, nil
56375637
}
56385638

5639-
// SetTorrentShareLimit sets share limits (ratio, seeding time) for torrents
5640-
func (sm *SyncManager) SetTorrentShareLimit(ctx context.Context, instanceID int, hashes []string, ratioLimit float64, seedingTimeLimit, inactiveSeedingTimeLimit int64) error {
5639+
// SetTorrentShareLimit sets share limits (ratio, seeding time, action, mode) for torrents.
5640+
// shareLimitAction and shareLimitsMode are only sent when the instance supports them (webAPI >= 2.15.3).
5641+
func (sm *SyncManager) SetTorrentShareLimit(ctx context.Context, instanceID int, hashes []string, ratioLimit float64, seedingTimeLimit, inactiveSeedingTimeLimit int64, shareLimitAction, shareLimitsMode string) error {
56415642
// Get client and sync manager
56425643
client, _, err := sm.getClientAndSyncManager(ctx, instanceID)
56435644
if err != nil {
@@ -5649,7 +5650,14 @@ func (sm *SyncManager) SetTorrentShareLimit(ctx context.Context, instanceID int,
56495650
return err
56505651
}
56515652

5652-
if err := client.SetTorrentShareLimitCtx(ctx, hashes, ratioLimit, seedingTimeLimit, inactiveSeedingTimeLimit); err != nil {
5653+
action := shareLimitAction
5654+
mode := shareLimitsMode
5655+
if !client.SupportsShareLimits() {
5656+
action = ""
5657+
mode = ""
5658+
}
5659+
5660+
if err := client.SetTorrentShareLimitCtx(ctx, hashes, ratioLimit, seedingTimeLimit, inactiveSeedingTimeLimit, action, mode); err != nil {
56535661
return fmt.Errorf("failed to set torrent share limit: %w", err)
56545662
}
56555663

‎internal/services/automations/processor.go‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ type torrentDesiredState struct {
3333
downloadRule ruleRef
3434

3535
// Share limits (last rule wins)
36-
ratioLimit *float64
37-
seedingMinutes *int64
38-
ratioRule ruleRef
39-
seedingRule ruleRef
36+
ratioLimit *float64
37+
seedingMinutes *int64
38+
shareLimitAction string
39+
shareLimitsMode string
40+
ratioRule ruleRef
41+
seedingRule ruleRef
4042

4143
// Pause (OR - any rule can trigger)
4244
shouldPause bool
@@ -304,6 +306,12 @@ func processRuleForTorrent(rule *models.Automation, torrent qbt.Torrent, state *
304306
state.seedingMinutes = conditions.ShareLimits.SeedingTimeMinutes
305307
state.seedingRule = ruleRef{id: rule.ID, name: rule.Name}
306308
}
309+
if conditions.ShareLimits.ShareLimitAction != nil {
310+
state.shareLimitAction = *conditions.ShareLimits.ShareLimitAction
311+
}
312+
if conditions.ShareLimits.ShareLimitsMode != nil {
313+
state.shareLimitsMode = *conditions.ShareLimits.ShareLimitsMode
314+
}
307315
} else if stats != nil {
308316
stats.ShareConditionNotMet++
309317
}

0 commit comments

Comments
 (0)