Skip to content

fix(search): download torrent files via backend for remote instances#686

Merged
s0up4200 merged 8 commits intomainfrom
fix/search-add-torrent-remote-instances
Dec 4, 2025
Merged

fix(search): download torrent files via backend for remote instances#686
s0up4200 merged 8 commits intomainfrom
fix/search-add-torrent-remote-instances

Conversation

@s0up4200
Copy link
Collaborator

@s0up4200 s0up4200 commented Dec 4, 2025

Summary by CodeRabbit

  • New Features

    • Optional indexer selection when adding torrents (URL submit and drops); API returns structured per-URL and per-file failure details.
  • Behavior Changes

    • URL submissions: magnets queued directly; non-magnet URLs can be downloaded via selected indexer with per-URL processing, cancellation, and appropriate service-unavailable fallback.
    • UI shows detailed success/partial-failure toasts and refreshes lists after add.
  • Tests

    • Expanded coverage for indexer flows, URL parsing, magnets, fallbacks and error reporting.

✏️ Tip: You can customize this high-level summary in your review settings.

@s0up4200 s0up4200 added this to the v1.9.0 milestone Dec 4, 2025
@s0up4200 s0up4200 added the bugfix label Dec 4, 2025
@s0up4200 s0up4200 linked an issue Dec 4, 2025 that may be closed by this pull request
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 4, 2025

Walkthrough

Adds optional Jackett/indexer handling for URL-based torrent additions: frontend can supply an indexerId; backend downloads non-magnet URLs via Jackett when available, falls back to direct URL/magnet addition otherwise, and returns per-URL and per-file success/failure details.

Changes

Cohort / File(s) Summary
Backend — Torrents handler
internal/api/handlers/torrents.go
Adds jackettService *jackett.Service to TorrentsHandler; updates NewTorrentsHandler signature; adds NewTorrentsHandlerForTesting, testable interfaces (torrentAdder, torrentDownloader), wrapper methods (addTorrent, addTorrentFromURLs, downloadTorrent, getAppPreferences); parses indexer_id from multipart form; per-URL processing with cancellation checks; downloads via Jackett for non-magnet URLs when indexer provided; accumulates added, failed, failedURLs, and failedFiles.
Backend — Server init
internal/api/server.go
Wires s.jackettService into NewTorrentsHandler(...) call (constructor signature updated).
Frontend — API client & types
web/src/lib/api.ts, web/src/types/index.ts
addTorrent payload accepts optional indexerId?: number and returns AddTorrentResponse; multipart form includes indexer_id when present; adds AddTorrentResponse, AddTorrentFailedURL, and AddTorrentFailedFile types.
Frontend — Add Torrent dialog
web/src/components/torrents/AddTorrentDialog.tsx
Extends AddTorrentDropPayload and form FormData to include optional indexerId; preserves and submits indexerId for URL submissions and drag/drop flows; shows toasts based on AddTorrentResponse.
Frontend — Search page
web/src/pages/Search.tsx
Propagates indexerId in AddTorrentDropPayload when dispatching add-torrent actions from search results.
Tests
internal/api/handlers/torrents_add_test.go
Adds comprehensive tests and mocks (mock SyncManager, mock Jackett service), adapters, unit/integration tests for indexer-aware flows (magnets vs HTTP, fallback behavior, empty inputs, per-URL failures), multipart/indexer_id parsing tests, and a benchmark for addTorrentWithIndexer logic.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant User
    participant UI as AddTorrentDialog
    participant API as Web API Client
    participant Handler as TorrentsHandler
    participant Jackett as Jackett Service
    participant QB as qBittorrent

    User->>UI: Submit URLs (+ optional indexerId)
    UI->>API: addTorrent(instanceId, { urls, indexerId? })
    API->>Handler: POST /api/torrents (form: urls, indexer_id?)

    alt indexer_id provided and Jackett available
        loop each URL
            Handler->>Handler: normalize & check cancellation
            alt URL is magnet
                Handler->>QB: Add torrent via URL/magnet
                QB-->>Handler: success/failure
            else non-magnet URL
                Handler->>Jackett: Download torrent (indexer_id, URL)
                Jackett-->>Handler: torrent bytes / error
                opt download success
                    Handler->>QB: Add torrent (file content)
                    QB-->>Handler: success/failure
                end
            end
        end
    else no indexer or Jackett unavailable
        Handler->>QB: AddTorrentFromURLs (batch URLs)
        QB-->>Handler: success/failure per call
    end

    Handler-->>API: { added, failed, failedURLs?, failedFiles? }
    API-->>UI: Response
    UI-->>User: Result / toasts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Focus review on:
    • internal/api/handlers/torrents.go (multipart parsing, per-URL branching, cancellation handling, aggregation of failure details)
    • internal/api/server.go (constructor wiring)
    • Frontend changes: web/src/components/torrents/AddTorrentDialog.tsx, web/src/lib/api.ts, web/src/pages/Search.tsx (type and form propagation)
    • New tests in internal/api/handlers/torrents_add_test.go (correctness and flakiness)

Possibly related PRs

Suggested labels

backend, web, torrent, tests

Poem

🐇 I hopped through code with tiny paws and cheer,
I carried indexer IDs so torrents appear.
Magnets leap, downloads fetch from far,
Each URL tried, we count every spar.
Hooray — the queue hums, the logs are clear!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 59.38% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: enabling backend torrent downloads via Jackett for remote instances, which is the primary enhancement across the Go handlers, TypeScript frontend, and API integration changes.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/search-add-torrent-remote-instances

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
web/src/lib/api.ts (1)

547-592: addTorrent indexerId plumbing is consistent; consider more explicit null check

The indexerId field is correctly added to the client payload and mapped to indexer_id in FormData, matching the server’s expectation.

To be slightly more robust against unexpected 0 values, you could guard on nullish rather than truthiness:

-    if (data.indexerId) formData.append("indexer_id", data.indexerId.toString())
+    if (data.indexerId != null) formData.append("indexer_id", data.indexerId.toString())

This keeps behavior identical for positive IDs while avoiding accidental omission if 0 ever appears.

web/src/components/torrents/AddTorrentDialog.tsx (1)

101-104: Indexer‑aware URL flow in AddTorrentDialog is wired correctly

The new indexerId field is threaded cleanly:

  • AddTorrentDropPayload URL variant carries indexerId.
  • The form tracks indexerId with a default of undefined.
  • URL‑based submissions forward indexerId into api.addTorrent, while file‑based submissions ignore it.

This matches the backend’s indexer_id handling and confines the behavior to the URL tab, so existing file workflows remain unchanged.

Also applies to: 135-136, 513-537, 565-586, 674-706

internal/api/handlers/torrents.go (1)

283-305: Indexer‑based URL torrent addition is well‑integrated with existing behavior

The new indexer_id handling in AddTorrent is structured sensibly:

  • indexer_id is only parsed for URL‑only submissions, matching the frontend’s usage.
  • When indexerID > 0 and jackettService is available:
    • Magnets are still added via AddTorrentFromURLs, preserving the previous path.
    • Non‑magnet URLs are resolved via jackettService.DownloadTorrent and then added via AddTorrent, which fixes the remote‑instance “can’t reach indexer” problem.
    • Per‑URL success/failure is tracked, and partial failures surface in the response message.
  • Without a valid indexer or service, the code falls back to the original AddTorrentFromURLs call, so existing clients continue to work as before.

If you ever need finer‑grained reporting, you could optionally expose the per‑URL failures instead of just counts, but the current behavior is coherent and backwards compatible.

Also applies to: 439-507

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a738ca8 and 7392901.

📒 Files selected for processing (5)
  • internal/api/handlers/torrents.go (5 hunks)
  • internal/api/server.go (1 hunks)
  • web/src/components/torrents/AddTorrentDialog.tsx (5 hunks)
  • web/src/lib/api.ts (2 hunks)
  • web/src/pages/Search.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/jackett/service.go:309-330
Timestamp: 2025-11-27T15:33:39.007Z
Learning: In internal/services/jackett/service.go, synchronous execution paths (test executors, interactive searches with explicit indexer selection, and scheduler-unavailable fallback) intentionally pass jobID=0 to result callbacks. This is acceptable because outcome tracking via ReportIndexerOutcome is only used for async cross-seed operations that always use the scheduler and receive unique jobIDs. Interactive searches and tests do not use outcome tracking.
📚 Learning: 2025-12-03T18:11:08.659Z
Learnt from: finevan
Repo: autobrr/qui PR: 677
File: web/src/components/torrents/AddTorrentDialog.tsx:496-504
Timestamp: 2025-12-03T18:11:08.659Z
Learning: In the AddTorrentDialog component (web/src/components/torrents/AddTorrentDialog.tsx), temporary path settings (useDownloadPath/downloadPath) should be applied per-torrent rather than updating global instance preferences. The UI/UX is designed to suggest that these options apply to the individual torrent being added.

Applied to files:

  • web/src/pages/Search.tsx
  • web/src/components/torrents/AddTorrentDialog.tsx
📚 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:

  • internal/api/server.go
  • internal/api/handlers/torrents.go
📚 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:

  • internal/api/server.go
  • internal/api/handlers/torrents.go
📚 Learning: 2025-11-27T15:33:39.007Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/jackett/service.go:309-330
Timestamp: 2025-11-27T15:33:39.007Z
Learning: In internal/services/jackett/service.go, synchronous execution paths (test executors, interactive searches with explicit indexer selection, and scheduler-unavailable fallback) intentionally pass jobID=0 to result callbacks. This is acceptable because outcome tracking via ReportIndexerOutcome is only used for async cross-seed operations that always use the scheduler and receive unique jobIDs. Interactive searches and tests do not use outcome tracking.

Applied to files:

  • internal/api/handlers/torrents.go
🧬 Code graph analysis (2)
internal/api/server.go (1)
internal/api/handlers/torrents.go (1)
  • NewTorrentsHandler (57-62)
internal/api/handlers/torrents.go (2)
internal/services/jackett/service.go (2)
  • Service (64-89)
  • TorrentDownloadRequest (249-255)
internal/api/handlers/helpers.go (1)
  • RespondError (34-38)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (3)
web/src/pages/Search.tsx (1)

730-751: Indexer context is correctly propagated into AddTorrentDialog

Passing indexerId: result.indexerId in the drop payload cleanly wires the originating indexer through to the add‑torrent flow for remote instances. The change is consistent with the updated AddTorrentDropPayload and backend expectations; no further adjustments needed here.

internal/api/server.go (1)

245-255: TorrentsHandler wiring with jackettService looks correct

Injecting s.jackettService into NewTorrentsHandler aligns with the updated constructor and keeps behavior unchanged when the service is nil.

internal/api/handlers/torrents.go (1)

25-33: jackettService injection into TorrentsHandler is straightforward and safe

Adding jackettService *jackett.Service to TorrentsHandler and extending NewTorrentsHandler to accept it is minimal and keeps callers free to pass nil. Downstream code checks for h.jackettService != nil before use, so there’s no risk of nil dereference.

Also applies to: 57-62

Tests the new indexer_id functionality that downloads torrent files via
the backend for remote qBittorrent instances:

- Download via backend when indexer_id provided
- Fallback to direct URL method without indexer_id or jackett service
- Magnet links bypass jackett and go directly to qBittorrent
- Mixed URL/magnet handling
- Error handling for download and add failures
- Partial success scenarios
- Form field parsing
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
internal/api/handlers/torrents_add_test.go (3)

101-108: Consider using strings.EqualFold for case-insensitive magnet prefix detection.

The current check only handles exactly magnet: or MAGNET: but will miss mixed-case variants like Magnet: or MaGnEt:. While rare, a more robust check would be:

+import "strings"
+
 // Magnet links can be added directly to qBittorrent
-if len(url) > 7 && (url[:7] == "magnet:" || url[:7] == "MAGNET:") {
+if len(url) > 7 && strings.EqualFold(url[:7], "magnet:") {

457-511: Consider eliminating code duplication with addTorrentWithIndexer.

addTorrentWithIndexerCustom duplicates ~50 lines from addTorrentWithIndexer. Since customMockJackettService has the same DownloadTorrent method signature, it could implement jackettServiceAdapter:

+// Ensure customMockJackettService implements jackettServiceAdapter
+var _ jackettServiceAdapter = (*customMockJackettService)(nil)
+
 // addTorrentWithIndexerCustom is like addTorrentWithIndexer but accepts customMockJackettService
 func addTorrentWithIndexerCustom(
 	ctx context.Context,
 	syncManager syncManagerAdapter,
 	jackettService *customMockJackettService,
 	instanceID int,
 	urls []string,
 	indexerID int,
 	options map[string]string,
 ) (addedCount int, failedCount int, lastError error) {
-	if indexerID > 0 && jackettService != nil {
-		// ... duplicated logic ...
-	}
-	return addedCount, failedCount, lastError
+	return addTorrentWithIndexer(ctx, syncManager, jackettService, instanceID, urls, indexerID, options)
 }

725-756: Consider using standard library string functions.

The custom implementations can be simplified using the standard library:

+import "strings"
+
 // processURLSeparators converts newlines to commas (like the handler)
 func processURLSeparators(s string) string {
-	result := make([]byte, 0, len(s))
-	for i := 0; i < len(s); i++ {
-		if s[i] == '\n' {
-			result = append(result, ',')
-		} else {
-			result = append(result, s[i])
-		}
-	}
-	return string(result)
+	return strings.ReplaceAll(s, "\n", ",")
 }

 // splitURLs splits on comma (like the handler)
 func splitURLs(s string) []string {
-	var result []string
-	var current []byte
-	for i := 0; i < len(s); i++ {
-		if s[i] == ',' {
-			if len(current) > 0 {
-				result = append(result, string(current))
-				current = current[:0]
-			}
-		} else {
-			current = append(current, s[i])
-		}
-	}
-	if len(current) > 0 {
-		result = append(result, string(current))
-	}
-	return result
+	var result []string
+	for _, part := range strings.Split(s, ",") {
+		if part != "" {
+			result = append(result, part)
+		}
+	}
+	return result
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7392901 and a11afdb.

📒 Files selected for processing (1)
  • internal/api/handlers/torrents_add_test.go (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/jackett/service.go:309-330
Timestamp: 2025-11-27T15:33:39.007Z
Learning: In internal/services/jackett/service.go, synchronous execution paths (test executors, interactive searches with explicit indexer selection, and scheduler-unavailable fallback) intentionally pass jobID=0 to result callbacks. This is acceptable because outcome tracking via ReportIndexerOutcome is only used for async cross-seed operations that always use the scheduler and receive unique jobIDs. Interactive searches and tests do not use outcome tracking.
📚 Learning: 2025-11-27T15:33:39.007Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/jackett/service.go:309-330
Timestamp: 2025-11-27T15:33:39.007Z
Learning: In internal/services/jackett/service.go, synchronous execution paths (test executors, interactive searches with explicit indexer selection, and scheduler-unavailable fallback) intentionally pass jobID=0 to result callbacks. This is acceptable because outcome tracking via ReportIndexerOutcome is only used for async cross-seed operations that always use the scheduler and receive unique jobIDs. Interactive searches and tests do not use outcome tracking.

Applied to files:

  • internal/api/handlers/torrents_add_test.go
🧬 Code graph analysis (1)
internal/api/handlers/torrents_add_test.go (1)
internal/services/jackett/service.go (1)
  • TorrentDownloadRequest (249-255)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (5)
internal/api/handlers/torrents_add_test.go (5)

1-20: LGTM!

The imports are appropriate for the test file, including the necessary testing utilities from testify and chi for route context handling.


22-70: LGTM!

The mock implementations follow standard Go testing patterns with proper call tracking and configurable return values.


72-81: LGTM!

The adapter interfaces correctly define the minimal contract needed for testing. The interfaces allow for clean dependency injection in the test helper functions.


140-171: LGTM!

Well-structured test with clear assertions verifying the download-via-backend flow. Good separation of require (for prerequisites) and assert (for verifications).


758-775: LGTM!

The benchmark correctly resets state between iterations and measures the core logic path.

- Remove unused callCount and originalDownload variables
- Fix assertion to only check actual mock calls
- Replace buggy custom parseInt with strconv.Atoi

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
internal/api/handlers/torrents_add_test.go (5)

84-139: Core indexer logic duplicated in test-only helpers; consider sharing implementation with the handler

addTorrentWithIndexer and addTorrentWithIndexerCustom encode the core indexer-aware add logic entirely inside a _test.go file. If the real handler’s implementation diverges, these tests can still pass while production behavior breaks. On top of that, addTorrentWithIndexerCustom is effectively a copy of addTorrentWithIndexer; customMockJackettService already satisfies jackettServiceAdapter, so you can reuse a single helper.

Consider:

  • Moving this logic into a non-test helper in the handler package and calling it from both the HTTP handler and tests, or
  • Driving the tests against the real handler (with mocks) instead of a reimplemented copy, and
  • Dropping addTorrentWithIndexerCustom and using addTorrentWithIndexer with customMockJackettService via the existing jackettServiceAdapter interface.

This reduces drift risk and avoids maintaining two near-identical code paths.

Also applies to: 451-505


507-592: Clarify whether negative indexer_id values are actually valid

TestParseIndexerIDFromForm asserts that a negative indexer_id string parses to a negative int and preserves that value. Downstream, your logic branches on indexerID > 0, so negative values effectively behave like “no indexer” but the test now enshrines accepting negative IDs.

If negative IDs are not meaningful in your domain, it might be safer to:

  • Treat non-positive values as “no indexer” at parse time (e.g., clamp < 1 to 0), and/or
  • Adjust the test to expect 0 for negative input.

Otherwise, keeping this test is fine but it would be good to explicitly document that negative indexer IDs are tolerated but ignored later.


594-625: “Handler” test only exercises form parsing, not the actual handler

TestAddTorrentHandler_IndexerIDInForm currently constructs a multipart request, calls ParseMultipartForm, and asserts on FormValue results, but never invokes the real TorrentsHandler method. The name suggests a higher-level handler test.

Two options to reduce confusion and increase value:

  • Either rename the test to reflect that it only validates multipart parsing behavior, or
  • Extend it to actually instantiate a TorrentsHandler, call the appropriate handler function via httptest.NewRecorder, and assert that indexer_id and related fields are wired into the sync/jackett calls as expected.

That would give you genuine end-to-end coverage of the new indexer-aware path.


627-727: Consider expanding URL-processing coverage (whitespace/CRLF) and reusing handler helpers if available

TestAddTorrentURLProcessing with processURLSeparators and splitURLs currently handles \n and , separators and ignores empty segments, but does not cover:

  • Windows-style \r\n newlines,
  • Leading/trailing spaces around URLs, or
  • Multiple consecutive separators.

If real-world input from the UI or API can include those cases, you may want to:

  • Either extend these helpers to mirror the production handler’s URL normalization exactly (e.g., trimming spaces, handling \r\n), or
  • Reuse the actual handler helpers here (if such functions already exist in non-test code) so behavior and tests stay in lockstep.

Not strictly required for correctness given current expectations but could harden the path against messy input.


729-746: Reset addTorrentFromURLsCalls in the benchmark loop for future-proofing

In BenchmarkAddTorrentWithIndexer, you reset mockSync.addTorrentCalls and mockJackett.downloadTorrentCalls inside the loop, but leave mockSync.addTorrentFromURLsCalls untouched. Today the benchmark path doesn’t hit AddTorrentFromURLs, so this is harmless, but if the logic ever starts using magnets or falls back in some cases, the benchmark will accumulate stale state.

You can future-proof this by also resetting addTorrentFromURLsCalls inside the loop:

 for i := 0; i < b.N; i++ {
-    mockSync.addTorrentCalls = nil
+    mockSync.addTorrentCalls = nil
+    mockSync.addTorrentFromURLsCalls = nil
     mockJackett.downloadTorrentCalls = nil
     _, _, _ = addTorrentWithIndexer(ctx, mockSync, mockJackett, 1, urls, 42, options)
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a11afdb and bcf4ba9.

📒 Files selected for processing (1)
  • internal/api/handlers/torrents_add_test.go (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/jackett/service.go:309-330
Timestamp: 2025-11-27T15:33:39.007Z
Learning: In internal/services/jackett/service.go, synchronous execution paths (test executors, interactive searches with explicit indexer selection, and scheduler-unavailable fallback) intentionally pass jobID=0 to result callbacks. This is acceptable because outcome tracking via ReportIndexerOutcome is only used for async cross-seed operations that always use the scheduler and receive unique jobIDs. Interactive searches and tests do not use outcome tracking.
📚 Learning: 2025-11-27T15:33:39.007Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/jackett/service.go:309-330
Timestamp: 2025-11-27T15:33:39.007Z
Learning: In internal/services/jackett/service.go, synchronous execution paths (test executors, interactive searches with explicit indexer selection, and scheduler-unavailable fallback) intentionally pass jobID=0 to result callbacks. This is acceptable because outcome tracking via ReportIndexerOutcome is only used for async cross-seed operations that always use the scheduler and receive unique jobIDs. Interactive searches and tests do not use outcome tracking.

Applied to files:

  • internal/api/handlers/torrents_add_test.go
📚 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:

  • internal/api/handlers/torrents_add_test.go
🧬 Code graph analysis (1)
internal/api/handlers/torrents_add_test.go (1)
internal/services/jackett/service.go (1)
  • TorrentDownloadRequest (249-255)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (1)
internal/api/handlers/torrents_add_test.go (1)

141-252: Indexer-aware and magnet-path tests look comprehensive

The tests around addTorrentWithIndexer for pure indexer downloads, fallback, magnet-only, and mixed magnet/HTTP flows exercise the important branches (Jackett vs direct URL, magnet detection, and proper call routing). The use of mocks and explicit call assertions makes the intended behavior clear.

No changes needed here.

- Remove duplicate addTorrentWithIndexerCustom, reuse existing helper
  via jackettServiceAdapter interface
- Rename misleading test to TestMultipartFormParsing_IndexerID
- Document negative indexer ID handling behavior
- Reset all mock slices in benchmark for future-proofing
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
internal/api/handlers/torrents_add_test.go (3)

84-139: Avoid duplicating core torrent/indexer logic in test-only addTorrentWithIndexer and consider simplifying magnet detection.

This helper fully reimplements the handler’s indexer-aware add logic but lives only in tests, so changes in the real handler won’t automatically be exercised. Consider factoring this into a shared unexported helper in the production code and calling it from both the handler and tests, or driving tests through the real handler with mocks, to avoid drift.

Also, the magnet detection currently relies on manual slicing and only accepts magnet: and MAGNET:; using strings.HasPrefix / strings.EqualFold (with a len(url) >= len("magnet:") guard) would be clearer and more robust to casing edge cases.


643-674: Consider reusing the production URL-normalization helpers or standard library functions.

processURLSeparators and splitURLs manually implement newline-to-comma replacement and comma-splitting. If the handler already has equivalent helpers, it would be safer to call those from tests (or move them into a shared internal package) instead of reimplementing them here. If not, and exact matching with handler code isn’t required, you could simplify this using strings.ReplaceAll plus strings.Split and optional trimming.


676-694: Verify b.Loop() usage against your Go version and CI toolchain.

b.Loop() is only available in newer Go releases; if your module or CI still targets Go versions without this API, the benchmark won’t compile. For broader compatibility you can fall back to the classic pattern:

 func BenchmarkAddTorrentWithIndexer(b *testing.B) {
@@
-	b.ResetTimer()
-	for b.Loop() {
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
 		mockSync.addTorrentCalls = nil
 		mockSync.addTorrentFromURLsCalls = nil
 		mockJackett.downloadTorrentCalls = nil
 		_, _, _ = addTorrentWithIndexer(ctx, mockSync, mockJackett, 1, urls, 42, options)
 	}
 }

Please confirm that your Go toolchain supports testing.B.Loop if you keep the current form.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bcf4ba9 and cc2836a.

📒 Files selected for processing (1)
  • internal/api/handlers/torrents_add_test.go (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/jackett/service.go:309-330
Timestamp: 2025-11-27T15:33:39.007Z
Learning: In internal/services/jackett/service.go, synchronous execution paths (test executors, interactive searches with explicit indexer selection, and scheduler-unavailable fallback) intentionally pass jobID=0 to result callbacks. This is acceptable because outcome tracking via ReportIndexerOutcome is only used for async cross-seed operations that always use the scheduler and receive unique jobIDs. Interactive searches and tests do not use outcome tracking.
📚 Learning: 2025-11-27T15:33:39.007Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/jackett/service.go:309-330
Timestamp: 2025-11-27T15:33:39.007Z
Learning: In internal/services/jackett/service.go, synchronous execution paths (test executors, interactive searches with explicit indexer selection, and scheduler-unavailable fallback) intentionally pass jobID=0 to result callbacks. This is acceptable because outcome tracking via ReportIndexerOutcome is only used for async cross-seed operations that always use the scheduler and receive unique jobIDs. Interactive searches and tests do not use outcome tracking.

Applied to files:

  • internal/api/handlers/torrents_add_test.go
📚 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:

  • internal/api/handlers/torrents_add_test.go
🧬 Code graph analysis (1)
internal/api/handlers/torrents_add_test.go (1)
internal/services/jackett/service.go (1)
  • TorrentDownloadRequest (249-255)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (3)
internal/api/handlers/torrents_add_test.go (3)

141-371: Index-aware add tests give solid coverage of success, failure, and mixed URL scenarios.

The suite around addTorrentWithIndexer (direct downloads, magnets-only, mixed URLs, download failures, and add failures using customMockJackettService) exercises the intended added/failed counts and lastError semantics well, and the per-call recording on the mocks makes the behavior easy to reason about.


451-572: Form parsing tests accurately pin down indexer_id, urls, and category behavior.

The multipart-based tests validate indexer_id parsing across valid/invalid/empty/negative cases and confirm the raw form values for urls and category, which should catch regressions in how the handler reads those fields.


574-641: URL normalization tests cover the key separator transformations.

The table-driven cases for single, newline-separated, comma-separated, and mixed magnet/HTTP URLs give good confidence that the handler’s URL normalization (as emulated by these helpers) behaves as expected.

…ions

- Return 400 Bad Request for invalid/negative/zero indexer_id instead
  of silently falling back to direct URL method
- Return 503 Service Unavailable when indexer_id provided but jackett
  service is not configured
- Add instanceID to download error logs for better debugging
- Add url and indexerID to add-torrent error logs
- Track failed URLs and include them in API response for partial failures
- Log skipped empty URLs at debug level
- Align test helper magnet detection with handler implementation
- Add HTTP handler integration tests for new error paths
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
internal/api/handlers/torrents_add_test.go (2)

85-141: Good extraction of core logic for unit testing.

The addTorrentWithIndexer function mirrors the handler's indexer-aware logic, allowing focused unit tests without HTTP concerns. Be aware that this creates a synchronization burden—changes to the handler logic must be reflected here.

Consider adding a code comment noting that this function must stay in sync with TorrentsHandler.AddTorrent, or alternatively, refactoring the handler to call this shared function to avoid divergence.


645-676: Custom URL processing helpers duplicate strings.ReplaceAll and strings.Split behavior.

These helper functions (processURLSeparators, splitURLs) reimplement standard library functionality. The handler uses strings.ReplaceAll and strings.Split directly.

Consider simplifying these helpers:

-// processURLSeparators converts newlines to commas (like the handler)
-func processURLSeparators(s string) string {
-	result := make([]byte, 0, len(s))
-	for i := 0; i < len(s); i++ {
-		if s[i] == '\n' {
-			result = append(result, ',')
-		} else {
-			result = append(result, s[i])
-		}
-	}
-	return string(result)
-}
-
-// splitURLs splits on comma (like the handler)
-func splitURLs(s string) []string {
-	var result []string
-	var current []byte
-	for i := 0; i < len(s); i++ {
-		if s[i] == ',' {
-			if len(current) > 0 {
-				result = append(result, string(current))
-				current = current[:0]
-			}
-		} else {
-			current = append(current, s[i])
-		}
-	}
-	if len(current) > 0 {
-		result = append(result, string(current))
-	}
-	return result
-}
+// processURLSeparators converts newlines to commas (like the handler)
+func processURLSeparators(s string) string {
+	return strings.ReplaceAll(s, "\n", ",")
+}
+
+// splitURLs splits on comma (like the handler)
+func splitURLs(s string) []string {
+	return strings.Split(s, ",")
+}

Note: The handler's strings.Split doesn't filter empty strings, but your custom implementation does. If that's intentional, document why the test needs different behavior.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cc2836a and ee29fa9.

📒 Files selected for processing (2)
  • internal/api/handlers/torrents.go (7 hunks)
  • internal/api/handlers/torrents_add_test.go (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/jackett/service.go:309-330
Timestamp: 2025-11-27T15:33:39.007Z
Learning: In internal/services/jackett/service.go, synchronous execution paths (test executors, interactive searches with explicit indexer selection, and scheduler-unavailable fallback) intentionally pass jobID=0 to result callbacks. This is acceptable because outcome tracking via ReportIndexerOutcome is only used for async cross-seed operations that always use the scheduler and receive unique jobIDs. Interactive searches and tests do not use outcome tracking.
📚 Learning: 2025-11-27T15:33:39.007Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/jackett/service.go:309-330
Timestamp: 2025-11-27T15:33:39.007Z
Learning: In internal/services/jackett/service.go, synchronous execution paths (test executors, interactive searches with explicit indexer selection, and scheduler-unavailable fallback) intentionally pass jobID=0 to result callbacks. This is acceptable because outcome tracking via ReportIndexerOutcome is only used for async cross-seed operations that always use the scheduler and receive unique jobIDs. Interactive searches and tests do not use outcome tracking.

Applied to files:

  • internal/api/handlers/torrents.go
  • internal/api/handlers/torrents_add_test.go
📚 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:

  • internal/api/handlers/torrents.go
  • internal/api/handlers/torrents_add_test.go
📚 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:

  • internal/api/handlers/torrents.go
🧬 Code graph analysis (2)
internal/api/handlers/torrents.go (3)
internal/services/crossseed/service.go (1)
  • Service (236-287)
internal/services/jackett/service.go (2)
  • Service (64-89)
  • TorrentDownloadRequest (249-255)
internal/api/handlers/helpers.go (2)
  • RespondError (34-38)
  • RespondJSON (22-31)
internal/api/handlers/torrents_add_test.go (2)
internal/services/jackett/service.go (1)
  • TorrentDownloadRequest (249-255)
internal/api/handlers/torrents.go (1)
  • NewTorrentsHandler (57-62)
🔇 Additional comments (12)
internal/api/handlers/torrents.go (7)

30-32: LGTM! Handler struct and constructor updated correctly for Jackett integration.

The TorrentsHandler struct now properly includes jackettService as an optional dependency, and the constructor wires it correctly.

Also applies to: 57-61


284-310: Solid validation for indexer_id with clear error messages.

The validation correctly:

  • Parses indexer_id only when URLs are provided (not for file uploads)
  • Returns 400 with a descriptive message for non-integer values
  • Returns 400 for non-positive integers

418-426: Good addition of structured error tracking for per-URL failures.

The failedURL struct provides useful feedback to clients about which specific URLs failed and why.


453-460: Appropriate 503 response when Jackett service is unavailable.

When a client provides indexer_id but the Jackett service isn't configured, returning 503 Service Unavailable with a helpful message is the right approach.


461-520: Well-structured per-URL processing with proper error handling.

The logic correctly:

  • Skips empty URLs with debug logging
  • Handles context cancellation for graceful shutdown
  • Passes magnet links directly to qBittorrent (case-insensitive check)
  • Downloads non-magnet URLs via Jackett, then adds via AddTorrent
  • Continues processing remaining URLs on individual failures
  • Tracks per-URL failures with specific error messages

521-533: Fallback path correctly preserved for non-indexer flows.

When indexer_id is not provided, the handler falls back to the original AddTorrentFromURLs behavior, maintaining backward compatibility.


556-564: Response properly includes failed URL details when applicable.

The conditional addition of failedURLs to the response provides actionable feedback without cluttering successful responses.

internal/api/handlers/torrents_add_test.go (5)

24-72: Well-structured mock implementations for testing.

The mock types cleanly capture call history and allow configurable error responses, enabling thorough testing of both success and failure paths.


289-323: Dead code from past review has been addressed.

The previous review flagged unused callCount and originalDownload variables. These have been removed, and line 319 now correctly uses only len(customJackett.calls).


706-733: Good integration test for invalid indexer_id handling.

This test properly verifies that non-integer indexer_id values return 400 with an appropriate error message.


789-817: Test correctly verifies 503 response when Jackett is unavailable.

This ensures the handler returns the appropriate service unavailable error when indexer_id is provided but jackettService is nil.


143-429: Comprehensive test coverage for indexer-aware torrent addition.

The tests cover key scenarios:

  • Download via backend with valid indexer
  • Fallback without indexer ID
  • Fallback with nil Jackett service
  • Magnet link passthrough (including case-insensitivity)
  • Mixed URLs and magnets
  • Partial failures with continuation
  • Empty URL handling

- Add interface-based testing support with torrentAdder and
  torrentDownloader interfaces for mocking dependencies
- Add NewTorrentsHandlerForTesting constructor for test isolation
- Add handler integration tests covering:
  - Successful indexer download flow (HTTP 201)
  - Magnet links bypassing indexer download
  - Mixed URLs and magnets handling
  - Partial failure with failedURLs response
  - Direct URL fallback when no indexer_id
- Track and report failed file uploads in response with failedFiles
- Change file read errors from Error to Warn log level
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6ff26ec and bd57193.

📒 Files selected for processing (3)
  • internal/api/handlers/torrents.go (9 hunks)
  • web/src/components/torrents/AddTorrentDialog.tsx (5 hunks)
  • web/src/lib/api.ts (4 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/jackett/service.go:309-330
Timestamp: 2025-11-27T15:33:39.007Z
Learning: In internal/services/jackett/service.go, synchronous execution paths (test executors, interactive searches with explicit indexer selection, and scheduler-unavailable fallback) intentionally pass jobID=0 to result callbacks. This is acceptable because outcome tracking via ReportIndexerOutcome is only used for async cross-seed operations that always use the scheduler and receive unique jobIDs. Interactive searches and tests do not use outcome tracking.
📚 Learning: 2025-12-03T18:11:08.659Z
Learnt from: finevan
Repo: autobrr/qui PR: 677
File: web/src/components/torrents/AddTorrentDialog.tsx:496-504
Timestamp: 2025-12-03T18:11:08.659Z
Learning: In the AddTorrentDialog component (web/src/components/torrents/AddTorrentDialog.tsx), temporary path settings (useDownloadPath/downloadPath) should be applied per-torrent rather than updating global instance preferences. The UI/UX is designed to suggest that these options apply to the individual torrent being added.

Applied to files:

  • web/src/components/torrents/AddTorrentDialog.tsx
📚 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:

  • web/src/lib/api.ts
  • internal/api/handlers/torrents.go
📚 Learning: 2025-11-27T15:33:39.007Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/jackett/service.go:309-330
Timestamp: 2025-11-27T15:33:39.007Z
Learning: In internal/services/jackett/service.go, synchronous execution paths (test executors, interactive searches with explicit indexer selection, and scheduler-unavailable fallback) intentionally pass jobID=0 to result callbacks. This is acceptable because outcome tracking via ReportIndexerOutcome is only used for async cross-seed operations that always use the scheduler and receive unique jobIDs. Interactive searches and tests do not use outcome tracking.

Applied to files:

  • internal/api/handlers/torrents.go
📚 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:

  • internal/api/handlers/torrents.go
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Build Docker images (linux/arm/v7)
  • GitHub Check: Build Docker images (linux/arm64)
  • GitHub Check: Build Docker images (linux/amd64/v2)
  • GitHub Check: Build Docker images (linux/amd64)
  • GitHub Check: Build Docker images (linux/arm/v6)
  • GitHub Check: Build Docker images (linux/amd64/v3)
  • GitHub Check: Build distribution binaries
🔇 Additional comments (4)
web/src/lib/api.ts (1)

568-568: LGTM! Clean addition of indexerId parameter.

The optional indexerId field is properly typed and correctly appended to the form data with appropriate snake_case naming for the backend.

Also applies to: 594-594

web/src/components/torrents/AddTorrentDialog.tsx (1)

104-104: LGTM! Proper indexerId threading through form state.

The indexerId field is correctly:

  • Typed as optional in both the drop payload and form data interfaces
  • Limited to URL-based additions (not file uploads)
  • Initialized with a proper default value
  • Propagated from drop payloads to form state
  • Conditionally included in API submissions

Also applies to: 136-136, 528-530, 579-579, 690-690

internal/api/handlers/torrents.go (2)

72-122: LGTM! Clean testing architecture.

The testing interfaces and wrapper methods provide a clean way to inject mocks without coupling production code to test infrastructure. The consistent delegation pattern (mock-first, then real implementation) is well-executed.


536-616: Well-structured indexer-based torrent download flow.

The implementation correctly:

  • Validates jackett service availability before attempting downloads
  • Processes each URL individually with cancellation checks
  • Handles magnet links separately (they bypass indexer downloads)
  • Tracks per-URL failures for clear user feedback
  • Maintains backward compatibility when no indexerID is provided

The per-URL error tracking is a significant UX improvement over the previous all-or-nothing approach.

- Add AddTorrentResponse type matching backend response shape
- Update api.addTorrent return type to include added/failed counts
- Show toast notifications with success, error, or partial failure info
- Display up to 3 failure details for failed URLs/files
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
web/src/components/torrents/AddTorrentDialog.tsx (1)

536-578: Enhanced response handling with detailed user feedback.

The toast logic correctly handles all success, all failure, and partial success scenarios. Failure details are appropriately limited to the first 3 items for readability.

Optional: Consider truncating long URLs in toast descriptions.

Long download URLs in failure messages may be difficult to read in toasts. Consider truncating URLs to ~50 characters with ellipsis for better UX:

       const failedDetails = [
-        ...(response.failedURLs?.map(f => `${f.url}: ${f.error}`) ?? []),
+        ...(response.failedURLs?.map(f => {
+          const truncatedUrl = f.url.length > 50 ? f.url.slice(0, 47) + '...' : f.url
+          return `${truncatedUrl}: ${f.error}`
+        }) ?? []),
         ...(response.failedFiles?.map(f => `${f.filename}: ${f.error}`) ?? [])
       ]

Apply similar truncation at line 570-572.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd57193 and 38e95c3.

📒 Files selected for processing (3)
  • web/src/components/torrents/AddTorrentDialog.tsx (7 hunks)
  • web/src/lib/api.ts (5 hunks)
  • web/src/types/index.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/lib/api.ts
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/jackett/service.go:309-330
Timestamp: 2025-11-27T15:33:39.007Z
Learning: In internal/services/jackett/service.go, synchronous execution paths (test executors, interactive searches with explicit indexer selection, and scheduler-unavailable fallback) intentionally pass jobID=0 to result callbacks. This is acceptable because outcome tracking via ReportIndexerOutcome is only used for async cross-seed operations that always use the scheduler and receive unique jobIDs. Interactive searches and tests do not use outcome tracking.
📚 Learning: 2025-12-03T18:11:08.659Z
Learnt from: finevan
Repo: autobrr/qui PR: 677
File: web/src/components/torrents/AddTorrentDialog.tsx:496-504
Timestamp: 2025-12-03T18:11:08.659Z
Learning: In the AddTorrentDialog component (web/src/components/torrents/AddTorrentDialog.tsx), temporary path settings (useDownloadPath/downloadPath) should be applied per-torrent rather than updating global instance preferences. The UI/UX is designed to suggest that these options apply to the individual torrent being added.

Applied to files:

  • web/src/types/index.ts
  • web/src/components/torrents/AddTorrentDialog.tsx
📚 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:

  • web/src/components/torrents/AddTorrentDialog.tsx
🧬 Code graph analysis (1)
web/src/components/torrents/AddTorrentDialog.tsx (1)
web/src/types/index.ts (1)
  • AddTorrentResponse (352-358)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (7)
web/src/types/index.ts (1)

342-358: LGTM! Well-structured failure reporting interfaces.

The new interfaces provide clear per-URL and per-file failure reporting, enabling detailed user feedback. The optional failure arrays in AddTorrentResponse appropriately handle success scenarios where no failures occur.

web/src/components/torrents/AddTorrentDialog.tsx (6)

42-42: LGTM! Correct type import.

The AddTorrentResponse import enables proper typing of the API response in the mutation's onSuccess handler.


103-105: LGTM! Backward-compatible payload extension.

The optional indexerId field enables indexer-aware URL handling while maintaining compatibility with existing code paths.


137-137: LGTM!

Adding indexerId to FormData appropriately extends the form state to track indexer context for URL-based additions.


529-531: LGTM! Correct conditional propagation.

The indexerId is appropriately included only for URL-based submissions when present, aligning with backend indexer-aware handling.


608-608: LGTM!

Initializing indexerId to undefined is correct for an optional field in the form defaults.


719-719: LGTM! Completes the indexerId data flow.

Propagating indexerId from the drop payload into form state ensures the indexer context is preserved through URL-based torrent additions from the Search page.

@s0up4200 s0up4200 merged commit 298ca05 into main Dec 4, 2025
11 checks passed
@s0up4200 s0up4200 deleted the fix/search-add-torrent-remote-instances branch December 4, 2025 20:30
alexlebens pushed a commit to alexlebens/infrastructure that referenced this pull request Dec 11, 2025
This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/autobrr/qui](https://github.com/autobrr/qui) | minor | `v1.8.1` -> `v1.9.1` |

---

### Release Notes

<details>
<summary>autobrr/qui (ghcr.io/autobrr/qui)</summary>

### [`v1.9.1`](https://github.com/autobrr/qui/releases/tag/v1.9.1)

[Compare Source](autobrr/qui@v1.9.0...v1.9.1)

#### Changelog

##### Bug Fixes

- [`441418b`](autobrr/qui@441418b): fix(api): remove user\_id session check from dashboard settings ([#&#8203;711](autobrr/qui#711)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`bd2587b`](autobrr/qui@bd2587b): fix(db): resolve cross-seed settings mutual exclusivity lockout ([#&#8203;714](autobrr/qui#714)) ([@&#8203;s0up4200](https://github.com/s0up4200))

**Full Changelog**: <autobrr/qui@v1.9.0...v1.9.1>

#### Docker images

- `docker pull ghcr.io/autobrr/qui:v1.9.1`
- `docker pull ghcr.io/autobrr/qui:latest`

#### What to do next?

- Join our [Discord server](https://discord.autobrr.com/qui)

Thank you for using qui!

### [`v1.9.0`](https://github.com/autobrr/qui/releases/tag/v1.9.0)

[Compare Source](autobrr/qui@v1.8.1...v1.9.0)

#### Changelog

##### Important

Cross-seeds are now added to `.cross`-suffixed categories by default. This is opt-out. The old delay logic is removed.

##### Highlights

- Customize your Dashboard-page (order, visibility)
- Tracker Breakdown section in Dashboard with import/export functionality
- Warnings and actions for cross-seeds when you attempt to delete torrents
- Show free space in torrent table footer

##### New Features

- [`1aa7360`](autobrr/qui@1aa7360): feat(dashboard): tracker breakdown and customizable layout ([#&#8203;637](autobrr/qui#637)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`85fd74b`](autobrr/qui@85fd74b): feat(jackett): propagate 429 rate limits with retry and cooldown ([#&#8203;684](autobrr/qui#684)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`a5777c4`](autobrr/qui@a5777c4): feat(reannounce): add configurable max retries setting ([#&#8203;685](autobrr/qui#685)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`6451e56`](autobrr/qui@6451e56): feat(settings): add TMM relocation behavior settings ([#&#8203;664](autobrr/qui#664)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`680fd25`](autobrr/qui@680fd25): feat(torrents): add confirmation dialogs for TMM and Set Location ([#&#8203;687](autobrr/qui#687)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`7f779f9`](autobrr/qui@7f779f9): feat(torrents): warn about cross-seeded torrents in delete dialogs ([#&#8203;670](autobrr/qui#670)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`1c489bc`](autobrr/qui@1c489bc): feat(ui): persist category collapse state in sidebar ([#&#8203;692](autobrr/qui#692)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`bdf807e`](autobrr/qui@bdf807e): feat(web): Torrent list details bar shows free space ([#&#8203;691](autobrr/qui#691)) ([@&#8203;finevan](https://github.com/finevan))

##### Bug Fixes

- [`9db8346`](autobrr/qui@9db8346): fix(crossseed): use matched torrent save path instead of category path ([#&#8203;700](autobrr/qui#700)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`40d7778`](autobrr/qui@40d7778): fix(instance): intern empty string on demand for bypass auth ([#&#8203;693](autobrr/qui#693)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`0aaf39e`](autobrr/qui@0aaf39e): fix(jackett): fetch indexer capabilities in parallel with retries ([#&#8203;701](autobrr/qui#701)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`50e585b`](autobrr/qui@50e585b): fix(qbittorrent): cache tracker health counts in background ([#&#8203;662](autobrr/qui#662)) ([@&#8203;KyleSanderson](https://github.com/KyleSanderson))
- [`298ca05`](autobrr/qui@298ca05): fix(search): download torrent files via backend for remote instances ([#&#8203;686](autobrr/qui#686)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`27ee31a`](autobrr/qui@27ee31a): fix(torrents): AddTorrentDialog uses the downloadPath api ([#&#8203;677](autobrr/qui#677)) ([@&#8203;finevan](https://github.com/finevan))
- [`2427fdd`](autobrr/qui@2427fdd): fix(ui): use full category paths in multi-select ([#&#8203;683](autobrr/qui#683)) ([@&#8203;jabloink](https://github.com/jabloink))
- [`917c65e`](autobrr/qui@917c65e): fix(web): add iOS Safari compatibility for torrent file picker ([#&#8203;707](autobrr/qui#707)) ([@&#8203;s0up4200](https://github.com/s0up4200))
- [`2ccdc28`](autobrr/qui@2ccdc28): fix(web): dont hide free space when disk is full ([#&#8203;694](autobrr/qui#694)) ([@&#8203;ewenjo](https://github.com/ewenjo))

##### Other Changes

- [`d684442`](autobrr/qui@d684442): chore(deps): bump the golang group with 7 updates ([#&#8203;660](autobrr/qui#660)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`e1267fa`](autobrr/qui@e1267fa): chore(deps): bump the npm group across 1 directory with 29 updates ([#&#8203;663](autobrr/qui#663)) ([@&#8203;dependabot](https://github.com/dependabot)\[bot])
- [`8671971`](autobrr/qui@8671971): docs: Update README to remove size field description ([#&#8203;695](autobrr/qui#695)) ([@&#8203;s0up4200](https://github.com/s0up4200))

**Full Changelog**: <autobrr/qui@v1.8.1...v1.9.0>

#### Docker images

- `docker pull ghcr.io/autobrr/qui:v1.9.0`
- `docker pull ghcr.io/autobrr/qui:latest`

#### What to do next?

- Join our [Discord server](https://discord.autobrr.com/qui)

Thank you for using qui!

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0Mi4zOS4xIiwidXBkYXRlZEluVmVyIjoiNDIuMzkuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiaW1hZ2UiXX0=-->

Reviewed-on: https://gitea.alexlebens.dev/alexlebens/infrastructure/pulls/2366
Co-authored-by: Renovate Bot <renovate-bot@alexlebens.net>
Co-committed-by: Renovate Bot <renovate-bot@alexlebens.net>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Adding torrents from Search page only works to local instances

1 participant