refactor(cross-seed): move local matching to backend#1069
Conversation
Frontend Levenshtein fuzzy matching (90% threshold) incorrectly matched
releases with different versions. Replace with backend matching using
the rls library for proper release metadata comparison.
Changes:
- Add GET /api/cross-seed/torrents/{instanceID}/{hash}/local-matches endpoint
- Add FindLocalMatches service method with three match strategies:
- content_path: same content location on disk
- name: exact torrent name match
- release: rls library metadata match
- Update TorrentDetailsPanel, useCrossSeedWarning, useCrossSeedFilter
to use backend API instead of frontend fuzzy matching
- Remove Levenshtein similarity code from cross-seed-utils.ts
- Fix tracker health not populating in GetCachedInstanceTorrents
✅ Deploy Preview for getqui canceled.
|
WalkthroughAdds a backend endpoint to find local cross-seed matches and related service/models, updates qbittorrent caching, OpenAPI docs, API client, frontend types/hooks/components to use the backend-driven local-match results. Changes
Sequence Diagram(s)sequenceDiagram
participant Browser as Client (Browser)
participant API as API Handler
participant Svc as CrossSeed Service
participant Cache as Instance Cache
participant QBT as qBittorrent Instance(s)
Browser->>API: GET /api/cross-seed/torrents/{instanceID}/{hash}/local-matches
API->>Svc: FindLocalMatches(sourceInstanceID, sourceHash)
Svc->>QBT: Fetch source torrent from sourceInstance
QBT-->>Svc: Source torrent
Svc->>Svc: Parse release metadata, normalize content path
rect rgb(235,245,255)
Note over Svc,Cache: Iterate all instances and cached torrents
loop For each instance
Svc->>Cache: GetCachedInstanceTorrents(instanceID)
Cache-->>Svc: Instance torrents
Svc->>Svc: determineLocalMatchType(source, candidate)
end
end
Svc-->>API: LocalMatchesResponse
API-->>Browser: 200 [LocalCrossSeedMatch...]
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 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)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR Review SummaryReviewed areas: Backend API, Integration (cross-seed service), Frontend (React hooks & utils), Performance, Documentation Passed Checks
RecommendationsPerformance
Code Quality
Documentation Updates Needed
Minor Issues
Positive Notes✅ Excellent refactoring - moving complex matching logic from frontend to backend is the right architectural choice Automated review by Claude Code |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
web/src/hooks/useCrossSeedWarning.ts (1)
188-191: RedundantinstanceNameoverride.
toCompatibleMatch(match)already setsinstanceName: m.instanceNameon line 55. Since matches are filtered to only include those from the currentinstanceId(line 172), overwritinginstanceNamehere with the hook parameter is redundant. Consider removing the override for clarity:allMatches.push({ ...toCompatibleMatch(match), - instanceName, })However, this is harmless and could be considered defensive coding if you prefer to keep it.
web/src/components/torrents/TorrentDetailsPanel.tsx (2)
1695-1698: Consider adding exhaustive type checking for match types.The type assertion on line 1695 assumes the backend always returns one of these four match types. While the ternary chain has a safe fallback to "Name", consider:
- If the backend returns an unexpected value, the UI will silently show "Name" and "Same torrent name" which may be misleading.
- The assertion hides potential type mismatches from TypeScript.
If the
CrossSeedTorrenttype fromtoCompatibleMatchalready typesmatchTypecorrectly, the assertion is unnecessary. If not, consider logging unexpected values in development.
1517-1517: Readability note.The inline ternary is dense. Consider extracting to a local variable if this pattern is used often in the codebase, but acceptable as-is.
internal/services/crossseed/service.go (1)
420-493: Tighten validation and normalization inFindLocalMatchesThe core flow looks solid, but a few consistency and robustness issues are worth addressing:
- Missing argument validation / instance existence check
Other service entrypoints (e.g.,
SearchTorrentMatches) explicitly validate:
instanceID > 0- non‑blank hash
- instance exists via
instanceStore.Get
FindLocalMatchesskips this and goes straight toGetTorrents, so:
sourceInstanceID <= 0or an unknown instance will surface as a generic"failed to get source torrent"instead of a clearErrInvalidRequest/instance‑not‑found error.- This makes the API behavior inconsistent with the rest of the cross‑seed surface.
Given this is a new exported method, it’s a good place to align error semantics up front.
- Defensive guard for nil instances from
ListYou iterate
instanceswithout checking for nil entries:for _, instance := range instances { // ... cachedTorrents, err := s.syncManager.GetCachedInstanceTorrents(ctx, instance.ID)Elsewhere (e.g.,
buildAutomationSnapshots) the code defensively skips nil instances. IfListever returns a nil, this will panic. Very cheap to guard and keeps this method in line with existing patterns.
- Path normalization for
ContentPathYou normalize the source content path as:
normalizedContentPath := strings.ToLower(strings.ReplaceAll(sourceTorrent.ContentPath, "\\", "/"))and the candidate likewise in
determineLocalMatchType. This misses:
- trailing slash differences (
/path/vs/path)filepath.Cleansemantics already used elsewhere vianormalizePathUsing the existing helper will make equality more robust and consistent with the rest of the service, while still preserving case‑insensitive comparison:
normalizedContentPath := strings.ToLower(normalizePath(sourceTorrent.ContentPath))(and similarly for the candidate).
- Minor consistency nit: filter constant
You currently pass
Filter: "all"toGetTorrents. Most of this file usesqbt.TorrentFilterOptions{Hashes: []string{hash}}(withoutFilter) or theqbt.TorrentFilterAllconstant. Not a bug, but switching to the constant for readability and consistency might help.Suggested refactor sketch (argument validation + guards + normalization)
func (s *Service) FindLocalMatches(ctx context.Context, sourceInstanceID int, sourceHash string) (*LocalMatchesResponse, error) { + if sourceInstanceID <= 0 { + return nil, fmt.Errorf("%w: sourceInstanceID must be positive", ErrInvalidRequest) + } + if strings.TrimSpace(sourceHash) == "" { + return nil, fmt.Errorf("%w: sourceHash is required", ErrInvalidRequest) + } + // Get all instances instances, err := s.instanceStore.List(ctx) if err != nil { return nil, fmt.Errorf("failed to list instances: %w", err) } + // Optional: ensure source instance exists, for clearer errors + if _, err := s.instanceStore.Get(ctx, sourceInstanceID); err != nil { + if errors.Is(err, models.ErrInstanceNotFound) { + return nil, fmt.Errorf("%w: instance %d not found", ErrInvalidRequest, sourceInstanceID) + } + return nil, fmt.Errorf("failed to load source instance %d: %w", sourceInstanceID, err) + } + // Find the source torrent - sourceTorrents, err := s.syncManager.GetTorrents(ctx, sourceInstanceID, qbt.TorrentFilterOptions{ - Filter: "all", - Hashes: []string{sourceHash}, - }) + sourceTorrents, err := s.syncManager.GetTorrents(ctx, sourceInstanceID, qbt.TorrentFilterOptions{ + Hashes: []string{sourceHash}, + }) @@ - // Normalize content path for comparison - normalizedContentPath := strings.ToLower(strings.ReplaceAll(sourceTorrent.ContentPath, "\\", "/")) + // Normalize content path for comparison (case-insensitive, trimmed/cleaned) + normalizedContentPath := strings.ToLower(normalizePath(sourceTorrent.ContentPath)) @@ - for _, instance := range instances { + for _, instance := range instances { + if instance == nil { + continue + } @@ - matchType := s.determineLocalMatchType( - &sourceTorrent, sourceRelease, - cached, normalizedContentPath, - ) + matchType := s.determineLocalMatchType(&sourceTorrent, sourceRelease, cached, normalizedContentPath)
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
internal/api/handlers/crossseed.gointernal/qbittorrent/sync_manager.gointernal/services/crossseed/models.gointernal/services/crossseed/service.gointernal/web/swagger/openapi.yamlweb/src/components/torrents/TorrentDetailsPanel.tsxweb/src/components/torrents/details/CrossSeedTable.tsxweb/src/hooks/useCrossSeedFilter.tsweb/src/hooks/useCrossSeedWarning.tsweb/src/lib/api.tsweb/src/lib/cross-seed-utils.tsweb/src/types/index.ts
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
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.
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).
📚 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/types/index.tsweb/src/hooks/useCrossSeedFilter.tsweb/src/hooks/useCrossSeedWarning.tsweb/src/components/torrents/details/CrossSeedTable.tsxweb/src/lib/api.tsweb/src/components/torrents/TorrentDetailsPanel.tsxweb/src/lib/cross-seed-utils.ts
📚 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/useCrossSeedFilter.tsweb/src/hooks/useCrossSeedWarning.tsinternal/services/crossseed/models.gointernal/services/crossseed/service.goweb/src/components/torrents/TorrentDetailsPanel.tsxinternal/api/handlers/crossseed.go
📚 Learning: 2025-12-03T18:11:08.682Z
Learnt from: finevan
Repo: autobrr/qui PR: 677
File: web/src/components/torrents/AddTorrentDialog.tsx:496-504
Timestamp: 2025-12-03T18:11:08.682Z
Learning: In the AddTorrentDialog component (web/src/components/torrents/AddTorrentDialog.tsx), temporary path settings (useDownloadPath/downloadPath) should be applied per-torrent rather than updating global instance preferences. The UI/UX is designed to suggest that these options apply to the individual torrent being added.
Applied to files:
web/src/hooks/useCrossSeedWarning.tsweb/src/components/torrents/TorrentDetailsPanel.tsxweb/src/lib/cross-seed-utils.ts
📚 Learning: 2025-11-28T22:21:20.730Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go:2415-2457
Timestamp: 2025-11-28T22:21:20.730Z
Learning: Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go
Learning: The determineSavePath function intentionally includes a contentLayout string parameter for future/content-layout branching and API consistency. Its presence is by design even if unused in the current body; do not flag as an issue in reviews.
Applied to files:
internal/services/crossseed/models.gointernal/services/crossseed/service.go
📚 Learning: 2025-12-28T18:44:10.496Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 876
File: internal/logstream/hub_test.go:188-192
Timestamp: 2025-12-28T18:44:10.496Z
Learning: In Go 1.25 (Aug 2025), use wg.Go(func()) to spawn a goroutine and automate the Add/Done lifecycle. Replace manual patterns like wg.Add(1); go func(){ defer wg.Done(); ... }() with wg.Go(func(){ ... }). Ensure the codebase builds with Go 1.25+ and apply this in relevant Go files (e.g., internal/logstream/hub_test.go). If targeting older Go versions, maintain the existing pattern.
Applied to files:
internal/services/crossseed/models.gointernal/qbittorrent/sync_manager.gointernal/services/crossseed/service.gointernal/api/handlers/crossseed.go
📚 Learning: 2025-12-11T08:40:01.329Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 746
File: internal/services/reannounce/service.go:480-481
Timestamp: 2025-12-11T08:40:01.329Z
Learning: In autobrr/qui's internal/services/reannounce/service.go, the hasHealthyTracker, getProblematicTrackers, and getHealthyTrackers functions intentionally match qbrr's lenient tracker health logic (skip unregistered trackers and check if any other tracker is healthy) rather than go-qbittorrent's strict isTrackerStatusOK logic (which treats unregistered as an immediate failure). For multi-tracker torrents, if one tracker is working, reannouncing won't help. The duplication of the health check logic across these three functions is acceptable as it's a simple one-liner, and extracting it would add unnecessary complexity.
Applied to files:
internal/qbittorrent/sync_manager.go
📚 Learning: 2025-11-21T21:11:50.633Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 625
File: internal/qbittorrent/sync_manager.go:1112-1123
Timestamp: 2025-11-21T21:11:50.633Z
Learning: In internal/qbittorrent/sync_manager.go, the GetCachedFilesBatch interface documentation (around line 39-40) should specify "uppercase hex" instead of "lowercase hex" to match the actual normalization practice used throughout the codebase (e.g., normalizeHash in internal/services/crossseed/service.go uses strings.ToUpper, and hash filtering uses uppercase).
Applied to files:
internal/qbittorrent/sync_manager.go
🧬 Code graph analysis (7)
web/src/hooks/useCrossSeedFilter.ts (1)
web/src/lib/api.ts (1)
api(1962-1962)
web/src/hooks/useCrossSeedWarning.ts (2)
web/src/types/index.ts (1)
LocalCrossSeedMatch(94-109)web/src/lib/cross-seed-utils.ts (2)
CrossSeedTorrent(40-44)normalizePath(12-12)
internal/services/crossseed/models.go (2)
web/src/types/index.ts (1)
Category(684-687)internal/qbittorrent/sync_manager.go (1)
TrackerHealth(85-85)
web/src/lib/api.ts (1)
web/src/types/index.ts (1)
LocalCrossSeedMatch(94-109)
web/src/components/torrents/TorrentDetailsPanel.tsx (1)
web/src/lib/cross-seed-utils.ts (1)
useLocalCrossSeedMatches(50-87)
web/src/lib/cross-seed-utils.ts (2)
web/src/types/index.ts (2)
Torrent(470-527)LocalCrossSeedMatch(94-109)web/src/lib/api.ts (1)
api(1962-1962)
internal/api/handlers/crossseed.go (1)
internal/api/handlers/helpers.go (2)
RespondError(43-47)RespondJSON(23-40)
⏰ 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). (2)
- GitHub Check: Run tests
- GitHub Check: review
🔇 Additional comments (29)
internal/qbittorrent/sync_manager.go (2)
216-237: Field initialization looks correct; validatedTrackerMapping properly set up for background refresh.The NewSyncManager constructor correctly initializes the new
validatedTrackerMappingfield as an empty map (line 228), which will be populated by the backgroundrefreshTrackerHealthCountsloop that builds pre-validated tracker-to-hash relationships. This integrates cleanly with the existing tracker health caching infrastructure.
1173-1189: Refactored TorrentView assembly correctly leverages cached tracker health for improved performance.The changes to
GetCachedInstanceTorrentsimprove the health determination flow by:
- Retrieving cached health counts upfront (line 1174)
- Using a local
viewvariable to build eachTorrentView(line 1178)- Properly prioritizing enriched tracker data when available, falling back to cached hash sets (lines 1180–1189)
This pattern aligns with the broader shift to cached/background-refreshed health data, avoiding inline API calls during cross-instance torrent enumeration. No functional issues detected.
web/src/components/torrents/details/CrossSeedTable.tsx (1)
85-86: LGTM! Match type label updated correctly.The new "release" match type aligns with the backend's rls library-based matching, providing clearer semantics for metadata-driven matches.
web/src/types/index.ts (1)
91-109: LGTM! Well-structured interface for backend-driven matches.The
LocalCrossSeedMatchinterface properly defines the structure for release metadata-based matching results from the backend. The field types and optionaltrackerHealthare appropriate.web/src/hooks/useCrossSeedFilter.ts (1)
37-71: LGTM! Successfully migrated to backend-driven matching.The refactor correctly replaces client-side orchestration with a single backend API call. The filter construction and error handling are appropriate.
One minor observation: Line 47 explicitly includes the selected torrent's hash in the filter conditions. This is correct since the backend returns matches from other instances, so the source torrent should be included to show all related torrents in the filtered view.
web/src/lib/api.ts (2)
771-821: LGTM! API method follows established patterns.The new
getLocalCrossSeedMatchesmethod correctly:
- Follows the existing snake_case → camelCase normalization pattern (as per learnings)
- Uses appropriate error handling via
this.request- Maps all backend fields to the frontend
LocalCrossSeedMatchtypeThe matchType cast on line 819 is type-safe given the union definition.
1035-1040: Formatting adjustment only.The ternary operator formatting change improves readability but has no functional impact.
internal/api/handlers/crossseed.go (4)
361-378: Excellent DRY refactor with proper validation.The new
parseTorrentParamshelper eliminates code duplication across multiple endpoints and provides consistent validation:
- Validates instanceID is a positive integer
- Validates hash is non-empty after trimming
- Returns a clear
okboolean for control flow- Sends appropriate 400 Bad Request responses with descriptive messages
This is a clean pattern that improves maintainability.
393-395: LGTM! Refactored to use shared helper.Both
AnalyzeTorrentForSearchandGetAsyncFilteringStatusnow correctly delegate parameter extraction and validation toparseTorrentParams, reducing duplication and ensuring consistent error handling.Also applies to: 426-428
446-477: LGTM! Handler follows established patterns.The new
GetLocalMatcheshandler is well-implemented:
- Uses the
parseTorrentParamshelper for validation- Calls the service layer appropriately
- Maps errors to correct HTTP status codes via
mapCrossSeedErrorStatus- Includes proper structured logging with context
- Follows the same pattern as other handlers in this file
The Swagger documentation (lines 446-457) clearly describes the endpoint's purpose and differentiates it from fuzzy string matching.
33-48: Formatting adjustments only.These spacing changes improve code alignment but have no functional impact.
Also applies to: 54-54, 734-735
web/src/hooks/useCrossSeedWarning.ts (3)
38-101: LGTM! Compatibility shim for backend migration.The
toCompatibleMatchhelper cleanly bridges the newLocalCrossSeedMatchbackend response to the existingCrossSeedTorrentinterface. The default values for unusedTorrentfields are appropriate for this use case.One minor note:
total_sizeon line 94 is set tom.size, which correctly mirrors thesizefield - good attention to detail.
164-165: Backend API integration looks correct.The migration to
api.getLocalCrossSeedMatches(instanceId, torrent.hash)properly delegates release matching (via the rls library) to the backend as intended by this PR.
177-178: Path property names correctly updated.Using
match.savePathandmatch.contentPath(camelCase) aligns with theLocalCrossSeedMatchinterface and the API layer's snake_case-to-camelCase normalization pattern.internal/web/swagger/openapi.yaml (2)
3163-3184: Well-structured endpoint definition.The new
/api/cross-seed/torrents/{instanceID}/{hash}/local-matchesendpoint follows existing patterns, correctly references the shared parameters, and documents the expected 400/500 error cases.
5022-5071: Schema definition is complete and consistent.The
LocalCrossSeedMatchschema correctly defines all fields with appropriate types, including:
trackerHealthas optional with proper enum constraintmatchTypewith the three valid values matching the backend constantsThis aligns well with the frontend type definition in
web/src/types/index.ts.internal/services/crossseed/models.go (2)
15-20: Match type constants look good.The unexported constants follow Go conventions for internal use and provide clear semantic meaning for the match types.
305-306: JSON tag consistency with OpenAPI.The
SavePathandContentPathfields usesave_pathandcontent_pathJSON tags (snake_case), which correctly match the OpenAPI schema'ssavePathandcontentPathproperties after the API layer's JSON marshaling. Just confirming this is intentional and aligns with the codebase's conventions.web/src/components/torrents/TorrentDetailsPanel.tsx (7)
23-23: LGTM!The import correctly switches to the new
useLocalCrossSeedMatcheshook which calls the backend API for local cross-seed matching instead of performing client-side matching.
142-143: LGTM!The hook invocation correctly passes
isCrossSeedTabActiveas the enabled flag, ensuring the backend API is only called when the cross-seed tab is active. The destructured values match the hook's return signature.
152-158: LGTM!The
.filter(k => k)on line 158 correctly handles the edge case wherematchingTorrentsKeysis an empty string, ensuring the result is an empty array rather than[""].
908-931: LGTM!The explicit fragment wrapper correctly groups the
Separatorand griddivas siblings within the conditional rendering block.
1706-1710: LGTM!The click handler correctly prevents navigation when clicking on the checkbox by checking
closest("[role=\"checkbox\"]"). The escaped quotes are proper JSX syntax.
597-602: LGTM!Minor formatting normalization for property names and template strings.
669-687: LGTM!The folder path extraction correctly builds progressive paths from file names, handling nested directories appropriately.
web/src/lib/cross-seed-utils.ts (4)
12-12: LGTM: Path normalization is correct.The function properly handles cross-platform path separators, case normalization, and edge cases with optional chaining.
14-37: LGTM: Path and hardlink utilities are well-structured.The functions correctly handle directory containment checks and hardlink detection with appropriate guard clauses.
50-87: pendingQueryCount is unused and can be safely simplified or removed.The
pendingQueryCountfield returned fromuseLocalCrossSeedMatchesis not consumed by any component in the codebase. TorrentDetailsPanel (the only consumer of this hook) destructures onlymatchingTorrents,isLoadingMatches, andallInstances, leavingpendingQueryCountas dead code. The simplification to return0 | 1poses no risk to consuming components.Consider removing this field from the hook's return value if it's no longer needed.
92-152: CrossSeedTorrent field usage is safe—no changes needed.The consuming components (CrossSeedTable, TorrentDetailsPanel, CrossSeedWarning) have been verified and handle the defaulted field values appropriately:
- CrossSeedTable and CrossSeedWarning only access properly mapped fields (name, instanceName, tracker, state, progress, size, save_path)
- TorrentDetailsPanel accesses
dlspeedandupspeedwith a safety guard(match.upspeed > 0 || match.dlspeed > 0), so the defaulted 0 values won't cause incorrect behavior- The problematic fields (ratio, num_seeds, num_leechs) are never accessed on CrossSeedTorrent objects
- Fix OpenAPI response to return object with matches field, not bare array - Remove stale "infohash" from match type comments (only content_path, name, release exist) - Extract duplicate toCompatibleMatch to shared cross-seed-utils
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (2)
internal/services/crossseed/models.go (1)
292-313: LGTM - past review issue addressed.The types are well-structured with consistent JSON tags (snake_case). The
MatchTypecomment now correctly lists only the three implemented types ("content_path","name","release"), addressing the previous review feedback about the stale "infohash" reference. TheTrackerHealthfield properly aligns with the corresponding field insync_manager.go.internal/services/crossseed/service.go (1)
446-447: Unify content path normalization usingnormalizePathYou’re normalizing content paths twice with ad‑hoc logic:
normalizedContentPath := strings.ToLower(strings.ReplaceAll(sourceTorrent.ContentPath, "\\", "/")) candidateContentPath := strings.ToLower(strings.ReplaceAll(candidate.ContentPath, "\\", "/"))Given
normalizePathalready exists and is used elsewhere, it would be safer and less error‑prone to reuse it so both sides share identical normalization (slashes,./.., trailing separators):Suggested localized change
- // Normalize content path for comparison - normalizedContentPath := strings.ToLower(strings.ReplaceAll(sourceTorrent.ContentPath, "\\", "/")) + // Normalize content path for comparison (case-insensitive, normalized separators) + normalizedContentPath := strings.ToLower(normalizePath(sourceTorrent.ContentPath)) @@ - // Strategy 1: Same content path - candidateContentPath := strings.ToLower(strings.ReplaceAll(candidate.ContentPath, "\\", "/")) + // Strategy 1: Same content path (case-insensitive, normalized) + candidateContentPath := strings.ToLower(normalizePath(candidate.ContentPath))Also applies to: 503-507
🧹 Nitpick comments (1)
internal/services/crossseed/service.go (1)
420-493: Local match discovery flow looks good; confirm inclusion of incomplete torrentsThe instance enumeration, source lookup, cached‑torrent iteration, and match aggregation all look sane, and logging on per‑instance cache failures is appropriate.
One behavioral nuance to double‑check:
FindLocalMatchescurrently considers every cached torrent as a potential match, regardless ofProgressor state. If the UI is meant to treat “local matches” only as fully usable seeds, you may want to filter here (e.g.candidate.Progress >= 1.0 && !s.shouldSkipErroredTorrent(candidate.State)) or downstream; if showing partial/in‑progress matches is intentional, this is fine as‑is.
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
internal/services/crossseed/models.gointernal/services/crossseed/service.gointernal/web/swagger/openapi.yamlweb/src/hooks/useCrossSeedWarning.tsweb/src/lib/cross-seed-utils.ts
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
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.
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).
📚 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/hooks/useCrossSeedWarning.tsweb/src/lib/cross-seed-utils.ts
📚 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.tsinternal/services/crossseed/models.gointernal/services/crossseed/service.go
📚 Learning: 2025-12-03T18:11:08.682Z
Learnt from: finevan
Repo: autobrr/qui PR: 677
File: web/src/components/torrents/AddTorrentDialog.tsx:496-504
Timestamp: 2025-12-03T18:11:08.682Z
Learning: In the AddTorrentDialog component (web/src/components/torrents/AddTorrentDialog.tsx), temporary path settings (useDownloadPath/downloadPath) should be applied per-torrent rather than updating global instance preferences. The UI/UX is designed to suggest that these options apply to the individual torrent being added.
Applied to files:
web/src/hooks/useCrossSeedWarning.tsweb/src/lib/cross-seed-utils.ts
📚 Learning: 2025-11-21T21:11:50.633Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 625
File: internal/qbittorrent/sync_manager.go:1112-1123
Timestamp: 2025-11-21T21:11:50.633Z
Learning: In internal/qbittorrent/sync_manager.go, the GetCachedFilesBatch interface documentation (around line 39-40) should specify "uppercase hex" instead of "lowercase hex" to match the actual normalization practice used throughout the codebase (e.g., normalizeHash in internal/services/crossseed/service.go uses strings.ToUpper, and hash filtering uses uppercase).
Applied to files:
internal/services/crossseed/models.gointernal/services/crossseed/service.go
📚 Learning: 2025-11-28T22:21:20.730Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go:2415-2457
Timestamp: 2025-11-28T22:21:20.730Z
Learning: Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go
Learning: The determineSavePath function intentionally includes a contentLayout string parameter for future/content-layout branching and API consistency. Its presence is by design even if unused in the current body; do not flag as an issue in reviews.
Applied to files:
internal/services/crossseed/models.gointernal/services/crossseed/service.go
📚 Learning: 2025-12-28T18:44:10.496Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 876
File: internal/logstream/hub_test.go:188-192
Timestamp: 2025-12-28T18:44:10.496Z
Learning: In Go 1.25 (Aug 2025), use wg.Go(func()) to spawn a goroutine and automate the Add/Done lifecycle. Replace manual patterns like wg.Add(1); go func(){ defer wg.Done(); ... }() with wg.Go(func(){ ... }). Ensure the codebase builds with Go 1.25+ and apply this in relevant Go files (e.g., internal/logstream/hub_test.go). If targeting older Go versions, maintain the existing pattern.
Applied to files:
internal/services/crossseed/models.gointernal/services/crossseed/service.go
📚 Learning: 2025-12-11T08:40:01.329Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 746
File: internal/services/reannounce/service.go:480-481
Timestamp: 2025-12-11T08:40:01.329Z
Learning: In autobrr/qui's internal/services/reannounce/service.go, the hasHealthyTracker, getProblematicTrackers, and getHealthyTrackers functions intentionally match qbrr's lenient tracker health logic (skip unregistered trackers and check if any other tracker is healthy) rather than go-qbittorrent's strict isTrackerStatusOK logic (which treats unregistered as an immediate failure). For multi-tracker torrents, if one tracker is working, reannouncing won't help. The duplication of the health check logic across these three functions is acceptable as it's a simple one-liner, and extracting it would add unnecessary complexity.
Applied to files:
internal/services/crossseed/service.go
🧬 Code graph analysis (3)
web/src/hooks/useCrossSeedWarning.ts (2)
web/src/lib/api.ts (1)
api(1962-1962)web/src/lib/cross-seed-utils.ts (2)
normalizePath(12-12)toCompatibleMatch(92-152)
internal/services/crossseed/models.go (2)
web/src/types/index.ts (1)
Category(684-687)internal/qbittorrent/sync_manager.go (1)
TrackerHealth(85-85)
web/src/lib/cross-seed-utils.ts (2)
web/src/types/index.ts (2)
Torrent(470-527)LocalCrossSeedMatch(94-109)web/src/lib/api.ts (1)
api(1962-1962)
⏰ 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)
internal/services/crossseed/models.go (1)
15-20: LGTM!Well-defined unexported constants for match types. Using named constants instead of magic strings improves maintainability and reduces typo risks.
internal/web/swagger/openapi.yaml (2)
3163-3190: LGTM! Well-documented backend endpoint.The new local-matches endpoint is properly documented and follows REST conventions. Wrapping the matches array in an object (rather than returning a bare array) provides better extensibility for future additions.
5027-5077: LGTM! Comprehensive schema definition.The LocalCrossSeedMatch schema is well-documented with clear field descriptions. The matchType enum correctly reflects the three match strategies mentioned in the PR objectives (content_path, name, release).
web/src/hooks/useCrossSeedWarning.ts (2)
99-127: Backend-driven matching implementation looks correct.The migration from frontend fuzzy matching to backend API calls is well-implemented. The sequential API calls for each torrent could be slow when checking many torrents, but this is acceptable since the search is user-initiated and opt-in.
The use of
toCompatibleMatchensures the backend response is properly normalized to theCrossSeedTorrentformat expected by downstream code.
112-113: Path field normalization is correct.The switch from
save_path/content_pathtosavePath/contentPathcorrectly reflects the API layer's camelCase normalization. ThetoCompatibleMatchfunction then maps these back to snake_case for theCrossSeedTorrenttype, maintaining compatibility with existing code.Based on learnings, this follows the established pattern where the API layer normalizes backend responses to camelCase.
web/src/lib/cross-seed-utils.ts (4)
12-12: Minor formatting improvement to path normalization.The changes to
normalizePathare cosmetic: added optional chaining (?.), updated escape sequences, and switched to double quotes. The logic remains identical and the optional chaining makes it safer.
43-43: matchType enum correctly reflects backend strategies.The narrowed matchType enum (
"content_path" | "name" | "release") aligns with the PR objectives and matches the OpenAPI schema definition. This correctly represents the three match strategies implemented in the backend service.
50-87: Backend-driven hook implementation looks solid.The refactored
useLocalCrossSeedMatcheshook properly integrates with the backend API using react-query. Good practices:
- Appropriate caching with
staleTimeandgcTime- Disabled
refetchOnMountandrefetchOnWindowFocusfor performance- Conditional query execution when torrent exists
- Combined loading state from both queries
92-152: Comprehensive compatibility mapping function.The
toCompatibleMatchfunction thoroughly mapsLocalCrossSeedMatchtoCrossSeedTorrentformat, ensuring backward compatibility with code expecting the full Torrent interface. All required fields are provided with sensible defaults (0 for numbers, false for booleans, empty strings for strings).The path field mapping (
savePath → save_path,contentPath → content_path) correctly handles the camelCase-to-snake_case conversion for the Torrent type.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
internal/services/crossseed/service.go (1)
420-493: Solid backend local-matching implementation; consider pre-normalizing source hashThe overall flow (resolve source torrent, parse release once, normalize content path, then scan cached torrents per instance and classify via
determineLocalMatchType) looks correct and aligns with the PR intent to move “local matching” server-side and rely on metadata instead of fuzzy name similarity.One small, non-blocking tweak for readability and micro‑perf: you recompute
normalizeHashon the source hash for every candidate:if instance.ID == sourceInstanceID && normalizeHash(cached.Hash) == normalizeHash(sourceHash) {You could normalize the source hash once before the instance loop and reuse it:
normalizedSourceHash := normalizeHash(sourceHash) ... if instance.ID == sourceInstanceID && normalizeHash(cached.Hash) == normalizedSourceHash { continue }This avoids repeated trimming/lowercasing on the same input and makes the intent a bit clearer.
📜 Review details
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
internal/services/crossseed/service.go
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
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).
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-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:
internal/services/crossseed/service.go
📚 Learning: 2025-11-28T22:21:20.730Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go:2415-2457
Timestamp: 2025-11-28T22:21:20.730Z
Learning: Repo: autobrr/qui PR: 641
File: internal/services/crossseed/service.go
Learning: The determineSavePath function intentionally includes a contentLayout string parameter for future/content-layout branching and API consistency. Its presence is by design even if unused in the current body; do not flag as an issue in reviews.
Applied to files:
internal/services/crossseed/service.go
📚 Learning: 2025-11-21T21:11:50.633Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 625
File: internal/qbittorrent/sync_manager.go:1112-1123
Timestamp: 2025-11-21T21:11:50.633Z
Learning: In internal/qbittorrent/sync_manager.go, the GetCachedFilesBatch interface documentation (around line 39-40) should specify "uppercase hex" instead of "lowercase hex" to match the actual normalization practice used throughout the codebase (e.g., normalizeHash in internal/services/crossseed/service.go uses strings.ToUpper, and hash filtering uses uppercase).
Applied to files:
internal/services/crossseed/service.go
📚 Learning: 2025-12-11T08:40:01.329Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 746
File: internal/services/reannounce/service.go:480-481
Timestamp: 2025-12-11T08:40:01.329Z
Learning: In autobrr/qui's internal/services/reannounce/service.go, the hasHealthyTracker, getProblematicTrackers, and getHealthyTrackers functions intentionally match qbrr's lenient tracker health logic (skip unregistered trackers and check if any other tracker is healthy) rather than go-qbittorrent's strict isTrackerStatusOK logic (which treats unregistered as an immediate failure). For multi-tracker torrents, if one tracker is working, reannouncing won't help. The duplication of the health check logic across these three functions is acceptable as it's a simple one-liner, and extracting it would add unnecessary complexity.
Applied to files:
internal/services/crossseed/service.go
📚 Learning: 2025-12-28T18:44:10.496Z
Learnt from: s0up4200
Repo: autobrr/qui PR: 876
File: internal/logstream/hub_test.go:188-192
Timestamp: 2025-12-28T18:44:10.496Z
Learning: In Go 1.25 (Aug 2025), use wg.Go(func()) to spawn a goroutine and automate the Add/Done lifecycle. Replace manual patterns like wg.Add(1); go func(){ defer wg.Done(); ... }() with wg.Go(func(){ ... }). Ensure the codebase builds with Go 1.25+ and apply this in relevant Go files (e.g., internal/logstream/hub_test.go). If targeting older Go versions, maintain the existing pattern.
Applied to files:
internal/services/crossseed/service.go
🧬 Code graph analysis (1)
internal/services/crossseed/service.go (2)
internal/services/crossseed/models.go (2)
LocalMatchesResponse(293-295)LocalMatch(298-313)internal/qbittorrent/sync_manager.go (2)
TrackerHealth(85-85)CrossInstanceTorrentView(99-103)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Run tests
🔇 Additional comments (1)
internal/services/crossseed/service.go (1)
495-523: Local match-type strategy ordering and normalization look goodThe three-tier strategy (normalized
content_pathequality → case‑insensitive exactNamematch → release‑metadata comparison) is clear and deterministic, and using the sharednormalizePathhelper for both source and candidate content paths avoids the subtle divergence that was called out earlier. The doc comment now also accurately reflects the actual return values. I don’t see any issues here.
Frontend Levenshtein fuzzy matching (90% threshold) incorrectly matched releases with different versions. Replace with backend matching using the rls library for proper release metadata comparison.
Changes:
Summary by CodeRabbit
New Features
Improvements
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.