Skip to content

feat(indexer): per indexer seed time and inactive seed time limits (#2206) - #2917

Merged
vavallee merged 3 commits into
mainfrom
feature/2206-seed-time
Oct 2, 2026
Merged

vavallee merged 3 commits into
mainfrom
feature/2206-seed-time

Conversation

@vavallee

@vavallee vavallee commented Oct 2, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #2206.

Torrent indexers get two new optional limits next to the seed ratio: seed time (total minutes) and inactive seed time (minutes without upload). They are carried to the download client at grab time, the same way the ratio is.

  • Migration 097 adds seed_time_minutes, inactive_seed_time_minutes (nullable, NULL = client's own rule) and seed_time_source.
  • Prowlarr exposes torrentBaseSettings.seedTime (int, minutes) per indexer, so the syncer fills the seed time with the same provenance rules as the ratio. It has its own source column on purpose: every PUT marks seed_ratio_source as user, so sharing it would stop Prowlarr from ever filling the seed time on indexers that have been edited once. Prowlarr has no inactive seed time, so that field is user only.
  • API: seedTimeMinutes / inactiveSeedTimeMinutes on indexer create and update. Omitted keeps the stored value, null clears, below 1 or above ten years is a 400.
  • UI: both fields under Seed ratio in the add and edit indexer forms, with the per client support in the help text and a "from Prowlarr" note. en.json only.
  • Docs: docs/API.md, docs/User-Guide-Wiki.md (support table), docs/QUICKSTART.md (rTorrent note).

Found along the way: the Transmission ratio never applied

AddTorrentDetailed put seedRatioLimit / seedRatioMode on torrent-add. Transmission's torrentAdd reads none of the seed keys; only torrentSet does (libtransmission/rpcimpl.cc, checked on 4.0.6, and the RPC spec lists them under torrent-set only). So the per indexer ratio has been a silent no-op on Transmission. Ratio and idle limit now go out in a torrent-set right after the add, addressed by info hash. A failed torrent-set is logged and the grab still succeeds, matching qBittorrent and Deluge. This has its own ### Fixed line in the fragment.

Client support

Client Seed ratio Seed time Inactive seed time How
qBittorrent yes yes yes (4.6+) one setShareLimits call, unset limits sent as -2 (global). Units are minutes: qBit compares seedingTimeLimit * 60 against its seconds counters.
Transmission yes (now actually applied) no yes torrent-set with seedIdleLimit + seedIdleMode=1. No total seed time exists in the RPC. Idle limit is a uint16 in the daemon, so values above 65535 are clamped.
Deluge yes no no core options are stop_at_ratio / stop_ratio / remove_at_ratio only
rTorrent no no no no per torrent limits over XML-RPC
SABnzbd / NZBGet n/a n/a n/a no seeding

A time limit the client cannot take is logged at debug (unappliedSeedLimits in the adapter) and the client's own rule applies. rTorrent's existing ratio warning is unchanged.

Kept localized for #2713: the Transmission AddTorrent / AddTorrentDetailed signatures are unchanged (new AddTorrentWithLimits underneath), and the adapter change is a few lines per client case.

Fail before

Behavioural, on main's Transmission client:

--- FAIL: TestAddTorrent_SeedRatio_Positive
    seed_limits_test.go:70: torrent-set was not called for a ratio override
--- FAIL: TestAddTorrent_SeedRatio_Unlimited
    seed_limits_test.go:96: torrent-set was not called for the unlimited sentinel

UI tests against main's IndexersTab.tsx: 6 failed (add/edit payloads missing the fields, fields absent). The Go tests for the new fields (db, api, prowlarr, qbittorrent, adapter, scheduler) fail to compile on main since the fields and methods do not exist.

Test plan

  • go build ./... && go vet ./internal/... ./cmd/...
  • go test ./internal/... ./cmd/... -count=1 (58 packages ok)
  • golangci-lint run ./internal/... ./cmd/... (0 issues)
  • migration 097 shape and pre-existing rows (TestMigrate097AddsSeedTimeColumns), repo round trip (TestIndexerRepo_SeedTimeLimits)
  • API: round trip, omitted keeps, null clears, 0 / negative / over max rejected on create and update (TestIndexerSeedTimeLimits_API)
  • Prowlarr: parse seedTime, provenance table, full sync with independent ratio and time ownership
  • qBittorrent, Transmission and adapter request bodies against httptest servers
  • cd web && npx tsc -b && npx eslint src && npx vitest run (1179 passed; eslint warnings are pre-existing, none in touched files)
  • Manual grab against a real qBittorrent 4.6+ / 5.x and a real Transmission 4.x

🤖 Generated with Claude Code

https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9

vavallee and others added 2 commits October 1, 2026 22:05
…2206)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9
Signed-off-by: vavallee <vavallee@protonmail.com>
…2206)

Adds seedTimeMinutes and inactiveSeedTimeMinutes to indexers next to the
seed ratio (migration 097), with a seed_time_source provenance column so a
Prowlarr sync fills the seed time from torrentBaseSettings.seedTime without
overwriting a value the user set or cleared. The provenance is separate from
the ratio's because any edit marks the ratio user owned, which would otherwise
keep Prowlarr from ever filling the new field on existing indexers.

The grab carries both limits to the client:
qBittorrent gets all three in one setShareLimits call (unset ones stay -2),
Transmission gets the inactive limit as seedIdleLimit, and Deluge and
rTorrent, which have no per torrent time limits, log at debug and keep their
own rules.

Transmission's torrent-add ignores seedRatioLimit and seedRatioMode (only
torrent-set reads them, see rpcimpl.cc), so the per indexer ratio never
applied there. Ratio and idle limit now go out in a torrent-set after the add,
addressed by info hash, and a failure is logged rather than failing the grab.

The API rejects a seed time below 1 minute or above ten years; null clears it.
The indexer form gains both fields with per client help text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9
Signed-off-by: vavallee <vavallee@protonmail.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this is a clean, well-reasoned change. The separate provenance column for seed time is the right call — sharing it with seed_ratio_source would freeze every previously-edited indexer out of Prowlarr auto-population. The Transmission ratio fix is a genuine bug fix backed by the libtransmission source.

One behavioral concern: Transmission idle-time clamping is logged at debug

internal/downloader/transmission/client.go — applySeedIdleArgs:

if v > maxSeedIdleMinutes {
    slog.Debug("transmission: inactive seed time clamped to the daemon's maximum", ...)
    v = maxSeedIdleMinutes
}

The API accepts up to 5,256,000 minutes; maxSeedIdleMinutes is 65,535 (~45 days). If a user sets, say, inactiveSeedTimeMinutes: 200000 on a Transmission indexer, they get 65,535 applied with no visible feedback. This is materially different from "limit not supported" (which we log and document): the limit is applied, but at a different value than configured. Debug-level means this is invisible unless the user has debug logging on.

Suggestion: bump to slog.Warn here, or add the Transmission idle-time ceiling to the help text / docs table. The cap is hardware-real (tr_torrentSetIdleLimit is uint16), so there's no workaround — just needs to be surfaced.


Everything else looks good:

  • Migration 097: additive, nullable defaults, independent down path. Pre-existing rows read back as "no override" correctly.
  • Auth / tenancy: no new endpoints; new fields ride the existing indexer CRUD which is already admin-gated. No cross-user visibility risk.
  • Prowlarr syncer: independent provenance per field, both reconciled unconditionally (not short-circuited), applyProwlarrSeedTime mirrors applyProwlarrSeedRatio faithfully.
  • qBittorrent: -2 for unset fields is correct; the IsZero guard before the call avoids a pointless setShareLimits on torrents with no override.
  • SeedLimitsFor(nil) is defended; the idx == nil removal in queue.go is safe because SeedLimitsFor handles it.
  • Tests: migration shape, repo round-trip, API (omit-keeps / null-clears / reject-invalid), Prowlarr parse table, qBittorrent form fields, Transmission torrent-set path all covered.

— 🤖 Bindery triage bot (automated). Reply to correct me; a human will see it.

Signed-off-by: vavallee <vavallee@protonmail.com>
@codecov

codecov Bot commented Oct 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.54167% with 22 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/downloader/adapter.go 81.63% 8 Missing and 1 partial ⚠️
internal/downloader/transmission/client.go 79.54% 5 Missing and 4 partials ⚠️
internal/api/queue.go 66.66% 1 Missing and 1 partial ⚠️
internal/scheduler/scheduler.go 66.66% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@vavallee
vavallee merged commit a227f15 into main Oct 2, 2026
38 of 39 checks passed
vavallee added a commit that referenced this pull request Oct 2, 2026
Main moved under this branch: Transmission downloads are keyed by info
hash (#2711), the importer polls the Transmission audiobook category
(#2712), remove on import exists (#2713) and per indexer seed limits
apply after add (#2917).

Conflict resolution keeps both sides. GetStalledTorrents now keys both
Transmission halves of the StallReport by lower-cased info hash, never
the session id, and the torrent-get field list carries main's addedDate
alongside metadataPercentComplete and peersConnected.

Re-checking the logic against current main turned up three things,
fixed here:

- Stall detection still polled only the Transmission ebook category, so
  an audiobook magnet was invisible to it. It now polls every category
  CategoriesToPoll returns, counting a torrent both filters return once
  so the outage guard's denominator is not doubled.
- A no-metadata torrent was removed with delete-local-data. It has no
  files, so it is now removed entry only (StallKind.DeletesData), the
  same as remove on import. Client reported stalls still delete data.
- The re-search after a no-metadata stall did nothing when the dead
  release still ranked first: it was approved again and the #2710
  cooldown refused the grab instead of trying the runner up. The
  re-search now carries the stalled GUID and a decision spec skips it
  for that one search only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016fJcCVbNnKsmj2MAAMwWj9
Signed-off-by: vavallee <vavallee@protonmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow total seed minutes/ seed inactive minutes on indexer config

1 participant