Skip to content

fix(scanners): reset orphaned processing media from deleted Radarr/Sonarr entries#2757

Open
fallenbagel wants to merge 4 commits intodevelopfrom
fallenbagel/fix/cleanup-orphaned-processing-media
Open

fix(scanners): reset orphaned processing media from deleted Radarr/Sonarr entries#2757
fallenbagel wants to merge 4 commits intodevelopfrom
fallenbagel/fix/cleanup-orphaned-processing-media

Conversation

@fallenbagel
Copy link
Copy Markdown
Collaborator

@fallenbagel fallenbagel commented Mar 24, 2026

Description

This PR addresses a long-standing issue where media stuck in "Processing" status would never get cleared if the corresponding entry was deleted from Radarr or Sonarr entirely, or if a Radarr movie was unmonitored without a file. It supersedes #817 with an approach that matches #2311 so it can handle both the scanners.

Previously, the Radarr scanner skipped unmonitored movies with no file entirely, meaning their status in Seerr was never updated. Now, these movies are still processed and their status is correctly reset to Unknown when they are no longer being actively tracked. The processing flag is also refined so that only monitored movies without a file are marked as Processing, rather than any movie without a file regardless of monitor state.

For media that has been completely removed from Radarr or Sonarr (as raised by #2756 (comment)), I have added a post-scan cleanup step that checks for any media still in Processing status that was not seen during the scan across all configured servers. If a movie or show is not found in any servarr, its status (and season statuses for shows) are now correctly reset to Unknown.

This cleanup only targets Processing status, so media marked as Available by Plex or Jellyfin scanners is never affected.

This PR builds on the season-level fix from #2311 which handles the case where a show still exists in Sonarr but specific seasons have zero episodes and are no longer processing.

TLDR; seasons cleaned up within an existing show, unmonitored Radarr movies, and media deleted entirely from either service are now properly processed.

How Has This Been Tested?

Note

I have only tested this via unit testing instead of manually by mocking radarr/sonarr api and movies/series. I have not pushed the unit tests to this PR yet. It should still work as expected though in theory.

Screenshots / Logs (if applicable)

Checklist:

  • I have read and followed the contribution guidelines.
  • Disclosed any use of AI (see our policy)
  • I have updated the documentation accordingly.
  • All new and existing tests passed.
  • Successful build pnpm build
  • Translation keys pnpm i18n:extract
  • Database migration (if required)
## Summary by CodeRabbit * **Bug Fixes** * Refined status transitions: media without files are marked UNKNOWN (not AVAILABLE) in more cases; deleted state is preserved; mediaAddedAt/changed flags only update when status actually changes; scanners no longer create new media when no file exists. * **New Features** * Per-run tracking (standard vs 4K) with automatic orphan cleanup of items stuck in PROCESSING when a full scan completes; cleanup is skipped if profile type wasn’t fully scanned. * **Tests** * Added comprehensive scanner tests covering monitored/unmonitored behavior and orphaned cleanup for standard and 4K.

@fallenbagel fallenbagel requested a review from Copilot March 24, 2026 02:11
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Track per-run scanned IDs for Radarr and Sonarr; pass hasFile into processing; adjust status-transition logic in the base scanner (including an early-return for new items with no file); and add orphaned-media cleanup that resets missing PROCESSING media to UNKNOWN when relevant servers were fully scanned.

Changes

Cohort / File(s) Summary
Base Scanner
server/lib/scanners/baseScanner.ts
Add optional hasFile?: boolean to ProcessOptions and default it in processMovie; refine existing-media status transitions (PROCESSING→UNKNOWN when hasFile is false), update mediaAddedAt/changedExisting only when status changes, and add early return for new media when processing === false && hasFile === false.
Radarr scanner & tests
server/lib/scanners/radarr/index.ts, server/lib/scanners/radarr/radarr.test.ts
Introduce per-run scannedTmdbIds/scanned4kTmdbIds and didScanStandard/didScan4k flags; always record seen tmdbIds and pass hasFile + processing: !hasFile && monitored to processMovie; remove prior skip for unmonitored+no-file items; add cleanupOrphanedMovies() to reset unseen MOVIE PROCESSING/status4kUNKNOWN when full-scan detected; add comprehensive tests for behaviors and cleanup.
Sonarr scanner & tests
server/lib/scanners/sonarr/index.ts, server/lib/scanners/sonarr/sonarr.test.ts
Add per-run scannedTvdbIds/scanned4kTvdbIds and didScanStandard/didScan4k flags; record series tvdbIds during processing and compute full-scan flags across servers; add cleanupOrphanedShows() to reset unseen TV PROCESSING/status4k (and season statuses) → UNKNOWN when full-scan detected; add tests covering orphan cleanup and 4K behavior.

Sequence Diagram(s)

sequenceDiagram
  participant Scanner as Scanner (Radarr / Sonarr)
  participant Remote as Remote Server(s)
  participant Repo as Media Repo (DB)
  participant Logger as Logger

  Scanner->>Remote: fetch items per server (includes is4k, monitored, hasFile)
  Remote-->>Scanner: list of items (tmdbId/tvdbId, monitored, hasFile)
  Scanner->>Scanner: record id in scanned set (standard or 4K)
  Scanner->>Repo: processMovie/processSeries(item, hasFile, processing)
  Repo-->>Scanner: save/update Media rows (status/status4k)
  Note right of Scanner: after all servers processed
  Scanner->>Repo: query Media where status/status4k = PROCESSING
  Repo-->>Scanner: candidate orphaned Media
  Scanner->>Scanner: filter candidates not in scanned sets
  Scanner->>Repo: set orphaned PROCESSING -> UNKNOWN and save
  Scanner->>Logger: log resets or skip if not fully scanned
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

bug, severity: medium

Suggested reviewers

  • gauthier-th
  • 0xSysR3ll

Poem

🐰 I hopped through lists both near and far,
Collected IDs like tiny stars.
When orphans hid, I nudged them home,
From PROCESSING to UNKNOWN they roam.
A tidy warren, neat and bright — hooray for order tonight! 🥕✨

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main change: fixing orphaned processing media by resetting them when they're deleted from Radarr/Sonarr entries.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves the Radarr and Sonarr scanners’ ability to recover from “orphaned” media that gets stuck in PROCESSING when items are deleted/untracked in *arr, and refines when Radarr marks movies as processing.

Changes:

  • Track scanned media IDs during Radarr/Sonarr runs and add a post-scan cleanup step to reset orphaned PROCESSING items back to UNKNOWN.
  • Adjust Radarr processing detection to only mark PROCESSING for monitored movies without files; additionally pass hasFile into the base movie processing logic.
  • Extend BaseScanner.processMovie with hasFile handling to allow resetting PROCESSINGUNKNOWN when an item is no longer actively processing.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
server/lib/scanners/sonarr/index.ts Adds scanned-id tracking and orphaned PROCESSING cleanup for shows/seasons (including 4K).
server/lib/scanners/radarr/index.ts Removes skip for unmonitored/no-file movies, refines processing, tracks scanned IDs, and adds orphaned PROCESSING cleanup (including 4K).
server/lib/scanners/baseScanner.ts Adds hasFile option and updates status transition logic for movies to support resetting orphaned processing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

@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: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/lib/scanners/radarr/index.ts`:
- Line 27: The code uses a single Set scannedTmdbIds for both standard and 4K
Radarr scans causing status and status4k cleanup to interfere; create two
separate sets (e.g., scannedTmdbIdsStandard and scannedTmdbIds4k) and use the
appropriate one when scanning/marking TMDB IDs and when deciding to transition
status or status4k out of PROCESSING; update all references where scannedTmdbIds
is read/modified (including the loops and cleanup logic around status/status4k
handling) so each Radarr profile maintains its own seen-ID set and add a unit
test that runs one-standard/one-4k scenario to assert independent behavior.
- Line 27: The instance-level Set scannedTmdbIds should be local to run():
remove the private scannedTmdbIds field and create a local const scannedTmdbIds
= new Set<number>() inside run(), accumulate seen IDs there, and collect the
service IDs actually scanned in that run; capture the sessionId returned from
BaseScanner.startRun() in run() and pass it into the cleanup call (instead of
using the instance state), and update the cleanup logic (the method invoked at
the end of run/cleanup block) to only operate on rows for the provided sessionId
and the specific service IDs and TMDB IDs collected for that run so concurrent
or subsequent runs cannot clear or flip unrelated PROCESSING rows.

In `@server/lib/scanners/sonarr/index.ts`:
- Line 35: The shared field scannedTvdbIds should be made local to the run
execution and the cleanup should be tied to the specific sessionId returned by
BaseScanner.startRun(); change the instance property scannedTvdbIds into a local
Set<number> inside run(), collect the TVDB ids scanned during that run, pass the
sessionId returned from startRun() into the cleanup call, and modify the cleanup
logic (the method invoked after run that currently clears PROCESSING → UNKNOWN)
to only operate on the specific service IDs present in that local Set for the
given sessionId (rather than the scanner-wide set) so overlapping or all-skipped
runs cannot reset unrelated shows/seasons; update all usages in run(), the
cleanup invocation, and any helper that referenced the instance scannedTvdbIds
accordingly.
- Line 35: The single Set scannedTvdbIds is being used for both standard and 4K
Sonarr hits which prevents independent cleanup; introduce two separate sets
(e.g. scannedTvdbIdsStandard and scannedTvdbIds4k), update all places that
read/write scannedTvdbIds to choose the correct set based on the profile
(standard vs 4k) and ensure cleanupOrphanedShows checks/marks shows and
season.status vs season.status4k against their respective set; update any loops
or helper code that currently references scannedTvdbIds (including the
scan/marking logic and the code paths referenced around the current
scannedTvdbIds usage) to use the appropriate per-profile set so standard and 4K
cleanups are independent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 666ba68d-dc94-49cd-bd9e-5a449b3381e1

📥 Commits

Reviewing files that changed from the base of the PR and between 865396f and b915f92.

📒 Files selected for processing (3)
  • server/lib/scanners/baseScanner.ts
  • server/lib/scanners/radarr/index.ts
  • server/lib/scanners/sonarr/index.ts

Copy link
Copy Markdown

@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.

♻️ Duplicate comments (2)
server/lib/scanners/radarr/index.ts (1)

78-83: ⚠️ Potential issue | 🟠 Major

Scope orphan cleanup to the Radarr services actually scanned.

didScanStandard/didScan4k only tell you that at least one server in that bucket ran. Lines 130-132 and 152-156 still load every movie stuck in PROCESSING for that profile, so a skipped/disabled server can have its rows reset to UNKNOWN as soon as another server of the same profile finishes. Please carry the scanned service IDs per profile and filter cleanup to those IDs before mutating status/status4k. A mixed scanned+skipped server regression test would cover this case.

Also applies to: 129-132, 151-156

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/lib/scanners/radarr/index.ts` around lines 78 - 83, The current
cleanup uses booleans didScanStandard/didScan4k (set via server4k) which only
indicate that some server in the profile ran, causing PROCESSING rows for
skipped/disabled servers to be reset; instead track the actual scanned service
IDs per profile (e.g., collect server.id for each server where server4k is true
into scanned4kIds and where false into scannedStandardIds) and when
loading/updating movies stuck in PROCESSING (the blocks that mutate
status/status4k) filter those queries/updates to only affect rows whose
service_id is in the corresponding scannedStandardIds or scanned4kIds set before
changing status/status4k so only movies for services that were actually scanned
are reset.
server/lib/scanners/sonarr/index.ts (1)

86-91: ⚠️ Potential issue | 🟠 Major

Restrict Sonarr cleanup to the services scanned in this run.

The new booleans only distinguish standard vs 4K. Lines 183-186 and 211-214 still query every show stuck in PROCESSING for that profile, so a skipped Sonarr server can have its show and season states reset to UNKNOWN when another server of the same profile finishes. Please track scanned service IDs per profile and filter the cleanup query to that set before mutating the parent row or its seasons.

Also applies to: 183-186, 211-214

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/lib/scanners/sonarr/index.ts` around lines 86 - 91, The cleanup
queries are currently scoped only by profile (standard vs 4K) and will reset
shows/seasons on Sonarr servers that were not scanned; add tracking of which
service IDs were actually scanned per profile (e.g., a map like
scannedServiceIdsByProfile with profileId => Set(serviceId)), populate it when
you process each server (use the existing server.id / server4k logic), and then
modify the cleanup queries that select shows in PROCESSING (the blocks that
mutate parent rows or seasons) to include a filter restricting to service_id IN
the tracked set for that profile so only shows from scanned services are reset;
apply the same change to both cleanup locations referenced in the comment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@server/lib/scanners/radarr/index.ts`:
- Around line 78-83: The current cleanup uses booleans didScanStandard/didScan4k
(set via server4k) which only indicate that some server in the profile ran,
causing PROCESSING rows for skipped/disabled servers to be reset; instead track
the actual scanned service IDs per profile (e.g., collect server.id for each
server where server4k is true into scanned4kIds and where false into
scannedStandardIds) and when loading/updating movies stuck in PROCESSING (the
blocks that mutate status/status4k) filter those queries/updates to only affect
rows whose service_id is in the corresponding scannedStandardIds or scanned4kIds
set before changing status/status4k so only movies for services that were
actually scanned are reset.

In `@server/lib/scanners/sonarr/index.ts`:
- Around line 86-91: The cleanup queries are currently scoped only by profile
(standard vs 4K) and will reset shows/seasons on Sonarr servers that were not
scanned; add tracking of which service IDs were actually scanned per profile
(e.g., a map like scannedServiceIdsByProfile with profileId => Set(serviceId)),
populate it when you process each server (use the existing server.id / server4k
logic), and then modify the cleanup queries that select shows in PROCESSING (the
blocks that mutate parent rows or seasons) to include a filter restricting to
service_id IN the tracked set for that profile so only shows from scanned
services are reset; apply the same change to both cleanup locations referenced
in the comment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c215b92d-13a4-46b0-a6ba-6a1a0c7597da

📥 Commits

Reviewing files that changed from the base of the PR and between b915f92 and 946866b.

📒 Files selected for processing (3)
  • server/lib/scanners/baseScanner.ts
  • server/lib/scanners/radarr/index.ts
  • server/lib/scanners/sonarr/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • server/lib/scanners/baseScanner.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/lib/scanners/sonarr/index.ts`:
- Around line 103-108: The Sonarr cleanup gate computes allStandardScanned and
all4kScanned using the wrong feature flag (this.enable4kMovie); update the two
filter predicates in the allStandardScanned and all4kScanned calculations to use
this.enable4kShow instead so the server subsets (this.servers filtered by is4k
and syncEnabled) match the Sonarr classification; keep the rest of the logic
(is4k, syncEnabled, didScanStandard/didScan4k usage) unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a898206a-3d2b-4bbd-88e1-f2adc35e0105

📥 Commits

Reviewing files that changed from the base of the PR and between 946866b and 8929632.

📒 Files selected for processing (2)
  • server/lib/scanners/radarr/index.ts
  • server/lib/scanners/sonarr/index.ts

@fallenbagel fallenbagel force-pushed the fallenbagel/fix/cleanup-orphaned-processing-media branch from 8929632 to 3d9939e Compare March 24, 2026 03:21
@bonswouar
Copy link
Copy Markdown

bonswouar commented Mar 24, 2026

Thanks for this cleaner implementation (and support for sonarr)!
I'll give it a try on my instance to see if it works as intended, but looks good to me
EDIT: I confirm it seems to work as expected!

@TorontoBaguette
Copy link
Copy Markdown

Hey,
Thanks a lot @fallenbagel for looking into this.

0xSysR3ll
0xSysR3ll previously approved these changes Mar 25, 2026
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (2)
server/lib/scanners/radarr/radarr.test.ts (1)

356-398: Add the symmetric skipped-4K-server guard test.

The standard cleanup path already proves that disabled servers prevent orphan resets, but the new 4K path doesn't have the same safety net. A regression in the all-4K-scanned gate could incorrectly clear status4k for movies that still exist on an unscanned 4K Radarr instance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/lib/scanners/radarr/radarr.test.ts` around lines 356 - 398, Add a
symmetric test that ensures a disabled/unscanned 4K Radarr server prevents
orphaned 4K resets: create a Media with tmdbId (e.g., 962) and status4k =
MediaStatus.PROCESSING, call configureRadarr([{ syncEnabled: false, is4k: true
}]) (or simulate an unscanned 4K server), set getMoviesImpl = async () => [] and
run radarrScanner.run(), then assert the saved Media (via
getRepository(Media).findOneOrFail where tmdbId) still has status4k ===
MediaStatus.PROCESSING; mirror the pattern used in the existing 4k tests to
ensure the all-4K-scanned guard is covered.
server/lib/scanners/sonarr/sonarr.test.ts (1)

452-521: Tighten the 4K cleanup coverage.

This block is still missing the skipped-4K-server safety case, and the second scenario only checks season rows. A regression where orphan cleanup stops resetting media.status4k for mixed season states, or where a disabled 4K server is ignored, would slip through here.

Small assertion to add in the second case
       const updated = await mediaRepository.findOneOrFail({
         where: { tmdbId: 1031 },
         relations: ['seasons'],
       });
+      assert.strictEqual(updated.status4k, MediaStatus.UNKNOWN);
       const s1 = updated.seasons.find((s) => s.seasonNumber === 1);
       const s2 = updated.seasons.find((s) => s.seasonNumber === 2);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/lib/scanners/sonarr/sonarr.test.ts` around lines 452 - 521, Add two
checks: in the existing test "does not reset 4k AVAILABLE season when show is
orphaned" also assert the parent media.status4k is updated/reset as expected
(e.g., assert.strictEqual(updated.status4k, MediaStatus.UNKNOWN)) so
mixed-season state regression is caught; and add a new test case under the "4k
orphaned show cleanup" describe that configures a Sonarr server with 4K disabled
(configureSonarr with syncEnabled: true, is4k: false or syncEnabled: false) and
a totally orphaned show (getSeriesImpl = async () => []), then run
sonarrScanner.run() and assert the media.status4k and seasons' status4k are NOT
reset (remain PROCESSING/AVAILABLE as initially set) to cover the
skipped-4K-server safety case.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@server/lib/scanners/radarr/radarr.test.ts`:
- Around line 356-398: Add a symmetric test that ensures a disabled/unscanned 4K
Radarr server prevents orphaned 4K resets: create a Media with tmdbId (e.g.,
962) and status4k = MediaStatus.PROCESSING, call configureRadarr([{ syncEnabled:
false, is4k: true }]) (or simulate an unscanned 4K server), set getMoviesImpl =
async () => [] and run radarrScanner.run(), then assert the saved Media (via
getRepository(Media).findOneOrFail where tmdbId) still has status4k ===
MediaStatus.PROCESSING; mirror the pattern used in the existing 4k tests to
ensure the all-4K-scanned guard is covered.

In `@server/lib/scanners/sonarr/sonarr.test.ts`:
- Around line 452-521: Add two checks: in the existing test "does not reset 4k
AVAILABLE season when show is orphaned" also assert the parent media.status4k is
updated/reset as expected (e.g., assert.strictEqual(updated.status4k,
MediaStatus.UNKNOWN)) so mixed-season state regression is caught; and add a new
test case under the "4k orphaned show cleanup" describe that configures a Sonarr
server with 4K disabled (configureSonarr with syncEnabled: true, is4k: false or
syncEnabled: false) and a totally orphaned show (getSeriesImpl = async () =>
[]), then run sonarrScanner.run() and assert the media.status4k and seasons'
status4k are NOT reset (remain PROCESSING/AVAILABLE as initially set) to cover
the skipped-4K-server safety case.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 59b29277-0e1c-4b5b-9475-2e9ade596173

📥 Commits

Reviewing files that changed from the base of the PR and between 3d9939e and 1966705.

📒 Files selected for processing (2)
  • server/lib/scanners/radarr/radarr.test.ts
  • server/lib/scanners/sonarr/sonarr.test.ts

@fallenbagel fallenbagel self-assigned this Mar 28, 2026
@fallenbagel fallenbagel modified the milestones: Next Release, v3.2.0 Mar 28, 2026
@seerr-team seerr-team deleted a comment from github-actions bot Apr 2, 2026
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.

5 participants