Skip to content

qBitrr TorrentPolicyManager parity, config, DB indexes, and parity docs#111

Open
Feramance wants to merge 14 commits intomasterfrom
tracker-sorter
Open

qBitrr TorrentPolicyManager parity, config, DB indexes, and parity docs#111
Feramance wants to merge 14 commits intomasterfrom
tracker-sorter

Conversation

@Feramance
Copy link
Copy Markdown
Owner

@Feramance Feramance commented Apr 21, 2026

Summary

This pull request brings Torrentarr closer to upstream qBitrr behavior for the global TorrentPolicyManager flow (tracker queue sort + free-space policy), tightens config and environment parity, adds SQLite maintenance aligned with qBitrr, and introduces documentation for strict parity tracking.

Why these changes

  1. TorrentPolicyManager equivalence — qBitrr runs a single global worker that can enable tracker sort and free-space independently, runs pre-sort tracker/tag sync before reordering the queue, applies topPrio separately for seeding vs downloading queues, then runs free-space logic. Torrentarr previously ran tracker sort only inside the free-space path, so sort never ran when free-space was disabled. The Host now mirrors the intended pipeline and gating.

  2. Config / env parity — qBitrr deployments often use QBITRR_* env overrides. Torrentarr now accepts aliases alongside TORRENTARR_*, documents them, and adds fields such as SortTorrents, BehindHttpsProxy, and SecureCookiesBehindHttpsProxy migration for WebUI.

  3. Queue sort priority — Upstream blends AddTags-derived priority with announce-based tracker priority. SeedingService exposes this for the Host sort step; workers skip duplicate ApplyTrackerActionsForTorrentAsync when the Host owns sync (global SortTorrents), matching qBitrr policy-manager ownership of tracker sync.

  4. Database — Remove legacy rows with empty ArrInstance and ensure idx_arrinstance_* indexes exist on upgraded databases, consistent with qBitrr schema expectations.

  5. Docs — Add docs/parity/* (matrix, contract baseline, certification report), update environment and WebUI API docs, and mark the old parity review as superseded so contributors have a single place for strict-parity work.

Testing

  • dotnet test --filter "Category!=Live" (pre-commit also runs dotnet build and webui production build).

Notes / follow-ups

  • get_effective_qbit_disabled() in qBitrr also considers modes such as SEARCH_ONLY; Torrentarr does not model that flag yet if you need identical gating.
  • Free-space path auto-selection and full qBittorrent state enum coverage for pause/resume may still differ slightly from Python; worth validating on real instances if issues appear.

Note

Medium Risk
Medium risk because it changes core torrent policy ordering (queue sorting + free-space gating), config resolution/migrations, and performs DB cleanup/index creation on startup, which can affect runtime behavior and existing deployments.

Overview
Implements a qBitrr-style global TorrentPolicyManager pipeline in the Host: optional pre-sort tracker/tag sync, tracker-priority queue reordering (via qBittorrent topPrio and /torrents/info?sort=priority), and free-space management gated independently by policy flags.

Updates config parity: adds GetDataDirectoryPath() and a revised config search order (prefers ./.config/config.toml), accepts QBITRR_* env var aliases alongside TORRENTARR_*, bumps ExpectedConfigVersion to 6.1.0, and adds migrations/defaults including WebUI.SecureCookiesWebUI.BehindHttpsProxy plus tracker SortTorrents support.

Hardens DB parity by adding ArrInstance indexes in the EF model and a one-time startup cleanup for rows with blank arrinstance, and updates workers/services to avoid duplicate tracker sync when the Host policy owns it. Docs and WebUI config UI/tooltips are refreshed accordingly, and a parity tracking doc set is added.

Reviewed by Cursor Bugbot for commit f16dfd8. Bugbot is set up for automated code reviews on this repo. Configure here.

Why:
- Align Host orchestration with qBitrr TorrentPolicyManager: independent
  tracker sort vs free-space, pre-sort tracker/tag sync, split-queue topPrio.
- Match qBitrr config/env semantics (QBITRR_* aliases, WebUI BehindHttpsProxy,
  SortTorrents, qBit defaults) and document strict-parity artifacts.
- Harden SQLite parity (ArrInstance cleanup + indexes) and API/docs contracts.

Key changes:
- TorrentPolicyHelper: gating, tag-to-priority map, queue position keys,
  monitored policy categories.
- ProcessTorrentPolicyAsync: PreSortTrackerTagSync, SortManagedTorrentsByTrackerPriority
  (tag+announce priority, seeding/downloading passes), then free-space.
- SeedingService: GetTorrentQueueSortPriorityAsync / GetQueueSortAnnouncePriorityAsync.
- TorrentProcessor: skip ApplyTrackerActions when Host owns sync (SortTorrents).
- QBittorrentClient: torrent sort + SetTopPriorityAsync; TorrentInfo.Priority.
- ConfigurationLoader: env aliases, migrations (SecureCookies), tracker/WebUI fields.
- Tests: TorrentPolicyHelperTests, SeedingService queue-sort tests, config tests.
- Docs: parity matrix, contract baseline, certification report, environment.md,
  webui api, supersede parity-review.
Comment thread src/Torrentarr.Core/Configuration/TorrentPolicyHelper.cs Outdated
Comment thread src/Torrentarr.Core/Configuration/ConfigurationLoader.cs
Comment thread src/Torrentarr.Core/Configuration/ConfigurationLoader.cs
…ort Contains

- ReadEnv: treat defined TORRENTARR_* (including empty) as blocking QBITRR_* alias
- ApplyConfigMigrations: refresh ConfigVersion when needsMigration or any change
- IsQueueSeedingForSort: use Ordinal Contains after ToLowerInvariant
- Add Load_EmptyTorrentarrEnvBlocksQbitrrAlias test
Comment thread src/Torrentarr.Core/Configuration/ConfigurationLoader.cs
Comment thread src/Torrentarr.Host/Program.cs Outdated
Torrentarr Dev added 2 commits April 22, 2026 16:21
React 19 requires matching react and react-dom versions; lockfile had 19.2.5 vs 19.2.4.
… cleanup

- ValidateAndFillConfig: default LocalAuthEnabled to true with AuthDisabled false (same as GenerateDefaultConfig) to avoid lockout when auth keys are missing.
- Run DeleteRowsWithEmptyArrInstance once per database via torrentarr_manual_migrations, not on every startup.
- Extend ConfigurationLoaderTests for WebUI auth defaults after fill.
Comment thread src/Torrentarr.Core/Configuration/TorrentarrConfig.cs
Comment thread src/Torrentarr.Core/Configuration/ConfigurationLoader.cs
Torrentarr Dev added 2 commits April 22, 2026 16:41
Isolate component-detection submission from build.yml's contents:read default.
Grant contents:write and id-token:write required by the dependency submission API.
Limit scan to NuGet to match existing CI usage.
- Default LocalAuthEnabled to true on WebUIConfig so new/fallback instances
  match GenerateDefaultConfig and never require auth with no login method.
- Only rewrite Settings.ConfigVersion to ExpectedConfigVersion when the
  file version is at or below expected; preserve newer forward-compatible
  versions when ValidateAndFill adds keys.
- Add regression test for newer ConfigVersion after validate-and-fill.
Comment thread src/Torrentarr.Core/Configuration/TorrentPolicyHelper.cs
Comment thread src/Torrentarr.Core/Configuration/ConfigurationLoader.cs
Comment thread src/Torrentarr.Infrastructure/Services/TorrentProcessor.cs
Comment thread src/Torrentarr.Core/Configuration/TorrentPolicyHelper.cs
Torrentarr Dev added 2 commits April 22, 2026 18:44
…ategories

- Set changed when bumping ConfigVersion so Load() writes TOML when no other
  migration touched the file (avoids repeated migration on every startup).
- Cache GetAllMonitoredPolicyCategories on TorrentarrConfig to avoid per-torrent
  HashSet allocation in the torrent processing loop.
- Add regression tests for both behaviors.
…pply

Exposes InvalidateMonitoredPolicyCategoriesCache and calls it after /web and /api
config saves so the cached HashSet matches updated Arr and qBit categories.
Adds a regression test for cache refresh after in-place changes.
Comment thread src/Torrentarr.Infrastructure/Services/TorrentProcessor.cs
Comment thread src/Torrentarr.Host/Program.cs Outdated
Torrentarr Dev added 2 commits April 22, 2026 20:13
…e sort key once

- Replace startup-only managed category snapshot with TorrentPolicyHelper.GetAllMonitoredPolicyCategories on each policy pass so runtime config updates match worker IsMonitoredPolicyCategory gating.
- Avoid double TorrentQueuePositionSortKey evaluation in free-space ordering and tracker sort currentByPosition.
Comment thread src/Torrentarr.Core/Configuration/TorrentPolicyHelper.cs
Comment thread src/Torrentarr.Infrastructure/ApiClients/QBittorrent/QBittorrentClient.cs Outdated
Torrentarr Dev added 2 commits April 22, 2026 21:10
- Guard MonitoredPolicyCategoriesCache populate/invalidate with a dedicated lock and double-checked locking in GetAllMonitoredPolicyCategories.
- Reorder QBittorrentClient.GetTorrentsAsync to (category, sort, cancellationToken); rename ct to cancellationToken and update call sites.
- Prefer cwd .config/config.toml first in search order; new defaults create .config/ with DB and logs alongside
- Add GetDataDirectoryPath() for Host/WebUI/Workers data root; unify ConfigReloader with GetDefaultConfigPath
- WebUI: fatal exit on config load failure (non-FileNotFound); Host warns when wwwroot SPA bundle is missing
- Add GetDataDirectoryPathTests; update ConfigReloader tests for TORRENTARR_CONFIG when file missing
- Refresh AGENTS, CLAUDE, and docs for search order and data directory behavior
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Free-space pause logic doesn't update _currentFreeSpace after pausing

Medium Severity

In ProcessSingleTorrentSpaceAsync, the resume branch updates _currentFreeSpace = freeSpaceTest (line 3302) to track remaining space, but the pause branch pauses the torrent without updating _currentFreeSpace. This means the running space counter never reflects freed capacity from pauses, so the loop keeps pausing every active download regardless of how much space has already been reclaimed — instead of stopping once enough torrents are paused.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 782cfda. Configure here.

.OrderBy(x => x.key.InactiveQueueGroup)
.ThenBy(x => x.key.Nq)
.ThenBy(x => x.torrent.AddedOn)
.Select(x => (x.client, x.torrent)))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Free-space manager pauses highest-priority downloads first

Medium Severity

The free-space torrent iteration sorts by TorrentQueuePositionSortKey ascending — active queue position 1 (highest priority) comes first. In ProcessSingleTorrentSpaceAsync, the first downloading torrents encountered while _currentFreeSpace < 0 get paused. This means the most important user-prioritized downloads are paused before lower-priority ones. The old code used OrderBy(AddedOn) (oldest first). To protect high-priority downloads, the sort for the free-space pause loop likely needs to process lowest-priority (highest queue position number) torrents first.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 782cfda. Configure here.

… touch-ups

- Add docs/parity/overview.md and contributor-reference.md; update matrix, certification, contract baseline, and index links; shorten parity-review pointer
- MkDocs redirects for removed split parity doc paths
- Config docs, release process, and README/AGENTS/CLAUDE alignment
- Torrent policy helper and WebUI config text/tests; host test factory; Database SchemaParityTests
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

There are 5 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

Bugbot Autofix is ON, but it could not run because on-demand usage is turned off. To enable Bugbot Autofix, turn on on-demand usage and set a spend limit in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f16dfd8. Configure here.

("LocalAuthEnabled", false),
("AuthDisabled", false),
// Must match <see cref="GenerateDefaultConfig"/>: when auth is required, local login is the default path.
("LocalAuthEnabled", true),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Auth default flip may lock out existing users

High Severity

The ValidateAndFillConfig defaults for AuthDisabled changed from true to false and LocalAuthEnabled from false to true. Existing users whose TOML config files lack these keys (relying on old model defaults of AuthDisabled=true) will have AuthDisabled=false and LocalAuthEnabled=true injected on upgrade. Since they likely have no Username/PasswordHash set, authentication is now required but unconfigurable without the setup flow — effectively locking them out of the WebUI if the setup-required redirect doesn't handle this gracefully for every entry point.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f16dfd8. Configure here.

public bool AuthDisabled { get; set; } = true;
public bool AuthDisabled { get; set; } = false;
/// <summary>When true, WebUI is assumed behind HTTPS reverse proxy and secure cookie behavior is enabled.</summary>
public bool BehindHttpsProxy { get; set; } = false;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Config field BehindHttpsProxy parsed but never consumed

Medium Severity

BehindHttpsProxy is added to the config model, parsed from TOML, serialized, and migrated from SecureCookies, but no code reads it to change cookie or proxy behavior. Both Host and WebUI still hardcode CookieSecurePolicy.SameAsRequest. The SecureCookies migration silently renames the key, destroying the user's original setting, while the replacement field has no effect.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f16dfd8. Configure here.

var queueTorrents = sortedTorrents.Where(t => queueMembership.GetValueOrDefault(t.Hash) == queueIsSeeding).ToList();
foreach (var torrent in queueTorrents.AsEnumerable().Reverse())
await client.SetTopPriorityAsync(torrent.Hash, cancellationToken);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Tracker sort processes seeding queue before download queue

Medium Severity

In SortManagedTorrentsByTrackerPriorityAsync, the topPrio loop iterates new[] { true, false }, processing the seeding queue before the downloading queue. In qBitrr, downloading torrents are typically prioritized first (download queue sorted, then seeding). Reversing the iteration order means the downloading queue's topPrio calls execute last and may not match the upstream sort behavior.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f16dfd8. Configure here.

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.

1 participant