feat(rules): add Sonarr episodeFileRank with seriesTitle/seriesId scoping#3095
feat(rules): add Sonarr episodeFileRank with seriesTitle/seriesId scoping#3095jackemcpherson wants to merge 3 commits into
Conversation
|
Thank you for the review - you make a good point about the Ranking Pool. I will give it some thought and go over the logic again. Other points can also be fixed in the next commit. |
|
Also, have a look here: https://github.com/Maintainerr/Maintainerr/tree/development/.claude (Since you use Claude) |
…t pool to hasFile The maintainer's review on PR Maintainerr#3095 flagged the ranking pool as a footgun: ranking over the full aired Sonarr catalog (including episodes the user has never downloaded) means a partial library can have every owned episode fall outside the newest N and get matched for deletion. Restrict the pool to `e.hasFile === true` so the rule reads as "keep the latest N downloaded" and lines up with what Maintainerr can actually act on. Drop the `sw_` prefix (only used by watch/library properties on Plex/Tautulli/Jellyfin/Emby) and rename to `episodeFileRank` with a human label that calls out the downloaded-only semantic. Spec helpers and bench helper now pass `hasFile: true` explicitly so the deterministic assertions don't trip on the createSonarrEpisode random default.
… scoping - episodeFileRank: rank on-disk episodes per show by air date (newest = 1). Pool requires hasFile, excludes specials (S00), unaired, and null-airDate episodes; out-of-pool ranks null so the comparator stays fail-closed. Rank maps are memoised through ArrLookupCache so long-running daily series fetch the full episode list once per show per rule-run. - Daily-series airDate fallback for Plex items carrying parentIndex (year) but no episode index; keyed on Sonarr's broadcast-date string (not a UTC day-bucket) so US primetime broadcasts straddling UTC midnight still match. - seriesTitle (TEXT) and seriesId (NUMBER) as scoping handles. seriesTitle is the discoverable entry point; seriesId is the stable handle for production rank-and-delete rules where a rename or case mismatch would silently lose the scope guard. - Opt-in bench spec (MAINTAINERR_BENCH=1) compares cached vs uncached rank-map paths at N=100/1000/9000 episodes.
e396dd7 to
2a59d19
Compare
|
Put some more work in to this today. Major:
Minor (review feedback):
Other:
Running end-to-end on my prod setup. Going to leave it for a bit. Will then take another pass over the whole thing. Try and trim some of the code and see if I can find anything performance wise before submitting for review. |
|
A note on scope before this goes further. The original draft added one property ( Why they're here: One caveat on That's three permanent rule properties now instead of one, so I'd rather you make the call than assume.
Either works for me; let me know which you'd rather review. Separately, I've removed the opt-in benchmark spec I mentioned earlier: it was dev-only scaffolding (gated behind |
|
I’m reading and commenting on an iPhone, so I apologize if this has already been discussed and I missed it. If this could also be applied to seasons, it would be one of the best features. A lot of people have been asking for a way to keep only the last X seasons. |

Updating this from the early draft after a couple of weeks dogfooding it on my own setup, and folding in @enoch85's review feedback. Still happy to rework scope, naming, or strategy.
Description & Design
What problem this solves
Sonarr getters can rank by season air date or count missing episodes, but there's no way to rank an episode within its show by air date and prune on a rolling window. A long-running daily series (thousands of episodes) can't be trimmed to "keep the most recent N."
What's in this PR
Three new Sonarr rule properties:
episodeFileRank(NUMBER, episode scope) — rank of an episode within its show by air date, newest = 1, computed only over episodes currently on disk (hasFile === true).episodeFileRank > 5keeps the latest five downloaded. (id 32)seriesTitle(TEXT) — the Sonarr series title, so a rank rule can be scoped to one show in the rule builder (seriesTitle EQUALS "...") instead of via out-of-band Sonarr tags or Plex collections. (id 33)seriesId(NUMBER) — the Sonarr series id; a stable scope handle for destructive rules that survives a title rename or a regional/year suffix. (id 34)seriesTitle/seriesIdwere added after the initial review specifically to giveepisodeFileRanka safe, discoverable way to scope to a single show; see the comment below for the reasoning, and I'm happy to split them out if you'd rather review the rank property on its own.Why this approach
A Sonarr rule property is the smallest viable change: it reuses the existing
arrLookupCacheplumbing, slots into the rule UI with no frontend work (the constants entry drives the dropdown and NUMBER/TEXT render through the generic custom-value input), and composes with the existing comparators. A custom rule action or collection setting would be a bigger surface area.How it works
episodeFileRankswitches oncase 'episodeFileRank'inSonarrGetterService.get.buildRankMapsbuilds the rank over the show's episodes filtered tohasFile === true, season > 0, a validairDateUtc, andairDateUtc <= now; sorted newest-first; rank = position + 1.getEpisodes(showId)call are memoized through the run-scopedarrLookupCache, keyed by(settingsId, showId), so ranking every episode of a long-running daily series pays the sort/fetch cost once per show per rule-run rather than per episode.airDate) backs a fallback for daily-air Plex items, which carry a year asparentIndexand no episode number. It's matched againstoriginallyAvailableAt's broadcast date so a primetime broadcast that crosses UTC midnight still lines up.null/undefined, keeping the comparator fail-closed (no surprise deletions).Trade-offs, edge cases, limitations
addDate-style rules).0001-01-01air date are excluded.0001-01-01T00:00:00Zare rejected explicitly (it parses to a finite year-1 epoch), symmetric on both the Sonarr and Plex sides.Related issue
None. Opened exploratory rather than tied to a ticket; happy to file a discussion if that helps tracking.
AI-Assisted Development
I used Claude as a coding aid for the getter case and the spec scaffolding. I reviewed every line, caught the .NET null-date sentinel and the daily-series UTC-midnight boundary against my own Sonarr instance, chose the
arrLookupCacheroute after profiling per-episode cost, and validated the change end-to-end on my production setup. I take full responsibility for the correctness, performance, and maintainability of the change.Checklist
How to test
Sonarr/Rank among on-disk episodes per show by air date (newest = 1)is bigger than5. Optionally scope withSonarr/Series titleEQUALS your show (orSeries IDfor a rename-proof handle).smaller than 6to confirm only the latest five match.0001-01-01T00:00:00Zair date, confirm it's excluded rather than ranking near the top.yarn testfrom the workspace root; coverage lives inapps/server/src/modules/rules/getter/sonarr-getter.service.spec.ts.