feat(torrents): warn about cross-seeded torrents in delete dialogs#670
feat(torrents): warn about cross-seeded torrents in delete dialogs#670
Conversation
WalkthroughAdds 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 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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🧰 Additional context used🧠 Learnings (2)📓 Common learnings📚 Learning: 2025-12-03T18:11:08.659ZApplied to files:
⏰ 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)
🔇 Additional comments (5)
Comment |
There was a problem hiding this comment.
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 relateduseMemo(line 119), anduseCrossSeedWarning(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
📒 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-xlclass 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
contextTorrentsappropriately represents the torrents being prepared for deletion. Theenabled: showDeleteDialogflag ensures the query only runs when the dialog is open.
2925-2950: LGTM on the delete dialog cross-seed warning UI.The
CrossSeedWarningcomponent is properly integrated with all required props. The!max-w-2xlclass 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
getTrackerDomainhelper 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
reduceis 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. ThestaleTimeof 10 seconds andgcTimeof 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: falsewhen disabled or no expression- Reports
isLoading: truewhile the query is pending- Filters out torrents being deleted using the
hashesBeingDeletedSet- Enriches remaining torrents with instance metadata and
matchType
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
web/src/hooks/useCrossSeedWarning.ts (1)
44-71: Expression building and path escaping now correctly handle Windows pathsThe memoized construction of
{ expr, hashesBeingDeleted }looks good: you dedupe hashes/paths viaSet, early‑out on empty input, and theContentPathconditions 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
📒 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
UseCrossSeedWarningOptionsandCrossSeedWarningResultare well‑named, minimal, and aligned with the existingCrossSeedTorrenttype. JSDoc comments clearly describe intent and should make the hook easy to adopt.
73-94: Query wiring and cache settings fit the use caseThe
useQuerysetup (keying on["cross-seed-warning", instanceId, expr], guarding withenabled && expr.length > 0, and using a modestlimitplus shortstaleTime/gcTime) matches the expected “ephemeral helper for dialogs” usage. This should avoid unnecessary calls while keeping results reasonably fresh.
|
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. |
|
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 |
|
does it make sense to extend this to warn users in these places:
|
There was a problem hiding this comment.
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 conditionisLoading || !matchesreturnsisLoading: trueindefinitely. Users see a perpetual loading indicator instead of graceful degradation.This was flagged in a previous review. Destructure
isErrorand 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.isLoadingis true, the user might confirm deletion before understanding which cross-seeded torrents would be affected. Consider either:
- Disabling the Delete button while loading
- 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)] : selectedHashesWhile 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)])] : selectedHashesweb/src/components/torrents/TorrentTableOptimized.tsx (1)
1969-1982: Same duplicate hash concern as TorrentManagementBar.The
handleDeleteWrapperin 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)])] : contextHashesweb/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: Addaria-expandedfor 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
📒 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 > 1indicating that cross-seed detection is limited to the first torrent.web/src/hooks/useTorrentActions.ts (1)
88-88: LGTM!The
deleteCrossSeedsstate 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
contextTorrentswhich 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
CrossSeedWarningPropsinterface is properly typed with clear, descriptive prop names.
24-35: LGTM!The
getTrackerDomainhelper 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 === trueguard 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.
Fixed.
Fixed.
This is more complex and will have to be looked at in a future PR. But good suggestions for sure. |
This comment was marked as resolved.
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
There was a problem hiding this comment.
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.hashas 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)])] + : selectedHashesweb/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
AbortControllerto 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
📒 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.tsxweb/src/hooks/useTorrentActions.tsweb/src/components/torrents/TorrentTableOptimized.tsxweb/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
deleteCrossSeedsstate to support the new cross-seed deletion feature.
739-742: LGTM!The
closeDeleteDialoghelper properly resetsdeleteCrossSeedsstate 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
modvariable comes from the hardcodedRELEASE_MODIFIERSarray 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
instanceIdto 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
useCrossSeedWarninghook 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
selectedTorrentsare passed to the hook.
617-636: LGTM!Clean integration of the new
DeleteTorrentDialogwith proper state cleanup on close - bothcloseDeleteDialog()andcrossSeedWarning.reset()ensure no stale state persists.web/src/components/torrents/TorrentTableOptimized.tsx (3)
6-6: LGTM!Clean import of the new
useCrossSeedWarninghook.
826-831: Cross-seed detection now checks all selected torrents.The hook correctly receives all
contextTorrentsrather 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
deleteCrossSeedsflag 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.
There was a problem hiding this comment.
Actionable comments posted: 2
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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.tsxweb/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
getTrackerDomainhelper 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
deleteFilesis 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
displayCountcalculation 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
useCrossSeedWarninghook iterates through all torrents in theforloop (lines 83-121) and processes each one individually. The hook's documentation explicitly states it "Checks ALL selected torrents, not just the first one." Notorrents[0]limitation exists in this implementation.Likely an incorrect or invalid review comment.
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 ([#​711](autobrr/qui#711)) ([@​s0up4200](https://github.com/s0up4200)) - [`bd2587b`](autobrr/qui@bd2587b): fix(db): resolve cross-seed settings mutual exclusivity lockout ([#​714](autobrr/qui#714)) ([@​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 ([#​637](autobrr/qui#637)) ([@​s0up4200](https://github.com/s0up4200)) - [`85fd74b`](autobrr/qui@85fd74b): feat(jackett): propagate 429 rate limits with retry and cooldown ([#​684](autobrr/qui#684)) ([@​s0up4200](https://github.com/s0up4200)) - [`a5777c4`](autobrr/qui@a5777c4): feat(reannounce): add configurable max retries setting ([#​685](autobrr/qui#685)) ([@​s0up4200](https://github.com/s0up4200)) - [`6451e56`](autobrr/qui@6451e56): feat(settings): add TMM relocation behavior settings ([#​664](autobrr/qui#664)) ([@​s0up4200](https://github.com/s0up4200)) - [`680fd25`](autobrr/qui@680fd25): feat(torrents): add confirmation dialogs for TMM and Set Location ([#​687](autobrr/qui#687)) ([@​s0up4200](https://github.com/s0up4200)) - [`7f779f9`](autobrr/qui@7f779f9): feat(torrents): warn about cross-seeded torrents in delete dialogs ([#​670](autobrr/qui#670)) ([@​s0up4200](https://github.com/s0up4200)) - [`1c489bc`](autobrr/qui@1c489bc): feat(ui): persist category collapse state in sidebar ([#​692](autobrr/qui#692)) ([@​jabloink](https://github.com/jabloink)) - [`bdf807e`](autobrr/qui@bdf807e): feat(web): Torrent list details bar shows free space ([#​691](autobrr/qui#691)) ([@​finevan](https://github.com/finevan)) ##### Bug Fixes - [`9db8346`](autobrr/qui@9db8346): fix(crossseed): use matched torrent save path instead of category path ([#​700](autobrr/qui#700)) ([@​s0up4200](https://github.com/s0up4200)) - [`40d7778`](autobrr/qui@40d7778): fix(instance): intern empty string on demand for bypass auth ([#​693](autobrr/qui#693)) ([@​s0up4200](https://github.com/s0up4200)) - [`0aaf39e`](autobrr/qui@0aaf39e): fix(jackett): fetch indexer capabilities in parallel with retries ([#​701](autobrr/qui#701)) ([@​s0up4200](https://github.com/s0up4200)) - [`50e585b`](autobrr/qui@50e585b): fix(qbittorrent): cache tracker health counts in background ([#​662](autobrr/qui#662)) ([@​KyleSanderson](https://github.com/KyleSanderson)) - [`298ca05`](autobrr/qui@298ca05): fix(search): download torrent files via backend for remote instances ([#​686](autobrr/qui#686)) ([@​s0up4200](https://github.com/s0up4200)) - [`27ee31a`](autobrr/qui@27ee31a): fix(torrents): AddTorrentDialog uses the downloadPath api ([#​677](autobrr/qui#677)) ([@​finevan](https://github.com/finevan)) - [`2427fdd`](autobrr/qui@2427fdd): fix(ui): use full category paths in multi-select ([#​683](autobrr/qui#683)) ([@​jabloink](https://github.com/jabloink)) - [`917c65e`](autobrr/qui@917c65e): fix(web): add iOS Safari compatibility for torrent file picker ([#​707](autobrr/qui#707)) ([@​s0up4200](https://github.com/s0up4200)) - [`2ccdc28`](autobrr/qui@2ccdc28): fix(web): dont hide free space when disk is full ([#​694](autobrr/qui#694)) ([@​ewenjo](https://github.com/ewenjo)) ##### Other Changes - [`d684442`](autobrr/qui@d684442): chore(deps): bump the golang group with 7 updates ([#​660](autobrr/qui#660)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`e1267fa`](autobrr/qui@e1267fa): chore(deps): bump the npm group across 1 directory with 29 updates ([#​663](autobrr/qui#663)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`8671971`](autobrr/qui@8671971): docs: Update README to remove size field description ([#​695](autobrr/qui#695)) ([@​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>
Summary by CodeRabbit
New Features
Refactor
Style
✏️ Tip: You can customize this high-level summary in your review settings.