Skip to content

fix(jackett): fetch indexer capabilities in parallel with retries#701

Merged
s0up4200 merged 12 commits intomainfrom
fix/parallel-caps-discovery
Dec 7, 2025
Merged

fix(jackett): fetch indexer capabilities in parallel with retries#701
s0up4200 merged 12 commits intomainfrom
fix/parallel-caps-discovery

Conversation

@s0up4200
Copy link
Collaborator

@s0up4200 s0up4200 commented Dec 5, 2025

Summary by CodeRabbit

  • Performance Improvements

    • Faster, more resilient indexer discovery via parallel, retriable capability/category fetching with context-driven timeouts and improved partial-failure logging.
  • New Features

    • Discovery, create and update flows now include and persist optional indexer categories; API responses can include warnings (IndexerResponse/Discover response types).
  • UI

    • Import/create/update dialogs surface warning counts/details and show warnings via toasts; info links display an external-link icon.

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

@s0up4200 s0up4200 added enhancement New feature or request bugfix cross-seed labels Dec 5, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 5, 2025

Walkthrough

Discovery was made context-aware and reworked to fetch capabilities and categories in parallel with retries and cancellation; Jackett indexer models gained Categories and a DiscoveryResult wrapper; API handlers and frontend were updated to accept/persist categories and surface discovery warnings.

Changes

Cohort / File(s) Summary
Jackett client — parallel capability & category fetch
internal/services/jackett/client.go
Discovery now accepts context.Context, returns DiscoveryResult (indexers + warnings), adds Categories []models.TorznabIndexerCategory on JackettIndexer, and implements parallel, retry-enabled capability/category fetchs via fetchCapsParallel, fetchCapsWithRetry, capsFetchResult, GetCapabilitiesDirect(), and related concurrency, logging and error-aggregation logic.
API handlers — create/update & discovery
internal/api/handlers/jackett.go
Added public IndexerResponse (indexer + optional Warnings); CreateIndexer/UpdateIndexer accept and persist Categories, reload indexers after storing caps/categories, accumulate/return warnings, and call discovery with request Context.
Web API client — response & signatures
web/src/lib/api.ts
createTorznabIndexer and updateTorznabIndexer now return IndexerResponse instead of TorznabIndexer; discoverJackettIndexers returns DiscoverJackettResponse. Exports/types updated accordingly.
Web UI — autodiscovery & dialogs
web/src/components/indexers/AutodiscoveryDialog.tsx, web/src/components/indexers/IndexerDialog.tsx
Autodiscovery uses response.indexers, surfaces response.warnings via toast, propagates discovered categories into create/update payloads, aggregates and displays import warnings/errors, and added small input accessibility attributes.
Web UI — cross-seed link UI
web/src/components/torrents/CrossSeedDialog.tsx
Added ExternalLink icon and adjusted link markup/classes for linked titles.
Web types — indexer schemas
web/src/types/index.ts
Added IndexerResponse (extends TorznabIndexer with warnings?: string[]) and added optional categories?: TorznabIndexerCategory[] to TorznabIndexerFormData, TorznabIndexerUpdate, and JackettIndexer; added warnings?: string[] to DiscoverJackettResponse.
Manifest / package metadata
package.json
Dependency/manifest metadata updated.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Areas to inspect closely:
    • Goroutine coordination, channels, cancellation, retries and backoff in fetchCapsParallel / fetchCapsWithRetry.
    • Context propagation and timeouts replacing previous hard-coded timeouts.
    • Correct mapping/application of fetched caps and categories onto indexer objects.
    • API handler changes for persisting/reloading caps/categories and warning accumulation.
    • Frontend type/signature updates and UI handling of warnings and categories.

Possibly related PRs

Suggested labels

backend, web

Suggested reviewers

  • Audionut

Poem

🐰 I hopped through pipes of parallel flight,

Fetching caps and categories by moonlight,
Retries hummed softly, contexts held tight,
Warnings gathered gently, then shown in sight,
A rabbit's small cheer — discovery takes flight.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% 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: improving Jackett indexer capability discovery by implementing parallel fetching with retry logic, which is the core enhancement across all modified files.
✨ 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/parallel-caps-discovery

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 26bc510 and 5fa19f1.

📒 Files selected for processing (6)
  • internal/api/handlers/jackett.go (7 hunks)
  • internal/services/jackett/client.go (5 hunks)
  • web/src/components/indexers/AutodiscoveryDialog.tsx (7 hunks)
  • web/src/components/torrents/CrossSeedDialog.tsx (2 hunks)
  • web/src/lib/api.ts (4 hunks)
  • web/src/types/index.ts (5 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 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/jackett.go
  • internal/services/jackett/client.go
📚 Learning: 2025-11-06T11:59:21.390Z
Learnt from: Audionut
Repo: autobrr/qui PR: 553
File: web/src/components/torrents/TorrentTableOptimized.tsx:1510-1515
Timestamp: 2025-11-06T11:59:21.390Z
Learning: In the qui project, the API layer in web/src/lib/api.ts normalizes backend snake_case responses to camelCase for frontend consumption. For CrossSeed search results, the backend's download_url field is transformed to downloadUrl in the searchCrossSeedTorrent method, so frontend code should always use the camelCase variant (result.downloadUrl).

Applied to files:

  • web/src/components/torrents/CrossSeedDialog.tsx
  • web/src/types/index.ts
📚 Learning: 2025-12-03T18:11:08.682Z
Learnt from: finevan
Repo: autobrr/qui PR: 677
File: web/src/components/torrents/AddTorrentDialog.tsx:496-504
Timestamp: 2025-12-03T18:11:08.682Z
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/CrossSeedDialog.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/services/jackett/client.go
🧬 Code graph analysis (4)
internal/api/handlers/jackett.go (2)
internal/models/torznab_indexer.go (2)
  • TorznabIndexer (64-81)
  • TorznabIndexerCategory (102-107)
internal/services/jackett/client.go (1)
  • DiscoverJackettIndexers (480-564)
web/src/lib/api.ts (2)
web/src/types/index.ts (3)
  • TorznabIndexerFormData (1101-1112)
  • IndexerResponse (987-989)
  • DiscoverJackettResponse (1220-1223)
internal/api/handlers/jackett.go (1)
  • IndexerResponse (31-34)
web/src/types/index.ts (2)
internal/api/handlers/jackett.go (1)
  • IndexerResponse (31-34)
internal/models/torznab_indexer.go (2)
  • TorznabIndexer (64-81)
  • TorznabIndexerCategory (102-107)
internal/services/jackett/client.go (3)
internal/models/torznab_indexer.go (2)
  • TorznabBackend (29-29)
  • TorznabIndexerCategory (102-107)
web/src/types/index.ts (2)
  • TorznabIndexerCategory (991-996)
  • JackettIndexer (1204-1213)
pkg/gojackett/jackett.go (2)
  • NewClient (47-76)
  • Config (26-45)
⏰ 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 (16)
web/src/components/torrents/CrossSeedDialog.tsx (1)

383-388: LGTM! Clean UI enhancement for external links.

The addition of the ExternalLink icon with inline-flex layout provides a clear visual indicator that the link opens externally. The truncate-enabled span ensures long titles don't break the layout.

web/src/types/index.ts (2)

986-989: LGTM! Type definition aligns with backend API.

The IndexerResponse correctly extends TorznabIndexer with an optional warnings array, matching the backend struct at internal/api/handlers/jackett.go:30-34.


1111-1111: LGTM! Categories and warnings integration is complete.

The type definitions correctly add:

  • Categories to indexer creation/update payloads (TorznabIndexerFormData, TorznabIndexerUpdate)
  • Categories to the JackettIndexer discovery response
  • Warnings to DiscoverJackettResponse for partial-failure signaling

All fields are appropriately optional.

Also applies to: 1124-1124, 1212-1212, 1222-1222

internal/api/handlers/jackett.go (4)

30-34: LGTM! Clean wrapper for partial-failure responses.

The IndexerResponse embeds TorznabIndexer and adds optional Warnings, enabling the API to signal when capabilities or categories storage partially failed while still returning the created/updated indexer.


414-460: LGTM! Robust handling of capabilities and categories with graceful degradation.

The create flow now:

  1. Accepts capabilities/categories in the request payload
  2. Persists them directly if provided, accumulating warnings on partial failure
  3. Falls back to service sync if not provided
  4. Returns IndexerResponse with accumulated warnings

This allows autodiscovery to include pre-fetched caps/categories while still supporting manual creation with service sync.


587-642: LGTM! Update flow mirrors create with consistent warning accumulation.

The update logic correctly:

  • Stores provided capabilities/categories
  • Logs failures as warnings instead of failing the operation
  • Reloads the indexer to include persisted data
  • Falls back to service sync if caps/categories not provided
  • Invalidates search cache after update

859-866: LGTM! Discovery now respects request context for cancellation.

Passing r.Context() to DiscoverJackettIndexers enables proper cancellation of in-flight capability fetches if the client disconnects or the request times out.

web/src/lib/api.ts (1)

1510-1514: LGTM! API client signatures align with backend responses.

The method signatures correctly reflect the backend API changes:

  • createTorznabIndexer and updateTorznabIndexer now return IndexerResponse (with potential warnings)
  • discoverJackettIndexers returns DiscoverJackettResponse (with indexers array and warnings)

Frontend consumers can now surface partial-failure warnings to users.

Also applies to: 1517-1521, 1551-1555

web/src/components/indexers/AutodiscoveryDialog.tsx (4)

63-85: LGTM! Discovery warnings are surfaced appropriately.

The flow now:

  1. Reads indexers from response.indexers
  2. Displays discovery warnings via toast.warning
  3. Shows existing indexer count in success message

This ensures users are informed when some indexers fail capability fetch during discovery.


140-163: LGTM! Import payloads include categories with warning collection.

The import flow correctly:

  • Includes discovered capabilities and categories in create/update payloads
  • Collects warnings from IndexerResponse
  • Accumulates per-indexer warning details for later display

179-203: LGTM! User-friendly warning and error reporting.

The success/failure messaging:

  • Shows first 3 warnings/errors with details
  • Includes a summary line for additional warnings/errors beyond the first 3
  • Distinguishes between full success and success-with-warnings

This prevents overwhelming users while still providing visibility into partial failures.


258-259: LGTM! Privacy and UX improvement.

Adding autoComplete="off" and data-1p-ignore prevents password managers from incorrectly treating the indexer URL and API key fields as login credentials.

Also applies to: 279-280

internal/services/jackett/client.go (4)

471-475: LGTM! DiscoveryResult enables partial-failure signaling.

The wrapper allows discovery to succeed while still reporting which indexers failed capability fetch, aligning with the broader PR goal of graceful degradation during parallel discovery.


480-564: LGTM! Context-aware discovery with parallel capability fetching.

The refactored discovery flow:

  1. Accepts context for cancellation support
  2. Discovers indexers via Jackett or Prowlarr
  3. Fetches capabilities/categories in parallel via fetchCapsParallel
  4. Applies results to indexers
  5. Returns DiscoveryResult with warnings for failed fetches

This enables the API handler to respect request cancellation and surface partial failures to users.


626-708: LGTM! Robust concurrent capability fetching with proper resource management.

The implementation correctly:

  • Limits concurrency with a semaphore (maxConcurrent=10) to avoid overwhelming the server
  • Checks context cancellation before acquiring semaphore and at goroutine start
  • Uses WaitGroup for synchronization
  • Collects results via buffered channel
  • Tracks failed indexers for warning propagation
  • Releases semaphore via defer to prevent leaks

The concurrency pattern is production-ready.


712-747: LGTM! Retry logic with exponential backoff and context cancellation.

The retry implementation:

  • Uses exponential backoff (500ms, 1s, 2s...)
  • Checks context cancellation between retries
  • Creates per-attempt timeout contexts
  • Calls cancel() immediately after FetchCaps (not deferred) to promptly release resources in the loop
  • Returns on first success or final error after all retries

The immediate cancel() call (line 728) is correct for loop contexts, as defer would accumulate calls until function exit.


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 (1)
internal/services/jackett/client.go (1)

674-699: Consider accepting a parent context for cancellation propagation.

The function uses context.Background() internally, so retries will continue even if the caller's context is cancelled. For discovery operations this is acceptable, but if this function is reused elsewhere, consider accepting a parent context to enable early termination:

-func fetchCapabilitiesWithRetry(baseURL, apiKey string, backend models.TorznabBackend, indexerID string, maxRetries int, retryDelay, timeout time.Duration) ([]string, error) {
+func fetchCapabilitiesWithRetry(ctx context.Context, baseURL, apiKey string, backend models.TorznabBackend, indexerID string, maxRetries int, retryDelay, timeout time.Duration) ([]string, error) {
     client := NewClient(baseURL, apiKey, backend, int(timeout.Seconds()))
 
     var lastErr error
     for attempt := 0; attempt <= maxRetries; attempt++ {
+        // Check for cancellation before retrying
+        select {
+        case <-ctx.Done():
+            return nil, ctx.Err()
+        default:
+        }
+
         if attempt > 0 {
             // Exponential backoff: 500ms, 1s, 2s...
             time.Sleep(retryDelay * time.Duration(1<<(attempt-1)))
         }
 
-        ctx, cancel := context.WithTimeout(context.Background(), timeout)
+        attemptCtx, cancel := context.WithTimeout(ctx, timeout)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8671971 and 7af946d.

📒 Files selected for processing (1)
  • internal/services/jackett/client.go (5 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/services/jackett/client.go
🧬 Code graph analysis (1)
internal/services/jackett/client.go (3)
web/src/types/index.ts (1)
  • JackettIndexer (1197-1205)
internal/models/torznab_indexer.go (3)
  • TorznabBackendProwlarr (35-35)
  • TorznabBackendJackett (33-33)
  • TorznabBackend (29-29)
pkg/prowlarr/client.go (2)
  • Indexer (76-84)
  • NewClient (46-73)
⏰ 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 (4)
internal/services/jackett/client.go (4)

493-540: LGTM!

The two-pass approach (build list → fetch caps in parallel → apply results) is clean and correctly modifies slice elements using index-based iteration.


559-592: LGTM!

Good optimization to only fetch capabilities for configured indexers, keeping the parallel pattern consistent with the Prowlarr path.


601-672: LGTM!

The semaphore-based concurrency limiting with WaitGroup + channel close pattern is idiomatic Go. Results map is safely written only from the main goroutine after channel receives, avoiding races.

The approach of spawning all goroutines upfront (blocking on semaphore) is fine for typical indexer counts. For extreme scales, a worker pool would be more memory-efficient, but that's not a concern here.


701-711: LGTM!

The method correctly delegates to the underlying jackett client which was initialized with a timeout, so requests won't hang indefinitely.

@s0up4200 s0up4200 force-pushed the fix/parallel-caps-discovery branch from 7af946d to cb32cbd Compare December 5, 2025 22:05
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
web/src/components/indexers/AutodiscoveryDialog.tsx (1)

120-147: Categories sent on update will currently be dropped by the API

Passing categories: indexer.categories in both updateData and createData is the right shape for the new feature, but note that the UpdateIndexer handler (internal/api/handlers/jackett.go) doesn’t currently define or persist a Categories field—only Capabilities. With capabilities present, the handler also skips SyncIndexerCaps, so existing indexers imported here won’t get categories persisted at all. Once the backend is updated to accept and store categories on update (mirroring the create path), this UI code will behave as intended.

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

497-589: UpdateIndexer ignores categories and can never persist them for existing indexers

UpdateIndexer’s request struct and post‑update logic only deal with Capabilities; there’s no Categories field and no call to SetCategories. With the new Autodiscovery flow, updates send both capabilities and categories:

  • JSON categories is silently ignored by encoding/json because the struct has no matching field.
  • Because Capabilities is non‑empty, the handler stores only caps and then skips SyncIndexerCaps, so categories are neither stored from the payload nor refreshed via the service.

Result: existing indexers imported via Autodiscovery never get categories persisted, defeating the purpose of the new category plumbing for updates.

You can fix this by mirroring the create logic in UpdateIndexer:

@@ func (h *JackettHandler) UpdateIndexer(w http.ResponseWriter, r *http.Request) {
-	var req struct {
-		Name           string   `json:"name"`
-		BaseURL        string   `json:"base_url"`
-		IndexerID      *string  `json:"indexer_id"`
-		APIKey         string   `json:"api_key"`
-		Backend        *string  `json:"backend"`
-		Enabled        *bool    `json:"enabled"`
-		Priority       *int     `json:"priority"`
-		TimeoutSeconds *int     `json:"timeout_seconds"`
-		Capabilities   []string `json:"capabilities,omitempty"`
-	}
+	var req struct {
+		Name           string                          `json:"name"`
+		BaseURL        string                          `json:"base_url"`
+		IndexerID      *string                         `json:"indexer_id"`
+		APIKey         string                          `json:"api_key"`
+		Backend        *string                         `json:"backend"`
+		Enabled        *bool                           `json:"enabled"`
+		Priority       *int                            `json:"priority"`
+		TimeoutSeconds *int                            `json:"timeout_seconds"`
+		Capabilities   []string                        `json:"capabilities,omitempty"`
+		Categories     []models.TorznabIndexerCategory `json:"categories,omitempty"`
+	}
@@ func (h *JackettHandler) UpdateIndexer(w http.ResponseWriter, r *http.Request) {
-	// If capabilities were provided in the request, store them directly
-	if len(req.Capabilities) > 0 {
-		if err := h.indexerStore.SetCapabilities(r.Context(), indexer.ID, req.Capabilities); err != nil {
-			log.Warn().
-				Err(err).
-				Int("indexer_id", indexer.ID).
-				Str("indexer", indexer.Name).
-				Msg("Failed to store provided capabilities")
-		}
-		// Reload indexer to include the stored capabilities
-		if updated, err := h.indexerStore.Get(r.Context(), indexer.ID); err == nil {
-			indexer = updated
-		}
-	} else if h.service != nil {
-		// No capabilities provided, try to fetch them from the service
-		if updated, err := h.service.SyncIndexerCaps(r.Context(), indexer.ID); err != nil {
-			log.Warn().
-				Err(err).
-				Int("indexer_id", indexer.ID).
-				Str("indexer", indexer.Name).
-				Msg("Failed to sync torznab caps after update")
-		} else if updated != nil {
-			indexer = updated
-		}
-	}
+	// If capabilities or categories were provided in the request, store them directly
+	if len(req.Capabilities) > 0 || len(req.Categories) > 0 {
+		if len(req.Capabilities) > 0 {
+			if err := h.indexerStore.SetCapabilities(r.Context(), indexer.ID, req.Capabilities); err != nil {
+				log.Warn().
+					Err(err).
+					Int("indexer_id", indexer.ID).
+					Str("indexer", indexer.Name).
+					Msg("Failed to store provided capabilities")
+			}
+		}
+		if len(req.Categories) > 0 {
+			if err := h.indexerStore.SetCategories(r.Context(), indexer.ID, req.Categories); err != nil {
+				log.Warn().
+					Err(err).
+					Int("indexer_id", indexer.ID).
+					Str("indexer", indexer.Name).
+					Msg("Failed to store provided categories")
+			}
+		}
+		// Reload indexer to include the stored capabilities and categories
+		if updated, err := h.indexerStore.Get(r.Context(), indexer.ID); err == nil {
+			indexer = updated
+		}
+	} else if h.service != nil {
+		// No capabilities/categories provided, try to fetch them from the service
+		if updated, err := h.service.SyncIndexerCaps(r.Context(), indexer.ID); err != nil {
+			log.Warn().
+				Err(err).
+				Int("indexer_id", indexer.ID).
+				Str("indexer", indexer.Name).
+				Msg("Failed to sync torznab caps after update")
+		} else if updated != nil {
+			indexer = updated
+		}
+	}

This makes the update behaviour consistent with create and ensures Autodiscovery updates can persist both caps and categories.

🧹 Nitpick comments (1)
web/src/types/index.ts (1)

1096-1120: Form/update types now carry full category objects; confirm backend ignores client indexer_id

Using TorznabIndexerCategory for both TorznabIndexerFormData.categories and TorznabIndexerUpdate.categories is consistent with the backend model, but it means create/update payloads can carry an indexer_id that doesn’t match the actual DB id. As long as SetCategories uses the path/indexer argument as the source of truth and overwrites/ignores indexer_id from the slice, this is fine; otherwise, it may be safer long‑term to introduce a narrower input type without indexer_id.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7af946d and cb32cbd.

📒 Files selected for processing (4)
  • internal/api/handlers/jackett.go (2 hunks)
  • internal/services/jackett/client.go (6 hunks)
  • web/src/components/indexers/AutodiscoveryDialog.tsx (2 hunks)
  • web/src/types/index.ts (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/services/jackett/client.go
🧰 Additional context used
🧠 Learnings (1)
📓 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.
🧬 Code graph analysis (2)
web/src/types/index.ts (1)
internal/models/torznab_indexer.go (1)
  • TorznabIndexerCategory (102-107)
internal/api/handlers/jackett.go (2)
web/src/types/index.ts (1)
  • TorznabIndexerCategory (986-991)
internal/models/torznab_indexer.go (1)
  • TorznabIndexerCategory (102-107)
⏰ 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/types/index.ts (1)

1199-1208: Exposing categories on JackettIndexer matches new discovery flow

Adding categories?: TorznabIndexerCategory[] to JackettIndexer aligns the discovery response with the new create/update payloads and lets the UI forward Jackett‑derived categories end‑to‑end. No issues from a typing perspective.

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

327-338: CreateIndexer request payload extension for capabilities/categories looks consistent

The extended request struct (adding Capabilities []string and Categories []models.TorznabIndexerCategory) matches the JSON contract used by the UI types and keeps create‑time validation logic intact. Field names and types line up with TorznabIndexerCategory and the existing caps model.


405-440: Create path correctly prefers provided caps/categories over service sync

The new post‑create branch cleanly handles three cases: (1) persist provided capabilities, (2) persist provided categories, and (3) when neither is provided and service is non‑nil, fall back to SyncIndexerCaps. This avoids an unnecessary network round‑trip when the client already has fresh data (e.g., via discovery) while preserving the old behaviour when nothing is supplied. Logging failures as warnings instead of failing the whole create is consistent with the prior caps logic.

…scovery

- Change log level from Debug to Warn for individual indexer capability
  fetch failures so they're visible in production logs
- Propagate request context through discovery functions to enable proper
  cancellation when HTTP request is aborted
- Use select-based backoff delays to allow interruption during retries
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 (4)
internal/api/handlers/jackett.go (2)

327-338: Guard against client-supplied IndexerID in Categories.

The CreateIndexer payload reuses models.TorznabIndexerCategory, which includes an IndexerID field, while you also pass indexer.ID separately into SetCategories. To avoid ever trusting client-supplied relationships (and to prevent leaking any remote Jackett/Prowlarr IDs into your local schema), make sure SetCategories overwrites or ignores category.IndexerID and uses the indexerID argument as the single source of truth. If that’s already the case, a future cleanup could be to introduce a slimmer API DTO without IndexerID to decouple wire format from storage model.


405-440: Capabilities/categories persistence is best‑effort; consider whether silent partial failure is acceptable.

When either SetCapabilities or SetCategories fails, you only log a warning and still return 201 with whatever the store currently has. That keeps creation robust, but the client won’t know caps/categories were dropped even though it explicitly sent them. If caps/categories are important for UX (e.g. cross‑seed behaviour), consider failing the request when explicit values are provided but persistence fails, or at least returning some flag/message indicating partial success.

internal/services/jackett/client.go (2)

471-545: Context-aware discovery and shared caps pipeline look solid; consider a small ctx guard.

The split between DiscoverJackettIndexers and discoverJackettIndexers is clean, and feeding both Jackett and Prowlarr discovery through the same parallel caps application keeps behaviour consistent for Caps/Categories. Using the incoming ctx for both GetIndexers and the subsequent caps fetch is exactly what you want for cancellation.

One minor defensive improvement: if any non-HTTP callers ever pass a nil context, both discovery flows will pass that nil into downstream calls. To avoid surprises, you could normalize at the top:

func DiscoverJackettIndexers(ctx context.Context, baseURL, apiKey string) ([]JackettIndexer, error) {
-	if baseURL = strings.TrimSpace(baseURL); baseURL == "" {
+	if ctx == nil {
+		ctx = context.Background()
+	}
+	if baseURL = strings.TrimSpace(baseURL); baseURL == "" {
 		return nil, fmt.Errorf("base url is required")
 	}

(and similarly inside discoverJackettIndexers if you want extra belt-and-suspenders).

Also applies to: 547-594


14-18: Parallel caps fetcher and retry logic look correct; you might want to reuse the client per backend.

The concurrency control and cancellation behaviour in fetchCapsParallel/fetchCapsWithRetry look good:

  • Bounded parallelism via the sem channel.
  • Per-attempt timeouts layered on top of the parent ctx.
  • Proper WaitGroup + resultsChan closing, with all map writes confined to the collector goroutine, so no races.
  • Partial failures are logged and don’t abort discovery, matching the comment.

The only tweak I’d consider is avoiding NewClient per indexer in fetchCapsWithRetry. For many indexers this will spin up multiple http.Client/transport instances against the same baseURL. Reusing a single Client per (baseURL, backend) within fetchCapsParallel (or passing one in) would preserve connection pooling and reduce allocations without changing behaviour.

Also applies to: 596-683, 685-720

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb32cbd and a7e1f1d.

📒 Files selected for processing (2)
  • internal/api/handlers/jackett.go (3 hunks)
  • internal/services/jackett/client.go (5 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/jackett.go
  • internal/services/jackett/client.go
🧬 Code graph analysis (1)
internal/api/handlers/jackett.go (3)
web/src/types/index.ts (1)
  • TorznabIndexerCategory (986-991)
internal/models/torznab_indexer.go (1)
  • TorznabIndexerCategory (102-107)
internal/services/jackett/client.go (1)
  • DiscoverJackettIndexers (473-545)
⏰ 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 (2)
internal/api/handlers/jackett.go (1)

817-817: Good: discovery now respects HTTP request cancellation.

Wiring r.Context() into jackett.DiscoverJackettIndexers ensures the parallel caps/category fetches get cancelled promptly when the HTTP client disconnects or the server times out, instead of running to completion in the background.

internal/services/jackett/client.go (1)

461-468: JackettIndexer struct extension for categories looks consistent.

Adding Categories []models.TorznabIndexerCategory alongside Caps []string cleanly exposes both capabilities and category metadata to callers, and the JSON shape aligns with the shared TorznabIndexerCategory model/frontend type.

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

♻️ Duplicate comments (1)
internal/api/handlers/jackett.go (1)

566-601: Duplicate comment - see lines 405-440.

This is the same duplicated storage logic referenced in the earlier comment.

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

405-440: Consider consolidating duplicate capability/category storage logic into a helper function.

The logic for storing capabilities and categories (lines 405-440 and 566-601) is duplicated between CreateIndexer and UpdateIndexer. This pattern of setting capabilities, logging warnings on failure, setting categories, logging warnings on failure, and then reloading the indexer is repeated verbatim.

Consider extracting this into a helper method to reduce duplication:

// storeCapsAndCategories stores capabilities and categories if provided, logs warnings on failures,
// and reloads the indexer to reflect updates. Returns the updated indexer or the original on reload failure.
func (h *JackettHandler) storeCapsAndCategories(ctx context.Context, indexer *models.TorznabIndexer, capabilities []string, categories []models.TorznabIndexerCategory) *models.TorznabIndexer {
	if len(capabilities) == 0 && len(categories) == 0 {
		return indexer
	}

	if len(capabilities) > 0 {
		if err := h.indexerStore.SetCapabilities(ctx, indexer.ID, capabilities); err != nil {
			log.Warn().
				Err(err).
				Int("indexer_id", indexer.ID).
				Str("indexer", indexer.Name).
				Msg("Failed to store provided capabilities")
		}
	}
	if len(categories) > 0 {
		if err := h.indexerStore.SetCategories(ctx, indexer.ID, categories); err != nil {
			log.Warn().
				Err(err).
				Int("indexer_id", indexer.ID).
				Str("indexer", indexer.Name).
				Msg("Failed to store provided categories")
		}
	}

	// Reload indexer to include the stored capabilities and categories
	if updated, err := h.indexerStore.Get(ctx, indexer.ID); err == nil {
		return updated
	}
	return indexer
}

Then both handlers would simplify to:

if len(req.Capabilities) > 0 || len(req.Categories) > 0 {
	indexer = h.storeCapsAndCategories(r.Context(), indexer, req.Capabilities, req.Categories)
} else if h.service != nil {
	// No capabilities/categories provided, try to fetch them from the service
	// ... existing sync logic
}
internal/services/jackett/client.go (2)

614-619: Consider making concurrency and retry parameters configurable.

The hardcoded constants for maxConcurrent, maxRetries, retryDelay, and fetchTimeout may not be optimal for all environments. For example:

  • Some Jackett/Prowlarr instances may handle more than 10 concurrent requests
  • Network conditions may require different retry strategies
  • Timeout values may need adjustment for slow indexers

Consider adding these as configuration options or at minimum, document the rationale for these values:

const (
	// maxConcurrent limits parallel capability fetches to avoid overwhelming the backend.
	// This value balances discovery speed with backend resource constraints.
	maxConcurrent = 10
	
	// maxRetries is the number of retry attempts per indexer.
	// With exponential backoff, this provides ~3.5 seconds of retry time.
	maxRetries = 2
	
	// retryDelay is the initial delay between retries.
	// Exponential backoff doubles this for each subsequent attempt.
	retryDelay = 500 * time.Millisecond
	
	// fetchTimeout is the maximum time to wait for a single capability fetch attempt.
	fetchTimeout = 15 * time.Second
)

633-639: Context cancellation check may introduce unnecessary overhead.

The explicit context cancellation check before acquiring the semaphore (lines 634-639) may not be necessary since:

  1. The semaphore acquisition itself doesn't block if the context is already cancelled
  2. The fetchCapsWithRetry function already checks ctx.Err() at multiple points
  3. The goroutine will exit quickly anyway if the context is cancelled

However, this is a minor performance consideration and the current defensive approach is valid.

If you want to optimize, you could remove the explicit check:

-		// Check if parent context is already cancelled
-		select {
-		case <-ctx.Done():
-			resultsChan <- capsFetchResult{indexerID: indexerID, err: ctx.Err()}
-			return
-		default:
-		}
-
 		// Acquire semaphore
 		sem <- struct{}{}
 		defer func() { <-sem }()

The function will still handle cancellation correctly via the context passed to fetchCapsWithRetry.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a7e1f1d and 068cba9.

📒 Files selected for processing (2)
  • internal/api/handlers/jackett.go (5 hunks)
  • internal/services/jackett/client.go (5 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 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/services/jackett/client.go
  • internal/api/handlers/jackett.go
🧬 Code graph analysis (2)
internal/services/jackett/client.go (2)
internal/models/torznab_indexer.go (4)
  • TorznabBackend (29-29)
  • TorznabIndexerCategory (102-107)
  • TorznabBackendProwlarr (35-35)
  • TorznabBackendJackett (33-33)
web/src/types/index.ts (2)
  • TorznabIndexerCategory (986-991)
  • JackettIndexer (1199-1208)
internal/api/handlers/jackett.go (3)
web/src/types/index.ts (1)
  • TorznabIndexerCategory (986-991)
internal/models/torznab_indexer.go (1)
  • TorznabIndexerCategory (102-107)
internal/services/jackett/client.go (1)
  • DiscoverJackettIndexers (473-548)
⏰ 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 (8)
internal/api/handlers/jackett.go (3)

328-337: LGTM - Categories field properly added to CreateIndexer request.

The addition of the Categories field to the request struct is consistent with the existing Capabilities field and matches the expected type from the models package.


497-507: LGTM - Categories field properly added to UpdateIndexer request.

The addition of the Categories field is consistent with the CreateIndexer request struct and matches the expected type.


829-829: LGTM - Context propagation enables proper cancellation.

Passing r.Context() to DiscoverJackettIndexers allows the discovery operation to be cancelled if the HTTP request is cancelled, preventing resource waste on abandoned requests.

internal/services/jackett/client.go (5)

14-18: LGTM - Imports properly added for new functionality.

The sync package is needed for sync.WaitGroup in the parallel cap fetching, and zerolog/log is needed for the new logging statements.


461-468: LGTM - Categories field properly added to JackettIndexer.

The Categories field is correctly typed as []models.TorznabIndexerCategory and includes the omitempty JSON tag, consistent with the Caps field.


471-548: LGTM - Context support and parallel cap fetching improve reliability and performance.

The changes enhance the discovery process by:

  • Supporting context cancellation throughout the discovery flow
  • Fetching capabilities in parallel with retries for all indexers
  • Handling partial failures gracefully without failing the entire discovery

550-597: LGTM - Jackett discovery path now matches Prowlarr implementation.

The refactoring to use parallel cap fetching for Jackett indexers is consistent with the Prowlarr path and provides the same benefits.


694-702: Exponential backoff implementation is correct and well-designed.

The retry logic properly implements:

  • Exponential backoff with 1<<(attempt-1) multiplier
  • Context cancellation awareness during sleep
  • Early exit on cancellation

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)
internal/services/jackett/client.go (1)

599-686: Parallel caps fetch implementation is sound; consider toning down logging on pure cancellations

fetchCapsParallel correctly:

  • Short‑circuits on empty indexerIDs.
  • Limits concurrency via a semaphore while still fan‑out/fan‑in on a buffered results channel.
  • Uses the parent ctx for both early bailout and for the retry helper, then aggregates per‑indexer errors plus a summary.

One optional refinement: when result.err is purely due to context cancellation or deadline (e.g. the HTTP request was cancelled by the caller), you may want to log at Debug or Info rather than Warn, or skip the per‑indexer warning altogether, to avoid noisy logs on expected user cancellations.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 068cba9 and 6c31581.

📒 Files selected for processing (1)
  • internal/services/jackett/client.go (5 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 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/services/jackett/client.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/services/jackett/client.go
🧬 Code graph analysis (1)
internal/services/jackett/client.go (2)
internal/models/torznab_indexer.go (3)
  • TorznabBackend (29-29)
  • TorznabIndexerCategory (102-107)
  • TorznabBackendJackett (33-33)
web/src/types/index.ts (2)
  • TorznabIndexerCategory (986-991)
  • JackettIndexer (1199-1208)
⏰ 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 (4)
internal/services/jackett/client.go (4)

461-468: JackettIndexer shape and JSON tags look consistent with UI types

The struct layout and JSON tags for ID, Name, Description, Type, Configured, Backend, Caps, and Categories line up with the TS JackettIndexer interface and should serialize cleanly to the UI.


471-548: Context-aware discovery flow and parallel caps fetch wiring look correct

DiscoverJackettIndexers now:

  • Safely defaults ctx to context.Background() when nil.
  • Reuses the new discoverJackettIndexers helper for Jackett, keeping Prowlarr fallback behavior intact.
  • For the Prowlarr path, builds indexers and indexerIDs, then uses fetchCapsParallel and cleanly applies Caps/Categories back onto the slice.

The partial‑failure semantics (log + continue) are reasonable for discovery and won’t break callers that only need the base indexer list even when some caps fail.


550-597: Jackett discovery helper correctly scopes caps fetch to configured indexers

discoverJackettIndexers:

  • Uses GetIndexersCtx(ctx) so discovery is cancelable.
  • Builds JackettIndexer values for all indexers while restricting caps fetch to configured ones via configuredIDs.
  • Applies Caps and Categories only when an entry exists in capsMap, which degrades gracefully when individual fetches fail.

This separation of indexer enumeration from caps fetching keeps the flow clear and avoids unnecessary work for unconfigured indexers.


688-723: Retry helper behavior and context usage look correct

fetchCapsWithRetry:

  • Constructs a backend‑appropriate Client once and reuses it across attempts.
  • Implements bounded retries with exponential backoff that honor the parent ctx (bailing early if cancelled).
  • Wraps each attempt in context.WithTimeout to cap individual request duration and defers cancel() immediately after creation, which avoids leaks even if FetchCaps panics or returns early, with at most maxRetries+1 outstanding defers.

Overall the retry logic and context handling are correct and safe for the small fixed retry count used here.

… caps fetch

- Remove defer cancel() from retry loop to prevent context accumulation
- Add select-based semaphore acquisition with context cancellation support
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

♻️ Duplicate comments (1)
internal/services/jackett/client.go (1)

693-728: Consider deferring cancel() immediately after context.WithTimeout in the retry loop.

Within fetchCapsWithRetry, attemptCtx, cancel := context.WithTimeout(ctx, timeout) followed by an explicit cancel() after FetchCaps works in the normal path, but if client.FetchCaps were ever to panic, the cancel would not run and the timer resources would live until timeout. A more idiomatic pattern is:

attemptCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()

caps, err := client.FetchCaps(attemptCtx, indexerID)

Given the small, bounded number of retries, the cost of a few defers is negligible and ensures cleanup even on panics or early returns.

🧹 Nitpick comments (2)
internal/services/jackett/client.go (2)

550-597: Jackett discovery + parallel caps fetch for configured indexers is well-structured.

Using GetIndexersCtx(ctx) keeps discovery cancellable, and only configured Jackett indexers are queued for caps fetch, which is efficient. Applying capsMap back onto the full indexers slice keeps the API backward compatible while enriching the data. If you expect discoverJackettIndexers to ever be called directly with a nil ctx, consider normalizing ctx at the top of this function as well, mirroring DiscoverJackettIndexers (optional).


599-691: Parallel caps fetching pattern looks safe; a couple of small robustness nits (optional).

The semaphore + WaitGroup + channel fan-in pattern is sound, and all goroutines either short-circuit on ctx.Done() or send exactly one capsFetchResult, so there’s no leak. Minor polish you could consider:

  • Preallocate results with len(indexerIDs) to avoid map growth.
  • At the top of fetchCapsParallel, normalize a nil ctx to context.Background() so this helper is robust even if used outside the current call sites.
    Also, you might want to avoid logging at Warn level for expected context.Canceled cases to reduce noise in busy systems.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c31581 and 9d8cfeb.

📒 Files selected for processing (1)
  • internal/services/jackett/client.go (5 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 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/services/jackett/client.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/services/jackett/client.go
🧬 Code graph analysis (1)
internal/services/jackett/client.go (4)
internal/models/torznab_indexer.go (4)
  • TorznabBackend (29-29)
  • TorznabIndexerCategory (102-107)
  • TorznabBackendProwlarr (35-35)
  • TorznabBackendJackett (33-33)
web/src/types/index.ts (2)
  • TorznabIndexerCategory (986-991)
  • JackettIndexer (1199-1208)
pkg/prowlarr/client.go (3)
  • NewClient (46-73)
  • Config (21-28)
  • Indexer (76-84)
pkg/gojackett/jackett.go (2)
  • NewClient (47-76)
  • Config (26-45)
⏰ 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 (2)
internal/services/jackett/client.go (2)

459-468: JackettIndexer extension (Backend, Caps, Categories) is consistent with models and UI contracts.

Struct fields and JSON tags align with models.TorznabBackend and TorznabIndexerCategory plus the frontend JackettIndexer type; this should round-trip cleanly across API and UI.


471-548: Context-aware discovery and Prowlarr caps enrichment flow look correct.

Nil context is normalized to context.Background, Jackett is tried first, then Prowlarr as a fallback; disabled/non-torrent indexers are skipped, and Prowlarr IDs are consistently stringified for both discovery and caps fetching. The parallel caps enrichment for Prowlarr indexers via fetchCapsParallel is wired in cleanly.

…/update

Add IndexerResponse type with optional warnings field to surface partial
failures (e.g., capabilities/categories storage errors) to the frontend.
Previously these errors were only logged server-side.

Frontend now displays warning toasts when the API returns warnings,
informing users to sync caps manually if needed.
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/jackett.go (2)

30-34: IndexerResponse + create flow behave correctly; consider surfacing sync failures as warnings and updating docs

The new IndexerResponse wrapper and create flow correctly:

  • persist provided capabilities/categories when present, appending warnings on partial failures, and
  • fall back to SyncIndexerCaps when nothing is provided.

Two non-blocking improvements to consider:

  • When SyncIndexerCaps fails (in the else if h.service != nil branch), you currently only log; adding a warning like "Failed to sync capabilities/categories - sync manually later" would give clients consistent feedback for partial failures.
  • The swagger @Success annotation for CreateIndexer still references models.TorznabIndexer, but the handler now returns IndexerResponse, which may confuse generated docs/clients.

Also applies to: 333-454


508-629: Update flow mirrors create correctly; same note on sync warnings and swagger type

The update handler’s capability/category handling matches the create logic and correctly reloads the indexer and invalidates the search cache; the warning accumulation for partial SetCapabilities/SetCategories failures is sound.

As above, you might:

  • also surface a warning when SyncIndexerCaps fails instead of only logging, and
  • update the @Success 200 swagger annotation to reference IndexerResponse instead of models.TorznabIndexer.
web/src/components/indexers/AutodiscoveryDialog.tsx (1)

96-175: Import flow correctly incorporates categories and warning-aware responses

The import loop now:

  • forwards discovered caps and categories into update/create payloads, and
  • counts responses with warnings to adjust the final toast.

This matches the new IndexerResponse contract and keeps error handling unchanged; looks good.

If you ever want more insight, you could also aggregate and log the actual warning messages per indexer instead of just counting them, but it’s not required for this change set.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9d8cfeb and 26bc510.

📒 Files selected for processing (6)
  • internal/api/handlers/jackett.go (8 hunks)
  • web/src/components/indexers/AutodiscoveryDialog.tsx (6 hunks)
  • web/src/components/indexers/IndexerDialog.tsx (2 hunks)
  • web/src/components/torrents/CrossSeedDialog.tsx (2 hunks)
  • web/src/lib/api.ts (2 hunks)
  • web/src/types/index.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/types/index.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-11-06T11:59:21.390Z
Learnt from: Audionut
Repo: autobrr/qui PR: 553
File: web/src/components/torrents/TorrentTableOptimized.tsx:1510-1515
Timestamp: 2025-11-06T11:59:21.390Z
Learning: In the qui project, the API layer in web/src/lib/api.ts normalizes backend snake_case responses to camelCase for frontend consumption. For CrossSeed search results, the backend's download_url field is transformed to downloadUrl in the searchCrossSeedTorrent method, so frontend code should always use the camelCase variant (result.downloadUrl).

Applied to files:

  • web/src/components/torrents/CrossSeedDialog.tsx
📚 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/CrossSeedDialog.tsx
📚 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/jackett.go
🧬 Code graph analysis (3)
web/src/lib/api.ts (2)
web/src/types/index.ts (2)
  • TorznabIndexerFormData (1101-1112)
  • IndexerResponse (987-989)
internal/api/handlers/jackett.go (1)
  • IndexerResponse (31-34)
web/src/components/indexers/IndexerDialog.tsx (1)
web/src/lib/api.ts (1)
  • api (1655-1655)
web/src/components/indexers/AutodiscoveryDialog.tsx (1)
web/src/lib/api.ts (1)
  • api (1655-1655)
⏰ 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 (6)
web/src/components/torrents/CrossSeedDialog.tsx (2)

39-39: LGTM! Clean import addition.

The ExternalLink icon is properly imported from lucide-react to support the visual enhancement below.


375-380: Excellent UX improvement with proper implementation.

The external link icon addition enhances user experience by providing a clear visual indicator for external links. The implementation is well-crafted:

  • inline-flex with items-center ensures proper vertical alignment
  • gap-1 provides balanced spacing between text and icon
  • shrink-0 on the icon prevents compression in constrained layouts
  • opacity-50 keeps the icon subtle and non-intrusive
  • Wrapping the title in a <span> preserves text truncation while keeping the icon visible
web/src/components/indexers/IndexerDialog.tsx (1)

87-92: Indexer create/update warning handling looks solid

Using the new IndexerResponse.warnings to choose between warning and success toasts is correct and keeps the happy-path unchanged; nothing blocking here.

Also applies to: 112-117

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

845-853: Good use of r.Context() in discovery

Passing r.Context() into jackett.DiscoverJackettIndexers is a nice improvement for cancellation/timeout propagation during discovery.

web/src/components/indexers/AutodiscoveryDialog.tsx (1)

239-241: Input autofill/1Password suppression changes are safe

Disabling autocomplete and adding data-1p-ignore on the URL and API key fields is low-risk and improves control over password-manager behavior.

Also applies to: 260-262

web/src/lib/api.ts (1)

57-61: API return type switch to IndexerResponse is appropriate

Updating createTorznabIndexer/updateTorznabIndexer to return IndexerResponse aligns the client with the backend wrapper and enables warning handling without breaking JSON shape. All call sites in IndexerDialog.tsx and AutodiscoveryDialog.tsx correctly access the response.warnings property.

@s0up4200 s0up4200 added this to the v1.9.0 milestone Dec 6, 2025
@s0up4200 s0up4200 merged commit 0aaf39e into main Dec 7, 2025
11 checks passed
@s0up4200 s0up4200 deleted the fix/parallel-caps-discovery branch December 7, 2025 20:56
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.

1 participant