Skip to content

feat(torrents): warn about cross-seeded torrents in delete dialogs#670

Merged
s0up4200 merged 11 commits intomainfrom
feat/delete-dialog-cross-seed-warning
Dec 3, 2025
Merged

feat(torrents): warn about cross-seeded torrents in delete dialogs#670
s0up4200 merged 11 commits intomainfrom
feat/delete-dialog-cross-seed-warning

Conversation

@s0up4200
Copy link
Collaborator

@s0up4200 s0up4200 commented Dec 2, 2025

CleanShot 2025-12-03 at 21 02 42@2x CleanShot 2025-12-03 at 21 03 23@2x CleanShot 2025-12-03 at 21 03 33@2x CleanShot 2025-12-03 at 21 03 40@2x CleanShot 2025-12-03 at 21 03 45@2x CleanShot 2025-12-03 at 21 04 47@2x

Summary by CodeRabbit

  • New Features

    • Cross-seed detection UI added to delete flow: shows affected torrents by instance, tracker-domain badges, incognito names, “+N more” overflow, loading/error states, and an option to include/delete cross-seeds.
    • Centralized delete confirmation dialog with file-preservation control and cross-seed toggle; counts/sizes update when cross-seeds are included.
  • Refactor

    • Unified delete flow by replacing inline confirmations with the new dialog across desktop and mobile views.
  • Style

    • Minor spacing adjustment in delete-files preference.

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

@s0up4200 s0up4200 added this to the v1.9.0 milestone Dec 2, 2025
@s0up4200 s0up4200 added enhancement New feature or request web torrent labels Dec 2, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 2, 2025

Walkthrough

Adds cross-seed detection and deletion controls: a new hook to find cross-seeded torrents, a CrossSeedWarning UI, a DeleteTorrentDialog that surfaces delete and cross-seed options, integration into torrent delete flows (management, table, mobile), client state for deleteCrossSeeds, and release-modifier checks in matching logic.

Changes

Cohort / File(s) Summary
Hook: cross-seed detection
web/src/hooks/useCrossSeedWarning.ts
New useCrossSeedWarning hook: manages searchState, checkedCount, affectedTorrents; fetches instance/torrent files, calls searchCrossSeedMatches, filters out deleting hashes, scopes to instance, deduplicates, and exposes affectedTorrents, searchState, hasWarning, totalToCheck, checkedCount, search(), reset().
UI: warning component
web/src/components/torrents/CrossSeedWarning.tsx
New CrossSeedWarning React component with idle/searching/error/complete states; groups per-instance results, shows up to 8 items with “+N more” tooltip, tracker-domain badges, incognito-aware naming, destructive styling when deletion toggles active, and an optional deleteCrossSeeds checkbox.
UI: delete dialog
web/src/components/torrents/DeleteTorrentDialog.tsx
New DeleteTorrentDialog component: modal combining DeleteFilesPreference and CrossSeedWarning; props for counts/sizes, lockable delete-files option, deleteCrossSeeds toggle and callbacks, and confirm/cancel actions; computes displayCount including cross-seed items when enabled.
Integration: management, table, mobile
web/src/components/torrents/TorrentManagementBar.tsx, web/src/components/torrents/TorrentTableOptimized.tsx, web/src/components/torrents/TorrentCardsMobile.tsx
Replace inline AlertDialog with DeleteTorrentDialog; integrate useCrossSeedWarning (using normalized instance id) into delete flows; pass crossSeedWarning, deleteCrossSeeds, and setter into dialog; augment delete logic to include cross-seed hashes when enabled; expose deleteCrossSeeds/setDeleteCrossSeeds on TorrentTableOptimized; reset cross-seed state on open/close.
State: torrent actions
web/src/hooks/useTorrentActions.ts
Add client state deleteCrossSeeds and setter setDeleteCrossSeeds; add closeDeleteDialog; reset deleteCrossSeeds on dialog open/close and after delete completion to avoid stale state.
Lib: cross-seed matching
web/src/lib/cross-seed-utils.ts
Add extractReleaseModifiers(name) and hasMatchingReleaseModifiers(name1, name2); require matching release modifiers before fuzzy filename matching in cross-seed detection logic.
Small UI tweak
web/src/components/torrents/DeleteFilesPreference.tsx
Remove vertical padding (py-4) from container; no API/logic changes.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as Torrent UI
    participant Hook as useCrossSeedWarning
    participant API as Server/API
    participant Dialog as DeleteTorrentDialog
    participant Component as CrossSeedWarning

    User->>UI: select torrents, open Delete dialog
    UI->>Hook: init(search with instanceId, instanceName, torrents)
    Hook->>API: fetch instances & torrent files, call searchCrossSeedMatches
    API-->>Hook: return matches
    Hook->>Hook: filter/de-dupe/scope results → affectedTorrents
    Hook-->>UI: searchState, affectedTorrents, hasWarning
    UI->>Dialog: render with crossSeedWarning & deleteCrossSeeds state
    Dialog->>Component: render CrossSeedWarning details and checkbox
    User->>Dialog: toggle deleteCrossSeeds
    User->>Dialog: confirm Delete
    Dialog->>API: send delete request (include cross-seed hashes if enabled)
    API-->>UI: delete result
    UI->>Hook: reset / close (reset search state / deleteCrossSeeds)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Inspect async flow and error handling in web/src/hooks/useCrossSeedWarning.ts.
  • Verify release-modifier extraction/comparison and its integration in web/src/lib/cross-seed-utils.ts.
  • Confirm delete augmentation (merging cross-seed hashes) and metadata updates in TorrentTableOptimized, TorrentManagementBar, and TorrentCardsMobile.
  • Review DeleteTorrentDialog prop wiring and useTorrentActions state resets to avoid stale deleteCrossSeeds.
  • Check UI grouped lists (8-item limit, "+N more" tooltip), tracker badges, and incognito name behavior.

Possibly related PRs

Suggested labels

cross-seed

Poem

🐇
I hop through hashes, sniff and cheer,
I find the twins that linger near.
A checkbox twitch, a gentle nudge,
Keep or drop — I’ll help the judge.
Carrot clap — safe torrents clear!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% 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 PR title accurately and concisely summarizes the main change: adding cross-seed warnings to delete dialogs.
✨ 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 feat/delete-dialog-cross-seed-warning

📜 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 726027c and e6e0239.

📒 Files selected for processing (1)
  • web/src/components/torrents/CrossSeedWarning.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
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.
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go:209-212
Timestamp: 2025-11-28T20:32:30.126Z
Learning: Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go
Learning: The cross-seed recheck-resume worker intentionally runs for the process lifetime and keys pending entries by hash only. This is acceptable under the current constraint that background seeded-search runs operate on a single instance at a time; graceful shutdown and instanceID|hash keying are deferred by design.
📚 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/CrossSeedWarning.tsx
⏰ 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)
web/src/components/torrents/CrossSeedWarning.tsx (5)

30-41: LGTM!

The helper function handles URL parsing defensively with a sensible regex fallback for malformed tracker URLs. The JSDoc comment clearly documents the expected transformation.


58-106: LGTM!

The idle state appropriately differentiates high-risk scenarios (deleting files) with more prominent styling and clear call-to-action. The type="button" on the Button prevents unintended form submissions.


153-175: LGTM!

The instance grouping and tracker aggregation logic is well-implemented. The fallback to Instance ${torrent.instanceId} handles cases where instanceName might be missing.


264-283: LGTM!

The torrent list rendering correctly uses unique composite keys (${torrent.hash}-${torrent.instanceId}) and handles incognito mode appropriately. The tracker domain badge provides useful context.


291-300: Previous issue addressed.

The tooltip overflow list now uses ${t.hash}-${t.instanceId} as the key (Line 294), consistent with the main list and avoiding potential React key collisions for cross-seeded torrents.


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: 1

Caution

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

⚠️ Outside diff range comments (1)
web/src/components/torrents/TorrentManagementBar.tsx (1)

96-98: Critical: Hooks called after early return violates Rules of Hooks.

The early return at lines 96-98 causes useInstances (line 118), the related useMemo (line 119), and useCrossSeedWarning (lines 174-179) to be called conditionally. React requires hooks to be called unconditionally and in the same order on every render.

Move all hooks before the early return guard, or restructure to avoid conditional hook calls:

 export const TorrentManagementBar = memo(function TorrentManagementBar({
   instanceId,
   selectedHashes = [],
   selectedTorrents = [],
   isAllSelected = false,
   totalSelectionCount = 0,
   totalSelectionSize = 0,
   filters,
   search,
   excludeHashes = [],
   onComplete,
 }: TorrentManagementBarProps) {
-  if (typeof instanceId !== "number" || instanceId <= 0) {
-    return null
-  }
-
   const selectionCount = totalSelectionCount || selectedHashes.length

   // Use shared metadata hook to leverage cache from table and filter sidebar
   const { data: metadata, isLoading: isMetadataLoading } = useInstanceMetadata(instanceId)
+  
+  // Get instance name for cross-seed warning
+  const { instances } = useInstances()
+  const instance = useMemo(() => instances?.find(i => i.id === instanceId), [instances, instanceId])
   // ... other hooks ...
+
+  // Cross-seed warning for delete dialog
+  const crossSeedWarning = useCrossSeedWarning({
+    instanceId,
+    instanceName: instance?.name ?? "",
+    torrents: selectedTorrents,
+    enabled: showDeleteDialog,
+  })
+
+  // Keep this guard after hooks so their invocation order stays stable.
+  if (typeof instanceId !== "number" || instanceId <= 0 || !hasSelection) {
+    return null
+  }

Based on static analysis hints from Biome.

Also applies to: 117-120, 173-179

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0af7504 and 9eaf9e6.

📒 Files selected for processing (4)
  • web/src/components/torrents/CrossSeedWarning.tsx (1 hunks)
  • web/src/components/torrents/TorrentManagementBar.tsx (6 hunks)
  • web/src/components/torrents/TorrentTableOptimized.tsx (6 hunks)
  • web/src/hooks/useCrossSeedWarning.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
web/src/components/torrents/CrossSeedWarning.tsx (3)
web/src/lib/cross-seed-utils.ts (1)
  • CrossSeedTorrent (68-72)
web/src/lib/incognito.ts (2)
  • useIncognitoMode (321-352)
  • getLinuxIsoName (156-163)
web/src/lib/utils.ts (1)
  • cn (9-11)
web/src/hooks/useCrossSeedWarning.ts (3)
web/src/types/index.ts (1)
  • Torrent (204-261)
web/src/lib/cross-seed-utils.ts (1)
  • CrossSeedTorrent (68-72)
web/src/lib/api.ts (1)
  • api (1647-1647)
web/src/components/torrents/TorrentTableOptimized.tsx (2)
web/src/hooks/useCrossSeedWarning.ts (1)
  • useCrossSeedWarning (37-138)
web/src/components/torrents/CrossSeedWarning.tsx (1)
  • CrossSeedWarning (34-182)
🪛 Biome (2.1.2)
web/src/components/torrents/TorrentManagementBar.tsx

[error] 118-118: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 119-119: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 174-174: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

⏰ 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 (9)
web/src/components/torrents/TorrentManagementBar.tsx (1)

619-644: LGTM on the CrossSeedWarning integration in the delete dialog.

The component is correctly wired with the hook's output and placed appropriately within the dialog layout. The max-w-xl class accommodates the warning content.

web/src/components/torrents/TorrentTableOptimized.tsx (2)

835-841: LGTM on cross-seed warning hook integration.

The hook is correctly called unconditionally at the component's top level, and contextTorrents appropriately represents the torrents being prepared for deletion. The enabled: showDeleteDialog flag ensures the query only runs when the dialog is open.


2925-2950: LGTM on the delete dialog cross-seed warning UI.

The CrossSeedWarning component is properly integrated with all required props. The !max-w-2xl class override is appropriate to accommodate the warning content width.

web/src/components/torrents/CrossSeedWarning.tsx (4)

1-32: LGTM on the component setup and helper function.

The getTrackerDomain helper properly handles both valid URLs and malformed tracker strings with a regex fallback. The props interface is well-defined with all necessary fields.


34-70: LGTM on state management and grouping logic.

The component correctly handles loading, no-warning, and warning states. The grouping by instance using reduce is clean and efficient.


82-121: LGTM on the warning header UI.

The conditional styling based on deleteFiles (destructive vs. informational) provides clear visual feedback. The message composition handles singular/plural and multi-instance cases correctly.


123-181: LGTM on the expandable torrent list.

The implementation correctly limits displayed torrents to 8 per instance with a "+ N more" indicator, uses stable keys, and properly respects incognito mode. The expand/collapse UX is straightforward.

web/src/hooks/useCrossSeedWarning.ts (2)

73-94: LGTM on the query configuration.

The query is properly gated with enabled && expr.length > 0, preventing unnecessary API calls. The staleTime of 10 seconds and gcTime of 30 seconds are reasonable given that torrent data can change. The limit of 100 is appropriate for a warning display context.


96-137: LGTM on the result computation.

The logic correctly:

  • Returns early with isLoading: false when disabled or no expression
  • Reports isLoading: true while the query is pending
  • Filters out torrents being deleted using the hashesBeingDeleted Set
  • Enriches remaining torrents with instance metadata and matchType

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)
web/src/hooks/useCrossSeedWarning.ts (1)

44-71: Expression building and path escaping now correctly handle Windows paths

The memoized construction of { expr, hashesBeingDeleted } looks good: you dedupe hashes/paths via Set, early‑out on empty input, and the ContentPath conditions now escape backslashes before quotes, which fixes the Windows path escaping problem highlighted in the earlier review.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9eaf9e6 and a20190c.

📒 Files selected for processing (2)
  • web/src/components/torrents/TorrentManagementBar.tsx (8 hunks)
  • web/src/hooks/useCrossSeedWarning.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/components/torrents/TorrentManagementBar.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
web/src/hooks/useCrossSeedWarning.ts (3)
web/src/types/index.ts (1)
  • Torrent (204-261)
web/src/lib/cross-seed-utils.ts (1)
  • CrossSeedTorrent (68-72)
web/src/lib/api.ts (1)
  • api (1647-1647)
⏰ 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)
web/src/hooks/useCrossSeedWarning.ts (2)

13-27: Hook options and result shape look solid and self‑documenting

UseCrossSeedWarningOptions and CrossSeedWarningResult are well‑named, minimal, and aligned with the existing CrossSeedTorrent type. JSDoc comments clearly describe intent and should make the hook easy to adopt.


73-94: Query wiring and cache settings fit the use case

The useQuery setup (keying on ["cross-seed-warning", instanceId, expr], guarding with enabled && expr.length > 0, and using a modest limit plus short staleTime/gcTime) matches the expected “ephemeral helper for dialogs” usage. This should avoid unnecessary calls while keeping results reasonably fresh.

@calliah333
Copy link

calliah333 commented Dec 2, 2025

IMO the wording on the dialog is unclear

"Safe to remove" would be better as something along the lines of "these torrents will be unaffected upon removal"

"this will break cross-seeded torrents" is also unclear and should clarify whether the matched torrents will be removed from qbittorrent or not.

@cmd430
Copy link
Contributor

cmd430 commented Dec 2, 2025

it might be a nice feature to have a checkbox that when selected also removes the cross-seeded torrents along with the current selected torrent

@jabloink
Copy link
Contributor

jabloink commented Dec 2, 2025

does it make sense to extend this to warn users in these places:

  • set location dialog: warn users that moving that torrent data will also break cross-seeds ( potentially update the paths of the cross-seeded torrents aswell )

  • set category dialog: specifically when the torrent has autotmm enabled

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)
web/src/hooks/useCrossSeedWarning.ts (1)

72-88: Query error state not handled - perpetual loading on failure.

The query doesn't destructure isError, so when the cross-seed search fails, the condition isLoading || !matches returns isLoading: true indefinitely. Users see a perpetual loading indicator instead of graceful degradation.

This was flagged in a previous review. Destructure isError and return early with { affectedTorrents: [], isLoading: false, hasWarning: false } when the query fails.

Also applies to: 100-106

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

80-88: Consider disabling Delete button while cross-seed warning is loading.

When crossSeedWarning.isLoading is true, the user might confirm deletion before understanding which cross-seeded torrents would be affected. Consider either:

  1. Disabling the Delete button while loading
  2. Adding a loading indicator to the button
         <AlertDialogFooter>
           <AlertDialogCancel>Cancel</AlertDialogCancel>
           <AlertDialogAction
             onClick={onConfirm}
+            disabled={crossSeedWarning.isLoading}
             className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
           >
-            Delete
+            {crossSeedWarning.isLoading ? "Checking..." : "Delete"}
           </AlertDialogAction>
         </AlertDialogFooter>
web/src/components/torrents/TorrentManagementBar.tsx (1)

206-219: Potential duplicate hashes when deleting cross-seeds.

If a cross-seed torrent's hash is already in selectedHashes (e.g., user selected both the original and its cross-seed), the combined array will contain duplicates:

const hashesToDelete = deleteCrossSeeds
  ? [...selectedHashes, ...crossSeedWarning.affectedTorrents.map(t => t.hash)]
  : selectedHashes

While the backend likely handles duplicates gracefully, consider deduplicating to avoid redundant API work:

     const hashesToDelete = deleteCrossSeeds
-      ? [...selectedHashes, ...crossSeedWarning.affectedTorrents.map(t => t.hash)]
+      ? [...new Set([...selectedHashes, ...crossSeedWarning.affectedTorrents.map(t => t.hash)])]
       : selectedHashes
web/src/components/torrents/TorrentTableOptimized.tsx (1)

1969-1982: Same duplicate hash concern as TorrentManagementBar.

The handleDeleteWrapper in this file has the same pattern that could result in duplicate hashes when cross-seeded torrents are included. For consistency and to avoid redundant API calls:

     const hashesToDelete = deleteCrossSeeds
-      ? [...contextHashes, ...crossSeedWarning.affectedTorrents.map(t => t.hash)]
+      ? [...new Set([...contextHashes, ...crossSeedWarning.affectedTorrents.map(t => t.hash)])]
       : contextHashes
web/src/components/torrents/CrossSeedWarning.tsx (2)

64-86: Consider memoizing derived state for large torrent lists.

The grouping and unique tracker calculations run on every render. For dialogs with many cross-seeded torrents, this could cause minor performance overhead.

+import { useState, useMemo } from "react"
-import { useState } from "react"
-  // Group by instance
-  const byInstance = affectedTorrents.reduce<Record<string, CrossSeedTorrent[]>>(
+  // Group by instance (memoized)
+  const byInstance = useMemo(() => affectedTorrents.reduce<Record<string, CrossSeedTorrent[]>>(
     (acc, torrent) => {
       const key = torrent.instanceName || `Instance ${torrent.instanceId}`
       if (!acc[key]) {
         acc[key] = []
       }
       acc[key].push(torrent)
       return acc
     },
     {}
-  )
+  ), [affectedTorrents])
 
-  const instanceCount = Object.keys(byInstance).length
+  const instanceCount = useMemo(() => Object.keys(byInstance).length, [byInstance])

152-164: Add aria-expanded for accessibility.

The toggle button should communicate its state to screen readers.

         <button
           type="button"
           onClick={() => setIsExpanded(!isExpanded)}
+          aria-expanded={isExpanded}
           className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
         >
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6070996 and 61eeeff.

📒 Files selected for processing (6)
  • web/src/components/torrents/CrossSeedWarning.tsx (1 hunks)
  • web/src/components/torrents/DeleteTorrentDialog.tsx (1 hunks)
  • web/src/components/torrents/TorrentManagementBar.tsx (9 hunks)
  • web/src/components/torrents/TorrentTableOptimized.tsx (6 hunks)
  • web/src/hooks/useCrossSeedWarning.ts (1 hunks)
  • web/src/hooks/useTorrentActions.ts (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-28T20:32:30.126Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go:209-212
Timestamp: 2025-11-28T20:32:30.126Z
Learning: Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go
Learning: The cross-seed recheck-resume worker intentionally runs for the process lifetime and keys pending entries by hash only. This is acceptable under the current constraint that background seeded-search runs operate on a single instance at a time; graceful shutdown and instanceID|hash keying are deferred by design.

Applied to files:

  • web/src/hooks/useCrossSeedWarning.ts
🧬 Code graph analysis (2)
web/src/hooks/useCrossSeedWarning.ts (2)
web/src/types/index.ts (1)
  • Torrent (204-261)
web/src/lib/cross-seed-utils.ts (2)
  • CrossSeedTorrent (68-72)
  • searchCrossSeedMatches (75-301)
web/src/components/torrents/TorrentTableOptimized.tsx (2)
web/src/hooks/useCrossSeedWarning.ts (1)
  • useCrossSeedWarning (37-124)
web/src/components/torrents/DeleteTorrentDialog.tsx (1)
  • DeleteTorrentDialog (36-92)
⏰ 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 (11)
web/src/hooks/useCrossSeedWarning.ts (1)

43-48: Multi-torrent deletion only checks cross-seeds for the first torrent.

The hook only examines torrents[0] for cross-seed matches. When bulk-deleting multiple torrents, cross-seeds for torrents at indices 1+ won't be detected or warned about.

If this is intentional (performance trade-off), consider adding a note in the UI when torrents.length > 1 indicating that cross-seed detection is limited to the first torrent.

web/src/hooks/useTorrentActions.ts (1)

88-88: LGTM!

The deleteCrossSeeds state is properly initialized, reset after successful delete operations, and exposed in the hook's public API. This follows the existing patterns for other dialog states.

Also applies to: 221-221, 397-397, 829-830

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

162-168: LGTM!

The cross-seed warning hook is correctly wired with appropriate guards: it only runs when the delete dialog is open (enabled: showDeleteDialog), uses a safe instance ID, and gracefully handles missing instance name.

web/src/components/torrents/TorrentTableOptimized.tsx (2)

826-832: LGTM!

The cross-seed warning integration follows the same correct pattern as TorrentManagementBar: enabled only when delete dialog is open, uses instance name from the resolved instance, and operates on contextTorrents which are the torrents being considered for deletion.


2920-2934: LGTM!

The DeleteTorrentDialog is wired correctly with all required props. The count logic properly handles both regular selection and select-all modes (isAllSelected ? effectiveSelectionCount : contextHashes.length).

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

6-22: LGTM!

The imports are well-organized and the CrossSeedWarningProps interface is properly typed with clear, descriptive prop names.


24-35: LGTM!

The getTrackerDomain helper is well-implemented with proper error handling. The try/catch with URL constructor and regex fallback pattern handles edge cases gracefully.


46-62: LGTM!

Clean early return pattern for loading and no-warning states. The subtle loading indicator is appropriately unobtrusive.


88-133: LGTM!

The header section effectively communicates the three different states (cross-seeds will be deleted, files will break cross-seeds, or data preserved) with appropriate visual styling and iconography.


135-148: LGTM!

The checkbox section is properly accessible with correct label association. The checked === true guard appropriately handles Radix UI's potential 'indeterminate' state.


166-205: LGTM!

The expandable torrent list is well-implemented with proper overflow handling, incognito mode support, and the "+N more" indicator for large lists. The key composition ensures uniqueness across instances.

@s0up4200
Copy link
Collaborator Author

s0up4200 commented Dec 2, 2025

IMO the wording on the dialog is unclear

"Safe to remove" would be better as something along the lines of "these torrents will be unaffected upon removal"

"this will break cross-seeded torrents" is also unclear and should clarify whether the matched torrents will be removed from qbittorrent or not.

Fixed.

it might be a nice feature to have a checkbox that when selected also removes the cross-seeded torrents along with the current selected torrent

Fixed.

does it make sense to extend this to warn users in these places:

  • set location dialog: warn users that moving that torrent data will also break cross-seeds ( potentially update the paths of the cross-seeded torrents aswell )
  • set category dialog: specifically when the torrent has autotmm enabled

This is more complex and will have to be looked at in a future PR. But good suggestions for sure.

@s0up4200

This comment was marked as resolved.

- Add search button to manually trigger cross-seed detection
- Check ALL selected torrents instead of just the first one
- Add release modifier detection (REPACK, PROPER, etc.) to prevent false matches
- Show contextual UI urgency when delete files is enabled
- Fix toast count to include cross-seeds when deleting together
- Reset deleteCrossSeeds state on dialog close
- Add tooltip to show remaining torrents in +N more
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 (3)
web/src/components/torrents/CrossSeedWarning.tsx (1)

309-313: Key may not be unique in overflow tooltip.

If the same torrent hash appears in the overflow list across different iterations (unlikely but possible with data issues), using only t.hash as the key could cause React warnings.

Consider using a composite key for consistency with the main list:

                          {torrents.slice(8).map((t) => (
-                            <p key={t.hash} className="truncate text-xs">
+                            <p key={`${t.hash}-${t.instanceId}`} className="truncate text-xs">
                              {incognitoMode ? getLinuxIsoName(t.hash) : t.name}
                            </p>
                          ))}
web/src/components/torrents/TorrentManagementBar.tsx (1)

205-224: Potential duplicate hashes when combining selection with cross-seeds.

If a cross-seeded torrent is already in selectedHashes, combining arrays could produce duplicates. While the backend likely handles this gracefully, deduplicating ensures accurate counts in toast messages.

  const handleDeleteWrapper = useCallback(() => {
    // Include cross-seed hashes if user opted to delete them
-   const hashesToDelete = deleteCrossSeeds
-     ? [...selectedHashes, ...crossSeedWarning.affectedTorrents.map(t => t.hash)]
-     : selectedHashes
+   const hashesToDelete = deleteCrossSeeds
+     ? [...new Set([...selectedHashes, ...crossSeedWarning.affectedTorrents.map(t => t.hash)])]
+     : selectedHashes
web/src/hooks/useCrossSeedWarning.ts (1)

71-129: Consider adding search cancellation support.

The search function correctly iterates through all selected torrents and handles errors gracefully. However, there's no way to cancel an in-progress search if the user closes the dialog. For long-running searches with many torrents, this could lead to wasted API calls.

Consider using an AbortController to enable cancellation:

const search = useCallback(async (signal?: AbortSignal) => {
  if (!instance || torrents.length === 0) return

  setSearchState("searching")
  setCheckedCount(0)
  setAffectedTorrents([])

  const allMatches: CrossSeedTorrent[] = []
  const seenHashes = new Set<string>()

  try {
    for (let i = 0; i < torrents.length; i++) {
      // Check for cancellation
      if (signal?.aborted) {
        setSearchState("idle")
        return
      }

      const torrent = torrents[i]
      // ... rest of the loop
    }
    // ... rest of the function
  } catch (error) {
    if (signal?.aborted) {
      setSearchState("idle")
    } else {
      console.error("[CrossSeedWarning] Search failed:", error)
      setSearchState("error")
    }
  }
}, [instance, torrents, instanceId, instanceName, hashesBeingDeleted])

Then update the return type and reset function to support cancellation.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eb29241 and f9a223c.

📒 Files selected for processing (8)
  • web/src/components/torrents/CrossSeedWarning.tsx (1 hunks)
  • web/src/components/torrents/DeleteFilesPreference.tsx (1 hunks)
  • web/src/components/torrents/DeleteTorrentDialog.tsx (1 hunks)
  • web/src/components/torrents/TorrentManagementBar.tsx (8 hunks)
  • web/src/components/torrents/TorrentTableOptimized.tsx (6 hunks)
  • web/src/hooks/useCrossSeedWarning.ts (1 hunks)
  • web/src/hooks/useTorrentActions.ts (5 hunks)
  • web/src/lib/cross-seed-utils.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/src/components/torrents/DeleteTorrentDialog.tsx
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
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.
📚 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/lib/cross-seed-utils.ts
📚 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/TorrentManagementBar.tsx
  • web/src/hooks/useTorrentActions.ts
  • web/src/components/torrents/TorrentTableOptimized.tsx
  • web/src/components/torrents/DeleteFilesPreference.tsx
📚 Learning: 2025-11-28T20:32:30.126Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go:209-212
Timestamp: 2025-11-28T20:32:30.126Z
Learning: Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go
Learning: The cross-seed recheck-resume worker intentionally runs for the process lifetime and keys pending entries by hash only. This is acceptable under the current constraint that background seeded-search runs operate on a single instance at a time; graceful shutdown and instanceID|hash keying are deferred by design.

Applied to files:

  • web/src/hooks/useCrossSeedWarning.ts
🧬 Code graph analysis (3)
web/src/components/torrents/CrossSeedWarning.tsx (4)
web/src/lib/cross-seed-utils.ts (1)
  • CrossSeedTorrent (96-100)
web/src/hooks/useCrossSeedWarning.ts (1)
  • CrossSeedSearchState (19-19)
web/src/lib/incognito.ts (2)
  • useIncognitoMode (321-352)
  • getLinuxIsoName (156-163)
web/src/lib/utils.ts (1)
  • cn (9-11)
web/src/components/torrents/DeleteFilesPreference.tsx (1)
web/src/lib/utils.ts (1)
  • cn (9-11)
web/src/hooks/useCrossSeedWarning.ts (3)
web/src/types/index.ts (1)
  • Torrent (204-261)
web/src/lib/cross-seed-utils.ts (2)
  • CrossSeedTorrent (96-100)
  • searchCrossSeedMatches (103-334)
web/src/lib/api.ts (1)
  • api (1647-1647)
🪛 ast-grep (0.40.0)
web/src/lib/cross-seed-utils.ts

[warning] 37-37: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp((?:^|[.\\-_\\s])${mod}(?:[.\\-_\\s]|$))
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

⏰ 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 (20)
web/src/components/torrents/DeleteFilesPreference.tsx (1)

25-25: LGTM!

Minor padding adjustment to better fit the component in the new DeleteTorrentDialog layout.

web/src/components/torrents/CrossSeedWarning.tsx (4)

30-41: LGTM!

Good defensive implementation with URL parsing and regex fallback for malformed tracker URLs.


58-122: LGTM!

Good UX design with contextual urgency indicators - appropriately warns users more prominently when deleting files (higher risk of breaking cross-seeds).


124-165: LGTM!

Clean state handling with appropriate visual feedback for searching, error, and success states.


240-252: LGTM!

Good implementation of the cross-seed deletion checkbox per community feedback.

web/src/hooks/useTorrentActions.ts (2)

88-88: LGTM!

Good addition of deleteCrossSeeds state to support the new cross-seed deletion feature.


739-742: LGTM!

The closeDeleteDialog helper properly resets deleteCrossSeeds state when the dialog is cancelled, addressing the checkbox persistence issue noted in PR comments.

web/src/lib/cross-seed-utils.ts (3)

34-41: Static analysis false positive - safe to ignore.

The mod variable comes from the hardcoded RELEASE_MODIFIERS array containing only safe alphanumeric strings. The regex pattern has no nested quantifiers, so there's no ReDoS risk here.


47-53: LGTM!

Clean comparison logic with early length check optimization. Using sorted arrays ensures order-independent matching.


218-223: LGTM!

Good addition of release modifier check before fuzzy matching - this correctly prevents false matches between different release versions (e.g., REPACK vs original).

web/src/components/torrents/TorrentManagementBar.tsx (3)

86-87: LGTM!

Good defensive guard for instanceId to prevent hooks from receiving invalid values.


162-167: Cross-seed detection for multi-select has a known limitation.

Per the PR author's self-review, the useCrossSeedWarning hook currently only inspects the first torrent when multiple are selected. This is documented and can be addressed in a follow-up.

The integration here is correct - all selectedTorrents are passed to the hook.


617-636: LGTM!

Clean integration of the new DeleteTorrentDialog with proper state cleanup on close - both closeDeleteDialog() and crossSeedWarning.reset() ensure no stale state persists.

web/src/components/torrents/TorrentTableOptimized.tsx (3)

6-6: LGTM!

Clean import of the new useCrossSeedWarning hook.


826-831: Cross-seed detection now checks all selected torrents.

The hook correctly receives all contextTorrents rather than just the first one, addressing the multi-torrent detection issue mentioned in the PR objectives.


1968-1987: LGTM!

The delete wrapper correctly composes the list of hashes to delete based on the deleteCrossSeeds flag and updates the client metadata for accurate toast notifications.

web/src/hooks/useCrossSeedWarning.ts (4)

13-44: LGTM!

The interfaces and types are well-defined with clear documentation. The opt-in search pattern (requiring explicit search() call) is a good design choice for performance.


50-69: LGTM!

The state management and instance query setup is clean. Pre-fetching the instances ensures the data is ready when the user initiates a search.


131-135: LGTM!

The reset function correctly clears all search state.


137-145: LGTM!

The return object provides a clean API surface with all necessary data and control functions.

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: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f9a223c and 726027c.

📒 Files selected for processing (3)
  • web/src/components/torrents/CrossSeedWarning.tsx (1 hunks)
  • web/src/components/torrents/DeleteTorrentDialog.tsx (1 hunks)
  • web/src/components/torrents/TorrentCardsMobile.tsx (10 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
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.
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go:209-212
Timestamp: 2025-11-28T20:32:30.126Z
Learning: Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go
Learning: The cross-seed recheck-resume worker intentionally runs for the process lifetime and keys pending entries by hash only. This is acceptable under the current constraint that background seeded-search runs operate on a single instance at a time; graceful shutdown and instanceID|hash keying are deferred by design.
📚 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/DeleteTorrentDialog.tsx
  • web/src/components/torrents/TorrentCardsMobile.tsx
🧬 Code graph analysis (2)
web/src/components/torrents/DeleteTorrentDialog.tsx (4)
web/src/hooks/useCrossSeedWarning.ts (1)
  • CrossSeedWarningResult (21-36)
web/src/components/ui/alert-dialog.tsx (8)
  • AlertDialog (151-151)
  • AlertDialogContent (155-155)
  • AlertDialogHeader (156-156)
  • AlertDialogTitle (158-158)
  • AlertDialogDescription (159-159)
  • AlertDialogFooter (157-157)
  • AlertDialogCancel (161-161)
  • AlertDialogAction (160-160)
web/src/components/torrents/DeleteFilesPreference.tsx (1)
  • DeleteFilesPreference (16-67)
web/src/components/torrents/CrossSeedWarning.tsx (1)
  • CrossSeedWarning (43-310)
web/src/components/torrents/TorrentCardsMobile.tsx (3)
web/src/hooks/useInstances.ts (1)
  • useInstances (10-188)
web/src/hooks/useCrossSeedWarning.ts (1)
  • useCrossSeedWarning (45-146)
web/src/components/torrents/DeleteTorrentDialog.tsx (1)
  • DeleteTorrentDialog (36-100)
⏰ 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 (10)
web/src/components/torrents/CrossSeedWarning.tsx (4)

1-16: LGTM!

Imports are well-organized and the component pulls in appropriate dependencies for its functionality.


30-41: LGTM!

The getTrackerDomain helper handles both standard URLs and edge cases gracefully with the regex fallback.


58-106: LGTM!

Good UX design - the contextual urgency (more prominent styling when deleteFiles is true) appropriately signals higher risk to users.


108-149: LGTM!

The searching, error, and complete states are well-implemented with appropriate feedback (progress indicator, retry option, success confirmation).

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

36-99: LGTM!

The dialog component is well-structured with clear separation of concerns. The displayCount calculation correctly reflects the total number of torrents to be deleted when cross-seeds are included.

web/src/components/torrents/TorrentCardsMobile.tsx (5)

67-70: LGTM!

New imports are correctly added for cross-seed warning functionality.


1251-1264: LGTM!

The cross-seed warning is correctly set up to handle both single torrent deletion and bulk selection scenarios.


1537-1581: LGTM!

The delete handler correctly integrates cross-seed deletion - it includes cross-seed hashes in both the deletion payload and the optimistic UI updates when the user opts in.


2117-2129: LGTM!

The delete action is correctly triggered through prepareDeleteAction, passing the necessary data for cross-seed detection.


1065-1068: The cross-seed detection correctly handles multi-select scenarios.

The useCrossSeedWarning hook iterates through all torrents in the for loop (lines 83-121) and processes each one individually. The hook's documentation explicitly states it "Checks ALL selected torrents, not just the first one." No torrents[0] limitation exists in this implementation.

Likely an incorrect or invalid review comment.

@s0up4200 s0up4200 merged commit 7f779f9 into main Dec 3, 2025
11 checks passed
@s0up4200 s0up4200 deleted the feat/delete-dialog-cross-seed-warning branch December 3, 2025 21:24
@coderabbitai coderabbitai bot mentioned this pull request Dec 8, 2025
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

enhancement New feature or request torrent web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants