feat(api): batch torrent uploads in single request#885
feat(api): batch torrent uploads in single request#885
Conversation
Use go-qbittorrent's new AddTorrentsFromMemoryCtx to send all torrent files in one HTTP request instead of sequential uploads. Fixes timeout issues when adding many torrents at once. Closes #883
WalkthroughThis PR introduces batch torrent addition support by adding an AddTorrents method to the torrentAdder interface and SyncManager, consolidating multiple torrent file uploads into single batch API operations instead of sequential individual additions, addressing context deadline issues with large torrent batches. Changes
Sequence DiagramsequenceDiagram
actor Client
participant TorrentsHandler
participant SyncManager
participant qBittorrent API
Client->>TorrentsHandler: POST /torrents/add (multiple files)
activate TorrentsHandler
TorrentsHandler->>TorrentsHandler: addTorrents(files, options)
alt torrentAdder (test/mock) available
TorrentsHandler->>TorrentsHandler: torrentAdder.AddTorrents()
else production flow
TorrentsHandler->>SyncManager: AddTorrents(files, options)
activate SyncManager
SyncManager->>qBittorrent API: client.AddTorrentsFromMemoryCtx(files)
activate qBittorrent API
qBittorrent API-->>SyncManager: success/error
deactivate qBittorrent API
rect rgb(200, 220, 255)
note over SyncManager: Post-modification sync
SyncManager->>SyncManager: sync("add_torrents_batch")
end
SyncManager-->>TorrentsHandler: error or nil
deactivate SyncManager
end
TorrentsHandler-->>Client: response
deactivate TorrentsHandler
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
internal/api/handlers/torrents_add_test.go (2)
876-908: fullMockSyncManager’s AddTorrents support matches the new interfaceThe added
addTorrentsCallstracking plusAddTorrentsmethod correctly satisfies the extendedtorrentAdderinterface and mirrors the existing single-file mock behavior. You may want a dedicated test that posts multipletorrentfiles and assertsaddTorrentsCallsis hit, to lock in the new batch path end‑to‑end.
455-457: Comment about negative indexer IDs is now staleThe note here still claims negative
indexer_idvalues are treated like “no indexer”, but the handler now explicitly rejectsindexer_id <= 0with a 400. Consider updating or removing this note to avoid confusion.
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (4)
go.modinternal/api/handlers/torrents.gointernal/api/handlers/torrents_add_test.gointernal/qbittorrent/sync_manager.go
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: Audionut
Repo: autobrr/qui PR: 553
File: internal/services/crossseed/service.go:1045-1082
Timestamp: 2025-11-06T12:11:04.963Z
Learning: The autobrr/qui project uses a custom go-qbittorrent client library (github.com/autobrr/go-qbittorrent) that supports both "paused" and "stopped" parameters when adding torrents via the options map. Both parameters should be set together when controlling torrent start state, as seen in internal/services/crossseed/service.go and throughout the codebase.
Learnt from: s0up4200
Repo: autobrr/qui PR: 632
File: internal/backups/service.go:1401-1404
Timestamp: 2025-11-25T22:46:03.762Z
Learning: In qui's backup service (internal/backups/service.go), background torrent downloads initiated during manifest import intentionally use a fire-and-forget pattern with the shared service context (s.ctx). Per-run cancellation is not needed, as orphaned downloads completing after run deletion are considered harmless and acceptable. This design prioritizes simplicity over per-run lifecycle management for background downloads.
📚 Learning: 2025-11-06T12:11:04.963Z
Learnt from: Audionut
Repo: autobrr/qui PR: 553
File: internal/services/crossseed/service.go:1045-1082
Timestamp: 2025-11-06T12:11:04.963Z
Learning: The autobrr/qui project uses a custom go-qbittorrent client library (github.com/autobrr/go-qbittorrent) that supports both "paused" and "stopped" parameters when adding torrents via the options map. Both parameters should be set together when controlling torrent start state, as seen in internal/services/crossseed/service.go and throughout the codebase.
Applied to files:
go.modinternal/api/handlers/torrents.go
📚 Learning: 2025-12-11T08:40:01.329Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 746
File: internal/services/reannounce/service.go:480-481
Timestamp: 2025-12-11T08:40:01.329Z
Learning: In autobrr/qui's internal/services/reannounce/service.go, the hasHealthyTracker, getProblematicTrackers, and getHealthyTrackers functions intentionally match qbrr's lenient tracker health logic (skip unregistered trackers and check if any other tracker is healthy) rather than go-qbittorrent's strict isTrackerStatusOK logic (which treats unregistered as an immediate failure). For multi-tracker torrents, if one tracker is working, reannouncing won't help. The duplication of the health check logic across these three functions is acceptable as it's a simple one-liner, and extracting it would add unnecessary complexity.
Applied to files:
go.mod
📚 Learning: 2025-11-25T22:46:03.762Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 632
File: internal/backups/service.go:1401-1404
Timestamp: 2025-11-25T22:46:03.762Z
Learning: In qui's backup service (internal/backups/service.go), background torrent downloads initiated during manifest import intentionally use a fire-and-forget pattern with the shared service context (s.ctx). Per-run cancellation is not needed, as orphaned downloads completing after run deletion are considered harmless and acceptable. This design prioritizes simplicity over per-run lifecycle management for background downloads.
Applied to files:
go.modinternal/api/handlers/torrents.gointernal/qbittorrent/sync_manager.go
📚 Learning: 2025-12-28T18:44:10.496Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 876
File: internal/logstream/hub_test.go:188-192
Timestamp: 2025-12-28T18:44:10.496Z
Learning: In Go 1.25 (Aug 2025), use wg.Go(func()) to spawn a goroutine and automate the Add/Done lifecycle. Replace manual patterns like wg.Add(1); go func(){ defer wg.Done(); ... }() with wg.Go(func(){ ... }). Ensure the codebase builds with Go 1.25+ and apply this in relevant Go files (e.g., internal/logstream/hub_test.go). If targeting older Go versions, maintain the existing pattern.
Applied to files:
internal/api/handlers/torrents.gointernal/api/handlers/torrents_add_test.gointernal/qbittorrent/sync_manager.go
🔇 Additional comments (6)
go.mod (1)
13-13: go-qbittorrent version bump looks appropriate for batch supportThe pseudo-version update is reasonable for picking up the new batch-adding API, as long as CI/builds are green and
go mod tidyhas been run to sync the module graph.internal/qbittorrent/sync_manager.go (2)
218-225: NewSyncManager field initialization remains consistentThe adjusted struct literal keeps the same defaults (TTL cache, debounce, concurrency limits, tracker caches) and doesn’t alter behavior; no issues here.
1501-1521: AddTorrents implementation cleanly mirrors existing AddTorrent behaviorThe batch method correctly:
- No-ops on an empty
filesslice.- Reuses
getClientAndSyncManagerfor instance resolution.- Delegates to
client.AddTorrentsFromMemoryCtxwith the shared options map.- Triggers
syncAfterModificationonly on success.This matches the existing AddTorrent pattern while reducing HTTP chatter for large batches.
internal/api/handlers/torrents.go (3)
30-36: torrentAdder interface extension keeps tests and production in syncAdding
AddTorrentsto the interface and plumbing it throughgetAppPreferences/addTorrentsmaintains a clean seam between realSyncManagerand mocks; the handler stays agnostic to the concrete implementation.
96-103: addTorrents wrapper is consistent with other adapter helpersThe new helper mirrors
addTorrent/addTorrentFromURLs, preferring the injectedtorrentAdderin tests and falling back tosyncManager.AddTorrentsin production. This keeps handler logic simple and centralizes the decision.
515-529: Batch file path looks good; verify clients are OK with new failedFiles shapeSwitching to a single
addTorrentscall for alltorrentFilesis appropriate for large batches and keeps context deadlines applied once across the whole upload. On batch failure you now emit a single entry withFilename: "batch"instead of per-filename failures, while still preserving per-file read errors separately. That’s reasonable given the API limitation, but it slightly changes the response shape—worth confirming any UI/API consumers handle the"batch"sentinel gracefully.
Summary
Dependencies
Requires autobrr/go-qbittorrent#101
Closes #883
Summary by CodeRabbit
Release Notes
New Features
Chores
✏️ Tip: You can customize this high-level summary in your review settings.