Skip to content

Fix duplicated grabbed items reported for Sonarr on season packs#549

Merged
Flaminel merged 4 commits into
mainfrom
fix_duplicated_grabbed_items
Apr 5, 2026
Merged

Fix duplicated grabbed items reported for Sonarr on season packs#549
Flaminel merged 4 commits into
mainfrom
fix_duplicated_grabbed_items

Conversation

@Flaminel

@Flaminel Flaminel commented Apr 5, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

Fix duplicate grabbed items for Sonarr season packs

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Eliminates duplicate grabbed items in Sonarr season pack tracking
• Groups queue records by DownloadId and selects first occurrence
• Prevents duplicate reporting when processing season pack downloads
Diagram
flowchart LR
  A["Queue Records"] -- "Filter by ItemType & IDs" --> B["Filtered Records"]
  B -- "GroupBy DownloadId" --> C["Grouped Records"]
  C -- "Select First from Each Group" --> D["Deduplicated Records"]
  D -- "Map to Result Objects" --> E["Unique Grabbed Items"]
Loading

Grey Divider

File Changes

1. code/backend/Cleanuparr.Infrastructure/Features/Jobs/SeekerCommandMonitor.cs 🐞 Bug fix +2/-0

Deduplicate grabbed items by DownloadId grouping

• Added .GroupBy(r => r.DownloadId) to group queue records by download identifier
• Added .Select(g => g.First()) to select only the first record from each group
• Prevents duplicate grabbed items from being reported for Sonarr season pack downloads

code/backend/Cleanuparr.Infrastructure/Features/Jobs/SeekerCommandMonitor.cs


Grey Divider

Qodo Logo

@Flaminel

Flaminel commented Apr 5, 2026

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review
@greptileai review

@qodo-code-review

qodo-code-review Bot commented Apr 5, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX Issues (0)

Grey Divider


Action required

1. Collapses blank DownloadIds🐞 Bug ≡ Correctness
Description
InspectDownloadQueueAsync now groups matched queue records by DownloadId and keeps only the first
record per group, so multiple records with null/empty DownloadId collapse into a single reported
grabbed item. The codebase explicitly treats empty DownloadId as invalid elsewhere, so this change
can silently under-report grabbed items instead of skipping/handling them explicitly.
Code

code/backend/Cleanuparr.Infrastructure/Features/Jobs/SeekerCommandMonitor.cs[R205-206]

+                        .GroupBy(r => r.DownloadId)
+                        .Select(g => g.First())
Evidence
The new grouping in SeekerCommandMonitor deduplicates purely by DownloadId. Separately,
ArrClient.IsRecordValid shows DownloadId can be null/empty and should be skipped; other
queue-processing code (QueueCleaner) checks IsRecordValid before acting on grouped DownloadId
records, avoiding the “collapse all empty IDs into one” behavior.

code/backend/Cleanuparr.Infrastructure/Features/Jobs/SeekerCommandMonitor.cs[197-213]
code/backend/Cleanuparr.Infrastructure/Features/Arr/ArrClient.cs[198-209]
code/backend/Cleanuparr.Infrastructure/Features/Jobs/QueueCleaner.cs[104-117]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`InspectDownloadQueueAsync` deduplicates queue records with `.GroupBy(r => r.DownloadId).Select(g => g.First())`. If `DownloadId` is null/empty, all such records end up in the same group and only one record is reported as grabbed.
### Issue Context
Elsewhere, `ArrClient.IsRecordValid()` explicitly treats null/empty `DownloadId` as invalid and skips processing; `QueueCleaner` checks validity before acting on `DownloadId` groups.
### Fix Focus Areas
- code/backend/Cleanuparr.Infrastructure/Features/Jobs/SeekerCommandMonitor.cs[197-213]
### What to change
In `InspectDownloadQueueAsync`, before grouping:
- Either filter out invalid records (e.g., `Where(r => !string.IsNullOrEmpty(r.DownloadId))`),
- Or use a grouping key that does not collapse missing IDs (e.g., fallback to `r.Id` when `DownloadId` is empty), depending on desired reporting behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

2. Case-sensitive hash grouping🐞 Bug ⚙ Maintainability
Description
The new deduplication groups by DownloadId using the default (case-sensitive) string comparer. Since
DownloadId is used as the download hash in the system and hashes are compared case-insensitively
elsewhere, this grouping can treat the same hash with different casing as different downloads,
undermining consistent behavior.
Code

code/backend/Cleanuparr.Infrastructure/Features/Jobs/SeekerCommandMonitor.cs[205]

+                        .GroupBy(r => r.DownloadId)
Evidence
The dedupe uses default GroupBy string equality (case-sensitive). In other parts of the repo, the
download identifier is treated as a hash and compared case-insensitively (e.g., qBittorrent hash
comparisons), and DownloadId is directly set/queried as a hash value.

code/backend/Cleanuparr.Infrastructure/Features/Jobs/SeekerCommandMonitor.cs[197-213]
code/backend/Cleanuparr.Infrastructure/Extensions/QBitExtensions.cs[8-24]
code/backend/Cleanuparr.Infrastructure/Services/RuleEvaluator.cs[206-213]
code/backend/Cleanuparr.Infrastructure/Events/EventPublisher.cs[360-372]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`GroupBy(r => r.DownloadId)` uses case-sensitive string equality by default, which is inconsistent with other parts of the codebase where hashes/DownloadId are treated case-insensitively.
### Issue Context
`DownloadId` is used as a hash identifier (e.g., compared to `torrent.Hash`, emitted as `DownloadId = hash`). Torrent hash comparisons elsewhere use `InvariantCultureIgnoreCase`.
### Fix Focus Areas
- code/backend/Cleanuparr.Infrastructure/Features/Jobs/SeekerCommandMonitor.cs[197-213]
### What to change
Use an explicit comparer for grouping/deduplication, e.g.:
- `.GroupBy(r => r.DownloadId, StringComparer.OrdinalIgnoreCase)` (or `InvariantCultureIgnoreCase` for consistency with existing code)
If you also address missing `DownloadId`, ensure the comparer approach doesn’t accidentally merge null/empty IDs (filter or fallback key first).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@codecov

codecov Bot commented Apr 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.66667% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...frastructure/Features/Jobs/SeekerCommandMonitor.cs 66.66% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@greptile-apps

greptile-apps Bot commented Apr 5, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a bug where Sonarr season packs were reported as multiple grabbed items — one per episode — rather than a single grab. The root cause is that Sonarr creates one QueueRecord per episode within a season pack, all sharing the same DownloadId (torrent hash / NZB identifier). Two LINQ operators are added to the chain in InspectDownloadQueueAsync to deduplicate before projection:

  • Correct placement: .GroupBy(r => r.DownloadId).Select(g => g.First()) is inserted after the .Where(...) filter and before the .Select(r => new { r.Title, r.Status, r.Protocol }) projection — the right spot to eliminate duplicates before reporting.
  • No null risk: DownloadId is declared required string in QueueRecord, so it is non-nullable and grouping by it is safe.
  • Correct field selection: All episode records for a season pack share the same Title, Status, and Protocol, so taking g.First() for the projection is accurate.
  • Test gap: No test file exists for SeekerCommandMonitor; adding a regression test for the deduplication behaviour is advisable."

Confidence Score: 5/5

Safe to merge — the fix is minimal, surgical, and logically correct with no regression risk.

Two-line LINQ addition on a non-nullable required field. The deduplication logic is straightforward and has no edge cases that would cause incorrect behaviour compared to the pre-fix code.

No files require special attention; the change is confined to a single LINQ chain in SeekerCommandMonitor.cs.

Important Files Changed

Filename Overview
code/backend/Cleanuparr.Infrastructure/Features/Jobs/SeekerCommandMonitor.cs Adds DownloadId-based deduplication to fix duplicated season pack items being reported as multiple grabbed downloads in Sonarr

Sequence Diagram

sequenceDiagram
    participant SCM as SeekerCommandMonitor
    participant Arr as ArrClient
    participant LINQ as LINQ Pipeline

    SCM->>Arr: GetQueueItemsAsync(instance, page=1)
    Arr-->>SCM: QueueListResponse (N records)
    Note over SCM,LINQ: For each tracker (Sonarr season pack)
    SCM->>LINQ: .Where(SeriesId && SeasonNumber match)
    LINQ-->>SCM: Filtered records (duplicates per episode)
    SCM->>LINQ: .GroupBy(r => r.DownloadId).Select(g => g.First())
    LINQ-->>SCM: Deduplicated (1 record per unique download)
    SCM->>LINQ: .Select(r => new { Title, Status, Protocol })
    LINQ-->>SCM: grabbedItems (correct count)
    SCM-->>SCM: Log and accumulate allGrabbedItems
Loading

Reviews (1): Last reviewed commit: "fixed duplicated grabbed items reported ..." | Re-trigger Greptile

Comment on lines +205 to +206
.GroupBy(r => r.DownloadId)
.Select(g => g.First())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 No test coverage for deduplication logic

The SeekerCommandMonitor class has no corresponding test file (SeekerCommandMonitorTests.cs does not exist). Given this bug fix introduces a specific deduplication step, a regression test covering the season pack scenario — where multiple QueueRecords share the same DownloadId — would help guard against future regressions. The test could mock IArrClientFactory to return a queue with several records sharing a DownloadId and assert that InspectDownloadQueueAsync reports only one grabbed item.

@Flaminel Flaminel merged commit 88aa71c into main Apr 5, 2026
12 checks passed
@Flaminel Flaminel deleted the fix_duplicated_grabbed_items branch April 5, 2026 19:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant