Skip to content

feat: clean up empty Sonarr shows after season actions#2458

Closed
enoch85 wants to merge 14 commits into
developmentfrom
feat/clean-up-empty-ended-shows-892
Closed

feat: clean up empty Sonarr shows after season actions#2458
enoch85 wants to merge 14 commits into
developmentfrom
feat/clean-up-empty-ended-shows-892

Conversation

@enoch85

@enoch85 enoch85 commented Mar 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #892

  • adds two season-level Sonarr actions:
    • Unmonitor and delete season + delete show if empty
    • Unmonitor season + unmonitor show if empty
  • deletes ended empty shows immediately after season cleanup
  • for continuing shows, checks Seerr for remaining requested seasons and only deletes when none remain
  • the delete-show-if-empty path inspects Seerr state instead of deleting Seerr requests, preserving Seerr history and audit trail
  • delete-show-if-empty is not gated by Force Seerr; for continuing shows it only needs Seerr to be configured and reachable to confirm that no other season requests remain
  • moves ServarrAction into shared contracts and updates the UI to use the shared enum
  • adds focused tests for Sonarr empty-show cleanup and Seerr request filtering

Test plan

  • Verify ended shows are deleted when the last season is removed with delete-show-if-empty
  • Verify a continuing show in the reviewer scenario (for example, only one requested season remains and Sonarr is not monitoring future seasons by default) is deleted when that last requested season is removed
  • Verify continuing shows are deleted only when Seerr shows no other requested seasons remain
  • Verify continuing shows are not deleted when Seerr still has another requested season
  • Verify continuing shows are not deleted when Seerr state is unavailable or Seerr is unreachable
  • Verify delete-show-if-empty does not remove Seerr requests or request history
  • Verify delete-show-if-empty still works without Force Seerr enabled
  • Verify unmonitor-show-if-empty only unmonitors ended shows with no monitored seasons left
  • Verify existing delete and unmonitor actions still behave as before

Add two new ServarrAction variants for season-level rules that
automatically clean up the parent show when it becomes empty and has
ended status in Sonarr:

- DELETE_SHOW_IF_EMPTY: deletes the season, then deletes the show if
  no episode files remain and the show has ended
- UNMONITOR_SHOW_IF_EMPTY: unmonitors the season, then unmonitors the
  show if no monitored seasons remain and the show has ended
enoch85 added a commit that referenced this pull request Mar 9, 2026
…, #2442, #2406, #2386, #2370

PR #2466 - fix: honor Jellyfin played threshold
- Respect configured played percentage threshold for Jellyfin watch status

PR #2461 - feat(rules): add ARR disk target path selection for disk space rules
- Allow selecting specific disk target paths for Radarr/Sonarr disk space rules

PR #2458 - feat: clean up empty ended shows in Sonarr after season actions
- Automatically remove ended shows from Sonarr when all seasons are processed

PR #2453 - fix: improve Plex viewCount reliability and add isWatched boolean
- Use native Plex viewCount field with watch history fallback
- Add new isWatched boolean rule property

PR #2452 - build(deps): bump actions/download-artifact from 7 to 8

PR #2451 - build(deps): bump actions/upload-artifact from 6 to 7

PR #2442 - fix(server): reject null/undefined in numeric rule comparisons
- Add getComparisonResult wrapper that fails closed on null/undefined operands
- Strict type checking for BIGGER/SMALLER comparisons

PR #2406 - Metadata provider abstraction layer with TVDB support
- Add MetadataService as central metadata resolution layer
- TVDB support as alternative metadata provider
- Dynamic provider preference with fallback
- Replace TmdbIdService with unified MetadataService

PR #2386 - feat: missing_episode rules
- Add missing episode count as a rule property for Sonarr

PR #2370 - build(deps-dev): bump the eslint group with 2 updates

enoch85 commented Mar 9, 2026

Copy link
Copy Markdown
Collaborator Author

This is now included in the jellyfin-dev docker container - among some other fixes. To upgrade, change tag in docker from latest (or whatever you have) to jellyfin-dev. Then do:

docker compose pull jellyfin-dev && docker compose up -d

You can check the latest commits here: https://github.com/Maintainerr/Maintainerr/commits/jellyfin-dev

  1. Consider the jellyfin-dev branch to be early development, and make a backup before switching to that branch!
  2. Please test that your issue is fixed
  3. Report back here, and tag me and @ydkmlt84

Thank you very much! 🚀

@andrew-kennedy

Copy link
Copy Markdown
Contributor

Ideally this PR would also allow for cleaning up shows that haven't ended if there are no outstanding requests. Seerr recently added a new setting that makes shows added to sonarr not default to monitoring all new seasons automatically, therefore a request for one season will never put the show in a state to download any more episodes/seasons. In this case, when the season is deleted due to a rule's action, if there is no outstanding request (because outstanding requests create a monitored season) the show itself should be erased too.

@enoch85

enoch85 commented Mar 11, 2026

Copy link
Copy Markdown
Collaborator Author

@andrew-kennedy

I went back through issue #892, and I think your point is valid but slightly outside the original scope. The issue there was specifically about ended shows that become empty after Maintainerr deletes a season, and the goal was to handle that at action time because those shows may no longer be discoverable through normal Plex-seeded rules afterward.

Your Seerr example is a real adjacent case: with Monitor New Seasons set to None, a continuing show can also be left behind in Sonarr as an empty shell once the last requested season is removed. I prototyped a narrow extension for the season delete variant so that, when Force Seerr is enabled and Seerr shows no remaining season requests after the deleted season is excluded, Maintainerr also deletes that continuing show.

I kept the ended-show behavior unchanged and made the Seerr path fail safe when request state is unknown. So I’d treat this as follow-up scope on top of the original 892 fix, rather than the PR missing the original issue entirely.

@andrew-kennedy

Copy link
Copy Markdown
Contributor

@enoch85 nice! I agree that it makes more sense as a follow up. What does Force Seerr do? I don't think I've seen that option since I last explored around my Maintainerr instance when I first configured it a few months back.

@enoch85

enoch85 commented Mar 12, 2026

Copy link
Copy Markdown
Collaborator Author

@enoch85 nice! I agree that it makes more sense as a follow up. What does Force Seerr do? I don't think I've seen that option since I last explored around my Maintainerr instance when I first configured it a few months back.

Well it's added now anyway 🙂

Here's the info: https://docs.maintainerr.info/latest/Rules/?h=force+see#general

Force resets the Seerr record by deleting any requests instead of relying on availability-sync. 'Enable CSRF Protection' needs to be disabled in Seerr's settings for this to work.

@andrew-kennedy

Copy link
Copy Markdown
Contributor

@enoch85 Why must requests be deleted directly vs relying on availability sync for seerr? is the idea that if a request persists until the next availability sync, maintainerr may think it's an "unfulfilled" request and thus the show overall should stay present even after deleting the season?

Couldn't we just check if any requests remain without forcing the deletion of requests for the currently processed season? Basically just ignore available requests for the season we are currently deleting, and only count other available or partially available requests for other seasons for the show. Then we don't have to futz with CSRF protection on Seerr etc.

this.logger.debug(err);
return undefined;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Here's some alternate pseudocode to get at what I mean:

public async hasRemainingSeasonRequests(
  tmdbid: string | number,
  removedSeasonNumber: number,
): Promise<boolean | undefined> {
  try {
    const media = await this.getShow(tmdbid);

    if (!media?.mediaInfo) {
      return undefined;
    }

    const requests = media.mediaInfo.requests ?? [];

    return requests
      .filter((request) => request.status !== MediaRequestStatus.DELETED)
      .flatMap((request) => request.seasons)
      .some((season) => season.seasonNumber !== removedSeasonNumber);
  } catch (err) {
    this.logger.warn(
      'Seerr communication failed. Is the application running?',
    );
    this.logger.debug(err);
    return undefined;
  }
}

Your original code means:

"Is there any request that contains at least one season other than removedSeasonNumber?"

but this rewrite proposal means:

"Is there any whole request left that does not contain removedSeasonNumber at all?"

Put another way, what we really want is:

"Among non-deleted requests for this show, does any request still contain any season whose number is not removedSeasonNumber?"

This would mean that we don't need to have Maintainerr mess with deleting Seerr requests itself at all, and we maintain the "audit trail" of cleaned up, deleted requests in the Seerr UI.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the review. Reverted the old commit and refactored a bit further. Please check it it's aligning wit your intentions.

@enoch85

enoch85 commented Mar 14, 2026

Copy link
Copy Markdown
Collaborator Author

Regarding the revert, and latest commit:

Why

The previous implementation could remove Seerr request data as part of the cleanup flow, which didn’t match the reviewer’s intent and risked losing useful request history. It also kept the action enum duplicated between server and UI code.

What it changes

This update makes DELETE_SHOW_IF_EMPTY inspect Seerr state instead of mutating it: ended empty shows are removed immediately, while continuing empty shows are only removed when no other Seerr season requests remain. It also moves ServarrAction into shared contracts, updates the UI to use that shared enum, and adds focused tests around the new Sonarr/Seerr behavior.

Why it’s better

The behavior is safer and more accurate because it preserves Seerr’s audit trail while still preventing orphaned empty shows in Sonarr. It’s also cleaner to maintain, since the shared enum removes magic numbers and keeps server and UI behavior aligned from one source of truth.

Regarding the Force Seerr option

DELETE_SHOW_IF_EMPTY is explicitly excluded from the forceSeerr mutation gate in collection-handler.ts:85-92, so this behavior does not require the “Force Seerr” checkbox to be enabled just to run.

The nuance is in the Sonarr handler: for continuing shows, it still calls Seerr to check whether any season requests remain in sonarr-action-handler.ts:326-335. So the behavior is:

Ended empty show: not dependent on forceSeerr; it can be deleted without that flag.
Continuing empty show: not gated by forceSeerr, but it does still depend on Seerr being configured/reachable so Maintainerr can confirm there are no remaining requests.
If Seerr state is unavailable, it will safely avoid deleting the continuing show.

@enoch85 enoch85 changed the title feat: clean up empty ended shows in Sonarr after season actions feat: clean up empty Sonarr shows after season actions Mar 14, 2026
@enoch85 enoch85 requested a review from andrew-kennedy March 14, 2026 12:18
enoch85 added a commit that referenced this pull request Mar 14, 2026
PR #2458 - feat: clean up empty Sonarr shows after season actions
- Move ServarrAction enum to @maintainerr/contracts for shared use
- Add SeerrApiService.hasRemainingSeasonRequests() for continuing show cleanup
- Update deleteShowIfEmpty to handle continuing shows via Seerr state check
- Exclude DELETE_SHOW_IF_EMPTY from Seerr request mutation in collection handler
- Use ServarrAction enum constants in UI AddModal instead of magic numbers
- Update force Seerr helper text to describe season-specific behavior
- Add SeerrApiModule to ActionsModule for DI
- Add comprehensive tests for all new Seerr-aware cleanup scenarios

@andrew-kennedy andrew-kennedy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks good to me when it comes to non-ended shows, but I had a question about ended shows: why do we care if they are ended at all?

Imagine I requested season 1 of breaking bad, an ended show. Someone else requested season 2, but for whatever reason (indexer rate limits, etc) that request hasn't been fulfilled yet. My maintainerr rule eventually gets triggered to delete season 1. Why should it matter if the show is ended? Shouldn't deleting the show only happen regardless of if it's "ended" if two things are both true:

  1. Show is empty of seasons in sonarr.
  2. No unfulfilled seasons besides the current one being acted on exist in any seerr requests.

I suspect we don't need to take into account ended status at all.

@andrew-kennedy

Copy link
Copy Markdown
Contributor

Any chance I can help further towards getting this merged @enoch85? Would really smooth out a rough edge I experience a lot with Maintainerr leaving a lot of empty shows today.

@enoch85

enoch85 commented Mar 24, 2026

Copy link
Copy Markdown
Collaborator Author

Any chance I can help further towards getting this merged @enoch85? Would really smooth out a rough edge I experience a lot with Maintainerr leaving a lot of empty shows today.

I want this as much as you. But I don't control the main repo, only jellyfin-dev.

So use 'jellyfin-dev' until it gets merged into main. I run it myself in production.

Cc @ydkmlt84 as we discussed already.

@enoch85

enoch85 commented Mar 24, 2026

Copy link
Copy Markdown
Collaborator Author

@andrew-kennedy Have you tested this change on jellyfin-dev?

@enoch85 enoch85 requested a review from andrew-kennedy March 24, 2026 20:10
@enoch85 enoch85 self-assigned this Mar 24, 2026

@andrew-kennedy andrew-kennedy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Currently deleteShowIfEmpty is too aggressive in deleting shows. Left a comment explaining the issue and how to fix.

Comment on lines +313 to +319
if (series.status === 'ended') {
await sonarrApiClient.deleteShow(series.id, true, listExclusions);
this.logger.log(
`[Sonarr] Show '${series.title}' is ended with no files remaining — deleted from Sonarr`,
);
return;
}

@andrew-kennedy andrew-kennedy Mar 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This will lead to ended shows getting deleted if they have another in-flight request for another season.

Here's the scenario:

  1. Someone requests season 1 of an ended show
  2. Season 1 Downloads
  3. Someone requests season 2 of the same ended show
  4. Due to some unknown issue, Season 2 does not yet dowload
  5. The rule runs and unmonitors and deletes season 1 in sonarr as all criteria are met
  6. This function runs as the show's files are "empty", and it deletes the show

In this case, there was still an outstanding unfulfilled request for season 2 of the show, and this function deletes the show before it can ever be filled. We still need to check Seerr or the monitored status of other seasons in sonarr to ensure we don't delete the show when pending requests still exist. Preferably seerr, as that's the true source of truth.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Great catch!

Feel free to dig in to this PR. Help is welcome! 🚀

@enoch85 enoch85 marked this pull request as draft March 27, 2026 22:11
@enoch85

enoch85 commented Apr 3, 2026

Copy link
Copy Markdown
Collaborator Author

Just an update here...

I plan to continue to work on this once 3.4.0 are released. Reason being that release is already quite huge in terms of UI + Metadata, and I don't want to push too many changes at once.

So, I have not forgotten, just focused on UI this release.

@enoch85 enoch85 removed the request for review from ydkmlt84 April 7, 2026 00:22
@enoch85 enoch85 changed the base branch from main to development April 7, 2026 00:23
@enoch85 enoch85 added this to the 3.5.0 milestone Apr 7, 2026

enoch85 commented Apr 7, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #2618. Rebuilt from current development so the valid fixes and review intent could be carried forward cleanly.

@enoch85 enoch85 closed this Apr 7, 2026
@enoch85 enoch85 deleted the feat/clean-up-empty-ended-shows-892 branch April 26, 2026 18:50
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.

TV shows are missing from Maintainerr when they are monitored but have no files

2 participants