fix(scanners): reset orphaned processing media from deleted Radarr/Sonarr entries#2757
fix(scanners): reset orphaned processing media from deleted Radarr/Sonarr entries#2757fallenbagel wants to merge 4 commits intodevelopfrom
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughTrack per-run scanned IDs for Radarr and Sonarr; pass Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ 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. Comment |
There was a problem hiding this comment.
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
PROCESSINGitems back toUNKNOWN. - Adjust Radarr processing detection to only mark
PROCESSINGfor monitored movies without files; additionally passhasFileinto the base movie processing logic. - Extend
BaseScanner.processMoviewithhasFilehandling to allow resettingPROCESSING→UNKNOWNwhen 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.
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
server/lib/scanners/baseScanner.tsserver/lib/scanners/radarr/index.tsserver/lib/scanners/sonarr/index.ts
There was a problem hiding this comment.
♻️ Duplicate comments (2)
server/lib/scanners/radarr/index.ts (1)
78-83:⚠️ Potential issue | 🟠 MajorScope orphan cleanup to the Radarr services actually scanned.
didScanStandard/didScan4konly tell you that at least one server in that bucket ran. Lines 130-132 and 152-156 still load every movie stuck inPROCESSINGfor that profile, so a skipped/disabled server can have its rows reset toUNKNOWNas soon as another server of the same profile finishes. Please carry the scanned service IDs per profile and filter cleanup to those IDs before mutatingstatus/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 | 🟠 MajorRestrict 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
PROCESSINGfor that profile, so a skipped Sonarr server can have its show and season states reset toUNKNOWNwhen 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
📒 Files selected for processing (3)
server/lib/scanners/baseScanner.tsserver/lib/scanners/radarr/index.tsserver/lib/scanners/sonarr/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- server/lib/scanners/baseScanner.ts
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
server/lib/scanners/radarr/index.tsserver/lib/scanners/sonarr/index.ts
8929632 to
3d9939e
Compare
|
Thanks for this cleaner implementation (and support for sonarr)! |
|
Hey, |
There was a problem hiding this comment.
🧹 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
status4kfor 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.status4kfor 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
📒 Files selected for processing (2)
server/lib/scanners/radarr/radarr.test.tsserver/lib/scanners/sonarr/sonarr.test.ts
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
Unknownwhen they are no longer being actively tracked. The processing flag is also refined so that only monitored movies without a file are marked asProcessing, 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
Processingstatus 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 toUnknown.This cleanup only targets
Processingstatus, so media marked asAvailableby 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
- Translation keys
- 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.pnpm buildpnpm i18n:extract