Skip to content

refactor(cross-seed): move local matching to backend#1069

Merged
s0up4200 merged 3 commits intodevelopfrom
fix/cross-seed-local-matching
Jan 3, 2026
Merged

refactor(cross-seed): move local matching to backend#1069
s0up4200 merged 3 commits intodevelopfrom
fix/cross-seed-local-matching

Conversation

@s0up4200
Copy link
Collaborator

@s0up4200 s0up4200 commented Jan 3, 2026

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

Summary by CodeRabbit

  • New Features

    • New backend endpoint to find local cross-seed matches for a selected torrent across instances.
    • Frontend API client and types added to consume local match results.
  • Improvements

    • Cross-seed matching moved to backend for simpler, more reliable results and UI flows.
    • Frontend hook renamed/updated to use backend-driven matches; match types narrowed to "content_path", "name", "release".
    • UI labels updated to surface "Release" matches.
  • Documentation

    • API docs updated with the new endpoint and response schema.

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

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
@s0up4200 s0up4200 added this to the v1.12.0 milestone Jan 3, 2026
@s0up4200 s0up4200 added the enhancement New feature or request label Jan 3, 2026
@netlify
Copy link

netlify bot commented Jan 3, 2026

Deploy Preview for getqui canceled.

Name Link
🔨 Latest commit ae077fa
🔍 Latest deploy log https://app.netlify.com/projects/getqui/deploys/69596da4aeef370008061182

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 3, 2026

Walkthrough

Adds 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

Cohort / File(s) Summary
Backend API Handler
internal/api/handlers/crossseed.go
Added GetLocalMatches endpoint and parseTorrentParams helper; refactored AnalyzeTorrentForSearch and GetAsyncFilteringStatus to use the helper.
Cross-seed Service
internal/services/crossseed/service.go
Added FindLocalMatches(ctx, sourceInstanceID, sourceHash) and determineLocalMatchType to scan instances and classify local matches (content_path, name, release).
Service Models
internal/services/crossseed/models.go
Added LocalMatchesResponse and LocalMatch types and match-type constants. ⚠️ Duplicated type definitions noted in file.
QBittorrent Sync / Cache
internal/qbittorrent/sync_manager.go
Initialized validatedTrackerMapping in NewSyncManager; adjusted GetCachedInstanceTorrents to use cached tracker-health counts when enrichment is absent.
Swagger / API Spec
internal/web/swagger/openapi.yaml
Documented new GET /api/cross-seed/torrents/{instanceID}/{hash}/local-matches and added LocalCrossSeedMatch schema; updated CrossInstanceFilterOptions.
Frontend Types
web/src/types/index.ts
Added LocalCrossSeedMatch interface with metadata and matchType union (`"content_path"
Frontend API
web/src/lib/api.ts
Added getLocalCrossSeedMatches(instanceId, hash) returning mapped LocalCrossSeedMatch[].
Frontend Cross-seed Utilities
web/src/lib/cross-seed-utils.ts
Replaced client-side multi-instance matching with backend-driven flow; added toCompatibleMatch and useLocalCrossSeedMatches; narrowed CrossSeedTorrent.matchType.
Frontend Hooks
web/src/hooks/useCrossSeedFilter.ts, web/src/hooks/useCrossSeedWarning.ts
Switched to backend getLocalCrossSeedMatches; removed frontend orchestration/timeouts and deep file comparisons; adapted normalization to backend shape.
Frontend Components
web/src/components/torrents/TorrentDetailsPanel.tsx, web/src/components/torrents/details/CrossSeedTable.tsx
Updated to use useLocalCrossSeedMatches; adjusted match-type labeling (added "Release", removed "Info Hash"/"Save Path") and minor formatting 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...]
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

web, bugfix

Poem

🐰 I hopped through code with joyful skips,

Found matching crumbs on local disks,
Backend now scouts each cozy den,
Frontend beams and hops again,
Carrots for tests and tiny flips! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'refactor(cross-seed): move local matching to backend' directly and clearly summarizes the main objective of the PR: shifting cross-seed matching logic from frontend to backend.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/cross-seed-local-matching

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.

❤️ Share

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

@github-actions
Copy link
Contributor

github-actions bot commented Jan 3, 2026

PR Review Summary

Reviewed areas: Backend API, Integration (cross-seed service), Frontend (React hooks & utils), Performance, Documentation

Passed Checks

  • Security: No security concerns detected - API endpoints properly validate input parameters
  • Frontend: Clean React 19 patterns, proper hook usage, no TypeScript any abuse
  • Integration: Proper error handling for cross-seed matching logic

Recommendations

Performance

  1. Backend - Potential N+1 Query Pattern internal/services/crossseed/service.go:450-456

    • The FindLocalMatches method iterates through all instances and calls GetCachedInstanceTorrents for each
    • Consider pre-fetching all instance torrents in a single operation if you have many instances
    • Current implementation uses cached data which mitigates the issue, but worth monitoring
  2. Backend - Memory Allocation internal/services/crossseed/service.go:458-489

    • The loop appends to matches slice without pre-allocation
    • For large torrent collections, consider pre-allocating: matches := make([]LocalMatch, 0, estimatedSize)
  3. Backend - String Operations in Loop internal/services/crossseed/service.go:504,511,517

    • Multiple string transformations (ToLower, ReplaceAll, TrimSpace) happen for every candidate torrent
    • Consider caching normalized strings if the same torrent is compared multiple times

Code Quality

  1. Frontend - Duplicate Function web/src/lib/cross-seed-utils.ts:92-152 and web/src/hooks/useCrossSeedWarning.ts:41-101

    • The toCompatibleMatch function is duplicated in both files with identical implementation
    • Recommendation: Extract to a shared location to follow DRY principle
  2. Backend - Error Context internal/services/crossseed/service.go:453-456

    • When GetCachedInstanceTorrents fails, the error is logged but processing continues
    • Consider adding more context about which instance failed and why (already has instanceID)

Documentation Updates Needed

  • Update documentation/docs/features/cross-seed/*.md - The matching strategy has fundamentally changed from frontend Levenshtein fuzzy matching (90% threshold) to backend rls library metadata matching. Document:
    • New matching strategies: content_path, name, release
    • Improved accuracy compared to old fuzzy matching
    • How the rls library parses release metadata
  • Update documentation/docs/api/overview.md - New endpoint added: GET /api/cross-seed/torrents/{instanceID}/{hash}/local-matches

Minor Issues

  1. Backend - Inconsistent Comment internal/services/crossseed/service.go:496

    • Comment mentions "infohash" as a match type, but the code never returns it (constants only define content_path, name, release)
    • Fix: Update comment to remove "infohash" or implement infohash matching
  2. Backend - Tracker Health Fix Quality internal/qbittorrent/sync_manager.go:1173-1189

    • Good fix to populate tracker health in cached instance torrents
    • Follows proper fallback pattern (enriched data → cached hash sets)

Positive Notes

✅ Excellent refactoring - moving complex matching logic from frontend to backend is the right architectural choice
✅ Proper use of rls library for release metadata parsing eliminates false positives
✅ Good API design with clear separation of concerns
✅ Frontend properly consumes the new API with backward-compatible types
✅ Comprehensive OpenAPI documentation added


Automated review by Claude Code

@github-actions github-actions bot added area/backend Backend changes area/frontend Frontend changes needs-docs Documentation updates needed labels Jan 3, 2026
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

🧹 Nitpick comments (4)
web/src/hooks/useCrossSeedWarning.ts (1)

188-191: Redundant instanceName override.

toCompatibleMatch(match) already sets instanceName: m.instanceName on line 55. Since matches are filtered to only include those from the current instanceId (line 172), overwriting instanceName here 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:

  1. If the backend returns an unexpected value, the UI will silently show "Name" and "Same torrent name" which may be misleading.
  2. The assertion hides potential type mismatches from TypeScript.

If the CrossSeedTorrent type from toCompatibleMatch already types matchType correctly, 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 in FindLocalMatches

The core flow looks solid, but a few consistency and robustness issues are worth addressing:

  1. Missing argument validation / instance existence check

Other service entrypoints (e.g., SearchTorrentMatches) explicitly validate:

  • instanceID > 0
  • non‑blank hash
  • instance exists via instanceStore.Get

FindLocalMatches skips this and goes straight to GetTorrents, so:

  • sourceInstanceID <= 0 or an unknown instance will surface as a generic "failed to get source torrent" instead of a clear ErrInvalidRequest/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.

  1. Defensive guard for nil instances from List

You iterate instances without 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. If List ever returns a nil, this will panic. Very cheap to guard and keeps this method in line with existing patterns.

  1. Path normalization for ContentPath

You 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.Clean semantics already used elsewhere via normalizePath

Using 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).

  1. Minor consistency nit: filter constant

You currently pass Filter: "all" to GetTorrents. Most of this file uses qbt.TorrentFilterOptions{Hashes: []string{hash}} (without Filter) or the qbt.TorrentFilterAll constant. 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

📥 Commits

Reviewing files that changed from the base of the PR and between c59b809 and 91ade91.

📒 Files selected for processing (12)
  • internal/api/handlers/crossseed.go
  • internal/qbittorrent/sync_manager.go
  • internal/services/crossseed/models.go
  • internal/services/crossseed/service.go
  • internal/web/swagger/openapi.yaml
  • web/src/components/torrents/TorrentDetailsPanel.tsx
  • web/src/components/torrents/details/CrossSeedTable.tsx
  • web/src/hooks/useCrossSeedFilter.ts
  • web/src/hooks/useCrossSeedWarning.ts
  • web/src/lib/api.ts
  • web/src/lib/cross-seed-utils.ts
  • web/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.ts
  • web/src/hooks/useCrossSeedFilter.ts
  • web/src/hooks/useCrossSeedWarning.ts
  • web/src/components/torrents/details/CrossSeedTable.tsx
  • web/src/lib/api.ts
  • web/src/components/torrents/TorrentDetailsPanel.tsx
  • web/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.ts
  • web/src/hooks/useCrossSeedWarning.ts
  • internal/services/crossseed/models.go
  • internal/services/crossseed/service.go
  • web/src/components/torrents/TorrentDetailsPanel.tsx
  • internal/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.ts
  • web/src/components/torrents/TorrentDetailsPanel.tsx
  • web/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.go
  • 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/models.go
  • internal/qbittorrent/sync_manager.go
  • internal/services/crossseed/service.go
  • internal/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 validatedTrackerMapping field as an empty map (line 228), which will be populated by the background refreshTrackerHealthCounts loop 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 GetCachedInstanceTorrents improve the health determination flow by:

  1. Retrieving cached health counts upfront (line 1174)
  2. Using a local view variable to build each TorrentView (line 1178)
  3. 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 LocalCrossSeedMatch interface properly defines the structure for release metadata-based matching results from the backend. The field types and optional trackerHealth are 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 getLocalCrossSeedMatches method 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 LocalCrossSeedMatch type

The 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 parseTorrentParams helper 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 ok boolean 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 AnalyzeTorrentForSearch and GetAsyncFilteringStatus now correctly delegate parameter extraction and validation to parseTorrentParams, reducing duplication and ensuring consistent error handling.

Also applies to: 426-428


446-477: LGTM! Handler follows established patterns.

The new GetLocalMatches handler is well-implemented:

  • Uses the parseTorrentParams helper 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 toCompatibleMatch helper cleanly bridges the new LocalCrossSeedMatch backend response to the existing CrossSeedTorrent interface. The default values for unused Torrent fields are appropriate for this use case.

One minor note: total_size on line 94 is set to m.size, which correctly mirrors the size field - 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.savePath and match.contentPath (camelCase) aligns with the LocalCrossSeedMatch interface 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-matches endpoint 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 LocalCrossSeedMatch schema correctly defines all fields with appropriate types, including:

  • trackerHealth as optional with proper enum constraint
  • matchType with the three valid values matching the backend constants

This 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 SavePath and ContentPath fields use save_path and content_path JSON tags (snake_case), which correctly match the OpenAPI schema's savePath and contentPath properties 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 useLocalCrossSeedMatches hook which calls the backend API for local cross-seed matching instead of performing client-side matching.


142-143: LGTM!

The hook invocation correctly passes isCrossSeedTabActive as 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 where matchingTorrentsKeys is an empty string, ensuring the result is an empty array rather than [""].


908-931: LGTM!

The explicit fragment wrapper correctly groups the Separator and grid div as 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 pendingQueryCount field returned from useLocalCrossSeedMatches is not consumed by any component in the codebase. TorrentDetailsPanel (the only consumer of this hook) destructures only matchingTorrents, isLoadingMatches, and allInstances, leaving pendingQueryCount as dead code. The simplification to return 0 | 1 poses 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 dlspeed and upspeed with 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

@s0up4200 s0up4200 changed the title fix(cross-seed): move local matching to backend using rls library refactor(cross-seed): move local matching to backend Jan 3, 2026
- 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
@s0up4200 s0up4200 removed the needs-docs Documentation updates needed label Jan 3, 2026
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 (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 MatchType comment now correctly lists only the three implemented types ("content_path", "name", "release"), addressing the previous review feedback about the stale "infohash" reference. The TrackerHealth field properly aligns with the corresponding field in sync_manager.go.

internal/services/crossseed/service.go (1)

446-447: Unify content path normalization using normalizePath

You’re normalizing content paths twice with ad‑hoc logic:

normalizedContentPath := strings.ToLower(strings.ReplaceAll(sourceTorrent.ContentPath, "\\", "/"))
candidateContentPath := strings.ToLower(strings.ReplaceAll(candidate.ContentPath, "\\", "/"))

Given normalizePath already 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 torrents

The 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: FindLocalMatches currently considers every cached torrent as a potential match, regardless of Progress or 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

📥 Commits

Reviewing files that changed from the base of the PR and between 91ade91 and 75edf0d.

📒 Files selected for processing (5)
  • internal/services/crossseed/models.go
  • internal/services/crossseed/service.go
  • internal/web/swagger/openapi.yaml
  • web/src/hooks/useCrossSeedWarning.ts
  • web/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.ts
  • web/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.ts
  • internal/services/crossseed/models.go
  • internal/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.ts
  • web/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.go
  • 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/models.go
  • 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/models.go
  • 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
🧬 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 toCompatibleMatch ensures the backend response is properly normalized to the CrossSeedTorrent format expected by downstream code.


112-113: Path field normalization is correct.

The switch from save_path/content_path to savePath/contentPath correctly reflects the API layer's camelCase normalization. The toCompatibleMatch function then maps these back to snake_case for the CrossSeedTorrent type, 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 normalizePath are 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 useLocalCrossSeedMatches hook properly integrates with the backend API using react-query. Good practices:

  • Appropriate caching with staleTime and gcTime
  • Disabled refetchOnMount and refetchOnWindowFocus for performance
  • Conditional query execution when torrent exists
  • Combined loading state from both queries

92-152: Comprehensive compatibility mapping function.

The toCompatibleMatch function thoroughly maps LocalCrossSeedMatch to CrossSeedTorrent format, 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
internal/services/crossseed/service.go (1)

420-493: Solid backend local-matching implementation; consider pre-normalizing source hash

The 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 normalizeHash on 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

📥 Commits

Reviewing files that changed from the base of the PR and between 75edf0d and ae077fa.

📒 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 good

The three-tier strategy (normalized content_path equality → case‑insensitive exact Name match → release‑metadata comparison) is clear and deterministic, and using the shared normalizePath helper 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/backend Backend changes area/frontend Frontend changes cross-seed enhancement New feature or request performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant