fix(sonarr): don't require never-downloaded seasons to be unmonitored before show cleanup#2897
Conversation
|
@daviddanko @andrew-kennedy Is this what you had in mind? |
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as outdated.
This comment was marked as outdated.
|
Looks good to me, I think. I have shows scheduled for complete deletion in 2, 6 and 8 days. I can report back. Will you push it to stable in that timespan or should I use nightly/development? |
|
@daviddanko Please use the PR tag. Thanks! 🙏 |
|
Sure thing. I decreased the actions taken by a day, so one show will be handled tmrw early morning. I'll keep you posted @enoch85. |
|
@daviddanko post here. |
I read the log again and this is the reason. So it checks if it's monitored and skips deletion if it is. Can you force handle collections again? Maybe it takes two times for it to be deleted if it's now unmonitored in Sonarr? Seer logs and Sonarr logs would be helpful. Debug please. |
|
Same behavior with Season 4 was the last, it got deleted, but it was kept in sonarr.
No change in sonarr, but yes, you are right, it's still monitored in sonarr:
But should it be? The rule actions I am using says unmonitor:
Unfortunately I do not have sonarr debug logs, only info, which is not helpful. I enabled debug and I can reduce the "Take action after days" if needed: seerr debug logs: |
… ended show DELETE_SHOW_IF_EMPTY's no-Seerr fallback required `series.status === 'ended'` AND every season unmonitored. Sonarr carries every TVDB season on the series object, including ones the user never downloaded — those stay monitored indefinitely, so the all-seasons-unmonitored check could be permanently false for a show that is genuinely empty and ended. Result: an ended show with zero episode files would never be deleted if the user only ever had a subset of its seasons (issues #2757 / #2891 — e.g. Smallville, where seasons 1-4 were processed but 5-10 were never downloaded and remain monitored). The `episodeFileCount === 0` gate above this check already proves the show has no files; `ended` confirms no further episodes are coming. Requiring all seasons unmonitored on top adds no safety, only breakage. Drop it. The file gate and the Seerr-authority path are unchanged. Strictly more permissive than before, so no deletion that happened previously stops happening.
a0064f4 to
5760c48
Compare
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
UNMONITOR_SHOW_IF_EMPTY's `unmonitorShowIfEmptyAndEnded` had the same flaw as the delete path: it required every season to be unmonitored, but Sonarr carries every TVDB season on the series object and the ones the user never downloaded stay monitored forever — so a finished show could never be unmonitored. This action keeps files, so it can't use a file-count gate like the delete path. Instead, the monitored check now ignores seasons that hold zero files: the show counts as empty once no season is still both monitored AND holding files. A monitored season with files still blocks, as it should — the user isn't done with it. Adds regression coverage for both the never-downloaded-seasons case and the monitored-season-with-files case.
This comment was marked as resolved.
This comment was marked as resolved.
Review follow-up. season.statistics is optional in Sonarr's response; `?? 0` made a monitored season with absent statistics look empty, which could unmonitor an ended show whose monitored season still has files. A monitored season is now only treated as empty when Sonarr explicitly reports episodeFileCount === 0 — missing statistics counts as still having content (conservative). Also corrects the success log: the predicate allows monitored zero-file seasons to remain, so "no monitored seasons" became "no monitored seasons holding files". Adds a regression test for a monitored season with no statistics.
This comment was marked as outdated.
This comment was marked as outdated.
|
@daviddanko Please try the latest pr-tag. reverted back to basics and had a deeper look at it. |
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as outdated.
This comment was marked as outdated.
|
So I tried it after this but before this. Same behavior. Although this show (DTF St. Louis) had only 1 season, maybe that's an edge case which wasn't considered. Still in monitored status and visible in sonarr:
|
|
Can you update to latest PR just to confirm... I don't think it makes a difference, but how knows. |
|
No change. And I have only one more show to delete :D Will need to download something else for testing. Would you like me to provide the logs? |
|
I think I found it, will post soon. |
…etch is fresh ExternalApiService.get() caches responses for 20 minutes. getSeriesByTvdbId and getMovieByTmdbId went through that cache, so the empty-show cleanup's refetch — which runs immediately after unmonitor + episode-file deletes — got served the pre-mutation snapshot and concluded the show still had content. This is the actual root cause behind #2757 / #2891: every gate-logic change was being fed stale input. - getSeriesByTvdbId / getMovieByTmdbId now use getWithoutCache. These endpoints are read straight after mutations and also drive rule evaluation; both need current truth, not a cached snapshot. - updateMovie's internal read-modify-write read is uncached too, so it no longer risks PUTting a stale movie object back to Radarr. - Verified against Sonarr's debug log: the post-delete refetch issued no HTTP GET at all — it was a cache hit.
|
Root cause — the 20-minute stale cache on getSeriesByTvdbId, with the debug-log evidence (no HTTP refetch was ever issued), and why it made every earlier gate-logic attempt moot. Secondary fix — the allSeasonsUnmonitored gate jamming on never-downloaded TVDB seasons. The hot-path note — getSeriesByTvdbId/getMovieByTmdbId are also used by rule getters, so the always-fresh trade-off is called out explicitly. |
This comment was marked as resolved.
This comment was marked as resolved.
|
@daviddanko Ok, this should be it. I think it was cache. It's always cache or DNS you know... :) Try the latest PR Release when it's built. 🙏🏼 |
|
Don't mind the failing check it's just a format error. Will fix pre merge. |
|
Released to |
|
> Released to Am I good to go @enoch85? |
Yes, good to go. 👍🏼 |
|
Looks good! Thanks for the hard work! |
Great!! Thanks for your testing and prompt answers. 🚀 |
|
@copilot please run yarn format fix to sort the failed test here. |
Agent-Logs-Url: https://github.com/Maintainerr/Maintainerr/sessions/c0f6f3ce-7ed3-4b95-bbf6-3d5bf7f73c44 Co-authored-by: enoch85 <4511254+enoch85@users.noreply.github.com>
Done in |
* build(deps): bump the nestjs group with 4 updates (#2892) Bumps the nestjs group with 4 updates: [@nestjs/common](https://github.com/nestjs/nest/tree/HEAD/packages/common), [@nestjs/core](https://github.com/nestjs/nest/tree/HEAD/packages/core), [@nestjs/platform-express](https://github.com/nestjs/nest/tree/HEAD/packages/platform-express) and [@nestjs/testing](https://github.com/nestjs/nest/tree/HEAD/packages/testing). Updates `@nestjs/common` from 11.1.19 to 11.1.20 - [Release notes](https://github.com/nestjs/nest/releases) - [Commits](https://github.com/nestjs/nest/commits/v11.1.20/packages/common) Updates `@nestjs/core` from 11.1.19 to 11.1.20 - [Release notes](https://github.com/nestjs/nest/releases) - [Commits](https://github.com/nestjs/nest/commits/v11.1.20/packages/core) Updates `@nestjs/platform-express` from 11.1.19 to 11.1.20 - [Release notes](https://github.com/nestjs/nest/releases) - [Commits](https://github.com/nestjs/nest/commits/v11.1.20/packages/platform-express) Updates `@nestjs/testing` from 11.1.19 to 11.1.20 - [Release notes](https://github.com/nestjs/nest/releases) - [Commits](https://github.com/nestjs/nest/commits/v11.1.20/packages/testing) --- updated-dependencies: - dependency-name: "@nestjs/common" dependency-version: 11.1.20 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nestjs - dependency-name: "@nestjs/core" dependency-version: 11.1.20 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nestjs - dependency-name: "@nestjs/platform-express" dependency-version: 11.1.20 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nestjs - dependency-name: "@nestjs/testing" dependency-version: 11.1.20 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: nestjs ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump jest from 30.3.0 to 30.4.2 (#2893) Bumps [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest) from 30.3.0 to 30.4.2. - [Release notes](https://github.com/jestjs/jest/releases) - [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md) - [Commits](https://github.com/jestjs/jest/commits/v30.4.2/packages/jest) --- updated-dependencies: - dependency-name: jest dependency-version: 30.4.2 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump zod from 4.4.1 to 4.4.3 (#2894) Bumps [zod](https://github.com/colinhacks/zod) from 4.4.1 to 4.4.3. - [Release notes](https://github.com/colinhacks/zod/releases) - [Commits](colinhacks/zod@v4.4.1...v4.4.3) --- updated-dependencies: - dependency-name: zod dependency-version: 4.4.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump @tanstack/eslint-plugin-query (#2895) Bumps [@tanstack/eslint-plugin-query](https://github.com/TanStack/query/tree/HEAD/packages/eslint-plugin-query) from 5.100.7 to 5.100.10. - [Release notes](https://github.com/TanStack/query/releases) - [Changelog](https://github.com/TanStack/query/blob/main/packages/eslint-plugin-query/CHANGELOG.md) - [Commits](https://github.com/TanStack/query/commits/HEAD/packages/eslint-plugin-query) --- updated-dependencies: - dependency-name: "@tanstack/eslint-plugin-query" dependency-version: 5.100.10 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * ci(docs-drift): add open_pr input to assign Copilot for drafting docs PR Adds an open_pr workflow_dispatch input. When enabled on a drift run, the workflow appends drafting guidance (work from local clones, read full files, source of truth is the listed commits, skip already-documented items, doc-only minimal edits, structured PR description) to the drift report before upserting the tracking issue on Maintainerr_docs, then assigns Copilot to that issue so the coding agent drafts the PR. * chore: wrap long lines in docs_drift.yml to satisfy yamllint (#2896) The Copilot guidance bullets exceeded the .yamllint 160-character limit. Wrapped each bullet onto continuation lines indented to align with the list marker so the rendered markdown stays a single paragraph per bullet. * chore: remove deprecated /api/plex legacy endpoints (#2884) * fix(release): backfill missing new contributor * docs(changelog): restore missing release headers * fix(sonarr): don't require never-downloaded seasons to be unmonitored before show cleanup (#2897) --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: maintainerr-automation[bot] <261505141+maintainerr-automation[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: enoch85 <mailto@danielhansson.nu> Co-authored-by: Anders Eriksson <36226327+blixten85@users.noreply.github.com>
|
🎉 This PR is included in version 3.11.2 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
…1.2 ) (#46) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/maintainerr/maintainerr](https://github.com/Maintainerr/Maintainerr) | patch | `3.11.1` → `3.11.2` | --- ### Release Notes <details> <summary>Maintainerr/Maintainerr (ghcr.io/maintainerr/maintainerr)</summary> ### [`v3.11.2`](https://github.com/Maintainerr/Maintainerr/releases/tag/v3.11.2) [Compare Source](Maintainerr/Maintainerr@v3.11.1...v3.11.2) ##### Fixes - Fixed an issue in Sonarr where the "Unmonitor and delete season + delete show if empty" action failed to delete the now-empty show after removing a season's files ([#​2897](Maintainerr/Maintainerr#2897)). ##### Internal - Restored missing release headers in changelog. - Backfilled missing new contributor in release notes. - Added `open_pr` input to the `docs-drift` CI workflow for drafting documentation PRs. ##### Dependencies - Updated 4 dependencies, including notable packages: [@​tanstack/eslint-plugin-query](https://github.com/tanstack/eslint-plugin-query), zod, and jest. </details> --- ### Configuration 📅 **Schedule**: (in timezone America/New_York) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xNjAuNyIsInVwZGF0ZWRJblZlciI6IjQzLjE2MC43IiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19--> Reviewed-on: https://git.greyrock.io/greyrock-labs/home-ops/pulls/46
#2897 made getSeriesByTvdbId/getMovieByTmdbId uncached so the empty-show cleanup reads post-deletion truth (#2757/#2891) — but that also de-cached the rule-evaluation path, where the same series/movie resolves once per item, so episode-level rules over large libraries regressed badly. Add ArrLookupCache: a run-scoped memo created for the evaluation loop only and never handed to the collection/action phase, so the cleanup still reads fresh and 'delete then read stale cache' is structurally impossible. Only the two uncached arr identity lookups use it; Tautulli/Seerr/Plex/Jellyfin/ Emby already cache at the API layer.
* chore(server): switch jest transform to @swc/jest and make circular deps SWC-safe ts-jest kept a full TypeScript program per worker, so workers were slow to tear down under CI load and intermittently logged "A worker process has failed to exit gracefully". @swc/jest is transpile-only: the server suite drops from ~66s to ~13s and workers exit cleanly with no warning. SWC's stricter CommonJS live-binding exports turn the codebase's existing circular dependencies into TDZ ReferenceErrors at module load, so this makes those cycles SWC-safe without changing runtime behaviour: - Wrap singular concrete TypeORM relation property types in `Relation<>` so emitted `design:type` metadata no longer eagerly references the related entity class. - Annotate forwardRef-injected constructor params with type-only import aliases so `design:paramtypes` doesn't eagerly reference the class; the value import stays for the forwardRef arrow. DI tokens are unchanged. - Give the media-server factory's three adapter injections explicit `@Inject(forwardRef(...))` (matching the sibling params) so their types can be aliased as well. Closes #2946 * Track collection media rule evaluation failures * fix(server): correct collection ruleGroup entity type * chore(server): remove legacy jest ts transforms * fix(ui): provide production peer dependencies * chore(server): format tsconfig.e2e.json * build(deps): bump node from 26.1.0-alpine3.22 to 26.2.0-alpine3.22 (#2950) Bumps node from 26.1.0-alpine3.22 to 26.2.0-alpine3.22. --- updated-dependencies: - dependency-name: node dependency-version: 26.2.0-alpine3.22 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump the nestjs group with 5 updates (#2951) Bumps the nestjs group with 5 updates: | Package | From | To | | --- | --- | --- | | [@nestjs/common](https://github.com/nestjs/nest/tree/HEAD/packages/common) | `11.1.21` | `11.1.22` | | [@nestjs/core](https://github.com/nestjs/nest/tree/HEAD/packages/core) | `11.1.21` | `11.1.22` | | [@nestjs/platform-express](https://github.com/nestjs/nest/tree/HEAD/packages/platform-express) | `11.1.21` | `11.1.22` | | [@nestjs/swagger](https://github.com/nestjs/swagger) | `11.4.3` | `11.4.4` | | [@nestjs/testing](https://github.com/nestjs/nest/tree/HEAD/packages/testing) | `11.1.21` | `11.1.22` | Updates `@nestjs/common` from 11.1.21 to 11.1.22 - [Release notes](https://github.com/nestjs/nest/releases) - [Commits](https://github.com/nestjs/nest/commits/v11.1.22/packages/common) Updates `@nestjs/core` from 11.1.21 to 11.1.22 - [Release notes](https://github.com/nestjs/nest/releases) - [Commits](https://github.com/nestjs/nest/commits/v11.1.22/packages/core) Updates `@nestjs/platform-express` from 11.1.21 to 11.1.22 - [Release notes](https://github.com/nestjs/nest/releases) - [Commits](https://github.com/nestjs/nest/commits/v11.1.22/packages/platform-express) Updates `@nestjs/swagger` from 11.4.3 to 11.4.4 - [Release notes](https://github.com/nestjs/swagger/releases) - [Commits](nestjs/swagger@11.4.3...11.4.4) Updates `@nestjs/testing` from 11.1.21 to 11.1.22 - [Release notes](https://github.com/nestjs/nest/releases) - [Commits](https://github.com/nestjs/nest/commits/v11.1.22/packages/testing) --- updated-dependencies: - dependency-name: "@nestjs/common" dependency-version: 11.1.22 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nestjs - dependency-name: "@nestjs/core" dependency-version: 11.1.22 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nestjs - dependency-name: "@nestjs/platform-express" dependency-version: 11.1.22 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nestjs - dependency-name: "@nestjs/swagger" dependency-version: 11.4.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nestjs - dependency-name: "@nestjs/testing" dependency-version: 11.1.22 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: nestjs ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump vite from 8.0.13 to 8.0.14 (#2952) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.13 to 8.0.14. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v8.0.14/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 8.0.14 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump @types/node from 22.19.17 to 22.19.19 (#2954) Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.19.17 to 22.19.19. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 22.19.19 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump rolldown from 1.0.1 to 1.0.2 (#2953) Bumps [rolldown](https://github.com/rolldown/rolldown/tree/HEAD/packages/rolldown) from 1.0.1 to 1.0.2. - [Release notes](https://github.com/rolldown/rolldown/releases) - [Changelog](https://github.com/rolldown/rolldown/blob/main/CHANGELOG.md) - [Commits](https://github.com/rolldown/rolldown/commits/v1.0.2/packages/rolldown) --- updated-dependencies: - dependency-name: rolldown dependency-version: 1.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * refactor(server): decouple SettingsService into a passive data store + coordinator Introduce SettingsStoreService as the sole owner of persisted settings state (the hydrated fields, init(), reads, public masking, and low-level persistence), depending only on the settings repositories. SettingsService becomes a pure coordinator (test/save/reinit flows) with no settings state of its own, and every read consumer injects the store. Because the API services and media-server adapters/factory now read settings from the store instead of SettingsService, the circular dependencies are gone: constructor forwardRef injections drop from 28 to 2 and the @swc/jest type-only import aliases from 28 to 2 -- the only remaining pair is the genuine, mutual MediaServerFactory <-> MediaServerSwitchService cycle (factory checks isSwitching(); switch calls uninitializeServer()). No behaviour change: SettingsService keeps its public API by delegating reads to the store; save flows persist via the store and re-hydrate. Public settings masking, save/test/connection, and media-server switch flows are preserved. * refactor(server): rename settings services (and the fields holding them) to reflect roles SettingsStoreService -> SettingsDataService (owns/reads/persists settings data) SettingsService -> SettingsOperationsService (tests connections + applies changes) Renames the classes, files, import paths, and the injected fields/mocks that hold them so each field's name matches the service it holds. Also points AppModule at SettingsDataService for init() (hydration is a data concern), and keeps the deliberate names where they read better (`appSettings` in notification agents, `settings` for plain data reads, the spec SUT `service`). No behaviour change. * fix: overlay notification titles for Emby/Jellyfin movies and UI input warnings - notifications: render media items by item type so Emby/Jellyfin movies (whose parentId points at the library folder) no longer render as "undefined - season undefined" - emby: log the server response body when a collection image upload fails, so a 500 is diagnosable instead of a bare status - rule builder: keep the Custom Value inputs controlled (coalesce undefined) - router: add a HydrateFallback element to silence the initial-hydration warning * chore: type-check on every test run Add a server check-types script and make the turbo test task depend on it, so SWC-transpiled tests (which skip type-checking) can no longer hide type errors. Remove stale testConnections mocks this surfaced in the collection-worker spec. * perf: parallelize per-item Plex watch-history reads during rule execution * refactor: address review — single batching layer, honest knob name, per-item abort - Collapse to one bounded-parallel layer in the comparator and revert the Plex-getter internal episode batching, so total in-flight lookups never exceed the cap (was up to cap x cap when nested). - Rename WATCH_HISTORY_CONCURRENCY -> RULE_EVALUATION_CONCURRENCY: the pool bounds per-item operand evaluation across all getters, not just Plex. - Restore abort checks inside the batched tasks for prompt cancellation. * perf: batch rule operand reads with bounded concurrency * perf(rules): dedupe uncached Sonarr/Radarr identity lookups per run #2897 made getSeriesByTvdbId/getMovieByTmdbId uncached so the empty-show cleanup reads post-deletion truth (#2757/#2891) — but that also de-cached the rule-evaluation path, where the same series/movie resolves once per item, so episode-level rules over large libraries regressed badly. Add ArrLookupCache: a run-scoped memo created for the evaluation loop only and never handed to the collection/action phase, so the cleanup still reads fresh and 'delete then read stale cache' is structurally impossible. Only the two uncached arr identity lookups use it; Tautulli/Seerr/Plex/Jellyfin/ Emby already cache at the API layer. * Handle rule evaluation failures for manual collection media * test(rules): cover ArrLookupCache dedupe and evict-on-failure * test(actions): assert empty-show cleanup reads series fresh, never via a memo Guards the integration invariant behind the rule-evaluation ArrLookupCache: the cleanup path resolves the series from the uncached Sonarr client on every run. If a stale/memoized value ever leaked into this path, the second run (files now gone) would skip deletion — this test would fail. * refactor(server): remove module forwardRefs left by the settings split #2955 split the SettingsService god-object and made SettingsModule a @global provider of SettingsDataService, but left the module-level forwardRef wiring in place. With the settings cycles gone, those forwardRefs are now removable. - Drop the vestigial `forwardRef(() => SettingsModule)` import from the Plex, TMDB, TVDB, Jellyfin and Emby modules: each only reads settings via the @global SettingsDataService, so it needs no import at all. - Convert SettingsModule's nine `forwardRef(() => XApiModule)` imports to plain imports now that no API module imports SettingsModule back. - Break the last MediaServerFactory <-> MediaServerSwitchService cycle (the pair #2955 called irreducible) by extracting the in-progress flag into a zero- dependency MediaServerSwitchState holder: the factory reads it, the switch service writes it, neither depends on the other. This removes the final two constructor forwardRefs and their two @swc/jest type-only aliases. - Move the data-only testSetup() onto SettingsDataService (kept as a delegating wrapper on SettingsOperationsService) so MediaServerSetupGuard reads it from the @global service, letting MediaServerModule drop its SettingsModule import. forwardRef constructor injections and type-only aliases both drop to 0. The only remaining forwardRefs are the genuine, pre-existing CollectionsModule <-> RulesModule domain cycle, untouched here. No behaviour change. * chore: pre-approve project MCP servers (github, playwright) Project-scoped .mcp.json servers require per-project approval before Claude Code loads them; without it the playwright server silently never attaches and its browser tools are unavailable. Add enabledMcpjsonServers so both configured servers auto-connect on session start. * build(deps): bump the nestjs group with 4 updates (#2960) Bumps the nestjs group with 4 updates: [@nestjs/common](https://github.com/nestjs/nest/tree/HEAD/packages/common), [@nestjs/core](https://github.com/nestjs/nest/tree/HEAD/packages/core), [@nestjs/platform-express](https://github.com/nestjs/nest/tree/HEAD/packages/platform-express) and [@nestjs/testing](https://github.com/nestjs/nest/tree/HEAD/packages/testing). Updates `@nestjs/common` from 11.1.22 to 11.1.23 - [Release notes](https://github.com/nestjs/nest/releases) - [Commits](https://github.com/nestjs/nest/commits/v11.1.23/packages/common) Updates `@nestjs/core` from 11.1.22 to 11.1.23 - [Release notes](https://github.com/nestjs/nest/releases) - [Commits](https://github.com/nestjs/nest/commits/v11.1.23/packages/core) Updates `@nestjs/platform-express` from 11.1.22 to 11.1.23 - [Release notes](https://github.com/nestjs/nest/releases) - [Commits](https://github.com/nestjs/nest/commits/v11.1.23/packages/platform-express) Updates `@nestjs/testing` from 11.1.22 to 11.1.23 - [Release notes](https://github.com/nestjs/nest/releases) - [Commits](https://github.com/nestjs/nest/commits/v11.1.23/packages/testing) --- updated-dependencies: - dependency-name: "@nestjs/common" dependency-version: 11.1.23 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nestjs - dependency-name: "@nestjs/core" dependency-version: 11.1.23 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nestjs - dependency-name: "@nestjs/platform-express" dependency-version: 11.1.23 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: nestjs - dependency-name: "@nestjs/testing" dependency-version: 11.1.23 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: nestjs ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump typescript-eslint from 8.59.3 to 8.59.4 (#2961) Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.59.3 to 8.59.4. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.59.4/packages/typescript-eslint) --- updated-dependencies: - dependency-name: typescript-eslint dependency-version: 8.59.4 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps): bump @hookform/resolvers from 5.2.2 to 5.4.0 (#2962) Bumps [@hookform/resolvers](https://github.com/react-hook-form/resolvers) from 5.2.2 to 5.4.0. - [Release notes](https://github.com/react-hook-form/resolvers/releases) - [Commits](react-hook-form/resolvers@v5.2.2...v5.4.0) --- updated-dependencies: - dependency-name: "@hookform/resolvers" dependency-version: 5.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * build(deps-dev): bump postcss from 8.5.14 to 8.5.15 (#2963) Bumps [postcss](https://github.com/postcss/postcss) from 8.5.14 to 8.5.15. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](postcss/postcss@8.5.14...8.5.15) --- updated-dependencies: - dependency-name: postcss dependency-version: 8.5.15 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * perf(rules): lower RULE_EVALUATION_CONCURRENCY 16 -> 8 16 over-drove co-located CPU-heavy backends (Tautulli history queries on an all-in-one N100), pushing requests past the 10s timeout and triggering the retry feedback loop. 8 keeps concurrent load in check while staying far faster than the old sequential path. * fix(collections): address review feedback on rule-evaluation failure tracking - collection-worker.server.spec: drop mock for non-existent SettingsDataService.testConnections (failed check-types CI) - collection-worker: keep original mediaToHandle naming, apply the ruleEvaluationFailed filter inline (per review feedback) - collections.service: make setCollectionMediaRuleEvaluationFailed best-effort so a failed flag write can't abort the rule run --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: enoch85 <mailto@danielhansson.nu> Co-authored-by: Kristian Matthews-Kennington <kristian@matthews-kennington.com> Co-authored-by: maintainerr-automation[bot] <261505141+maintainerr-automation[bot]@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>




Summary
Closes #2891 (and re-closes #2757). The "Unmonitor and delete season + delete show if empty" action deleted a season's files but never deleted the now-empty show. Two distinct bugs were stacked on top of each other.
Root cause: stale cache (the reason nothing earlier worked)
ExternalApiService.get()caches responses for 20 minutes.getSeriesByTvdbIdwent through that cache. The empty-show cleanup's "refetch" — which runs immediately after the unmonitor + episode-file deletes — was a cache hit, so it evaluated the pre-mutation snapshot and concluded the show still had content.Confirmed from daviddanko's Sonarr debug log: after the deletes, no HTTP
GET /series?tvdbId=was issued at all — the refetch never hit Sonarr. Every gate-logic change before this was being fed stale input, which is why "reverted to basics" still failed.Fix:
getSeriesByTvdbId/getMovieByTmdbIdnow read viagetWithoutCache. These endpoints are read straight after mutations and also drive rule evaluation — both need current truth.updateMovie's internal read-modify-write read is uncached too, so it can't PUT a stale movie object back to Radarr.Secondary fix: the
allSeasonsUnmonitoredgateEven with a fresh refetch, the no-Seerr fallback required every season to be unmonitored. Sonarr carries every TVDB season on the series — including ones the user never downloaded — and those stay monitored forever, permanently blocking deletion of a genuinely empty, ended show (e.g. a library with only seasons 1–4 of a 10-season show).
Fix: the no-Seerr fallback now requires
series.status === 'ended'and no longer requires all seasons unmonitored. TheepisodeFileCount === 0file gate and the Seerr-authority path are unchanged.isShowEmpty's'monitored'mode now also treats a monitored-but-fileless season as empty, and missing season statistics are treated as unknown rather than empty.Commits
5760c484— don't require all seasons unmonitored to delete an empty ended show620292f9— apply the same fix to the unmonitor-show check841c4adf— treat missing season statistics as unknown, not empty252ad047— read series/movie lookups uncached so the post-mutation refetch is freshNote
getSeriesByTvdbId/getMovieByTmdbIdare also called by the rule getters (once per media item per rule evaluation), so they no longer benefit from the 20-minute cache there. For local *arr APIs this is negligible per-call, and rule evaluation arguably should run on current data anyway — but it's a deliberate trade of some read-amplification for guaranteed freshness.