Skip to content

fix(server): reject null/undefined in numeric rule comparisons#2442

Closed
enoch85 wants to merge 20 commits into
Maintainerr:mainfrom
enoch85:fix/null-comparison-in-rule-evaluator
Closed

fix(server): reject null/undefined in numeric rule comparisons#2442
enoch85 wants to merge 20 commits into
Maintainerr:mainfrom
enoch85:fix/null-comparison-in-rule-evaluator

Conversation

@enoch85

@enoch85 enoch85 commented Mar 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #2441 — Plex IMDb rating (and other nullable values) incorrectly matching SMALLER/BIGGER rules when the value is missing.

Root Cause

Two issues in rule.comparator.service.ts:

  1. Broken null guard — The expression (firstVal !== undefined || null) does not reject null due to JS operator precedence. Since getters return null for missing data, null values pass through to comparisons.

  2. Unsafe numeric coercionBIGGER/SMALLER use raw val1 > val2 / val1 < val2. JS coerces null to 0, so null < 6 evaluates to true, causing false matches.

Changes

  • rule.comparator.service.ts: Fix the null guard to firstVal != null (rejects both null and undefined). Add typeof number checks in BIGGER/SMALLER (consistent with how BEFORE/AFTER already guard for Date types).
  • rule.comparator.service.doRuleAction.spec.ts: Replace individual BIGGER/SMALLER tests with a single table-driven numericComparisonData array (follows existing listCountData pattern), covering normal cases and null/undefined edge cases.

The rule comparator guard `(firstVal !== undefined || null)` did not
reject null values due to JS operator precedence. Since getters return
null for missing data (e.g. missing IMDb rating), null reached
BIGGER/SMALLER comparisons where JS coerced it to 0, causing false
matches.

- Fix the null guard to use `firstVal != null` (rejects both null and
  undefined)
- Add explicit number type checks in BIGGER/SMALLER so non-numeric
  operands return false
- Add table-driven regression tests for numeric comparisons including
  null/undefined cases

Closes Maintainerr#2441
@enoch85 enoch85 requested a review from ydkmlt84 as a code owner March 1, 2026 00:27
Comment thread apps/server/src/modules/rules/tests/rule.comparator.service.doRuleAction.spec.ts Outdated
enoch85 added 3 commits March 1, 2026 09:14
Keep null→0 coercion for BIGGER/SMALLER to match existing behaviour
and avoid unexpected rule changes on upgrade. Add debug logging when
non-number values are encountered in numeric comparisons.
enoch85 added a commit that referenced this pull request Mar 1, 2026
PR #2438 - feat(rules): add rule Jellyfin favorited by including parents
- Added Jellyfin favorited rule that checks parent items (series/seasons)
- Includes unit tests for the new favorited-by-parents getter

PR #2439 - fix: stop rule execution when rule group is deleted mid-run
- Changed onRuleGroupDeleted to call stopProcessingRuleGroup instead of removeFromQueue
- Fires abort signal for in-flight execution, preventing null reference crashes
- Added unit test for delete-during-execution scenario

PR #2442 - fix(server): reject null/undefined in numeric rule comparisons
- Fixed null guard using proper nullish check (`firstVal != null`)
- Preserves v2.27.0 null→0 coercion for BIGGER/SMALLER to avoid upgrade breakage
- Logs warning when non-number values are encountered in numeric comparisons
- Updated regression tests for numeric comparisons including null/undefined cases
@enoch85 enoch85 requested a review from SynVisions March 1, 2026 17:36
@enoch85

enoch85 commented Mar 9, 2026

Copy link
Copy Markdown
Collaborator Author

Why this fix is better

This fix is better for two reasons: it closes the real safety issue, and it leaves the codebase in a better shape to maintain.

On the rule-engine side, it fixes the actual failure mode instead of preserving legacy coercion behavior. Missing operands now go through one comparison path, are treated as a non-match, and are logged for debugging. Numeric BIGGER / SMALLER comparisons now fail closed for non-numeric inputs instead of letting null, undefined, or other invalid values behave like numbers through coercion. That removes the dangerous class of bugs where a missing value could accidentally satisfy a destructive rule.

On the rating side, the refactor is better because the shared logic is now actually shared. Provider/source alias handling and lookup behavior live in media-rating.helper.ts, Plex normalizes provider-aware ratings in plex.mapper.ts, and both Plex and Jellyfin getters use the same lookup path instead of each re-implementing slightly different matching logic. That reduces duplication and makes future changes to rating lookup semantics much safer.

Which issue it solves

This solves two related problems.

First, it fixes the rule-engine bug behind PR #2442: missing numeric values no longer match numeric rules by accident. That is the critical bug, because it can cause unintended cleanup actions. The regression is covered by rule.comparator.service.executeRulesWithData.spec.ts.

Second, it addresses the Plex rating extraction gap that came up in review. Plex can expose external ratings through both Rating[] and top-level fields like rating, audienceRating, ratingImage, and audienceRatingImage. Before this change, Maintainerr could miss ratings that Plex clearly had. The fix now handles both paths and normalizes them consistently before rule evaluation. That behavior is covered by plex-getter.service.spec.ts.

It also improves Jellyfin parity where it makes sense. Jellyfin still has server-specific limitations, but it now uses the same shared lookup mechanics for provider-based rating selection, with Jellyfin-specific fallback behavior kept local to the Jellyfin getter.

Why the refactor is better

The refactor is better because it moves the right logic to the right layer.

The media-server layer now owns the cross-server part: provider aliases, source matching, and rating lookup semantics. That is the reusable part, so it belongs in media-rating.helper.ts.

The server-specific code still owns the server-specific part: how Plex exposes ratings, how Jellyfin exposes ratings, and what fallback behavior is reasonable when a server does not provide an exact provider-specific value. That separation keeps the abstraction honest instead of forcing Plex-specific or Jellyfin-specific behavior into a fake generic layer.

The result is:

  • less duplication
  • clearer separation of concerns
  • lower regression risk
  • easier extension if more rating providers or media servers are added later

Validation

Focused tests passed for the relevant areas:

  • shared media-rating helper
  • Plex getter rating coverage
  • Jellyfin getter rating coverage
  • rule comparator regression coverage

Overall, this is better than the original PR direction because it fixes the unsafe behavior at the root, improves Plex rating extraction instead of only changing comparator semantics, and leaves the rating logic more coherent for future maintenance.

enoch85 added 3 commits March 9, 2026 16:35
Tighten the follow-up review changes around rule comparisons and rating lookups.\n\n- keep PlexMapper.toMediaRatings private again because only metadataToMediaRatings is used externally\n- simplify Jellyfin rating rule handling by calling the shared media rating helper directly and splitting the show-rating branches for readability\n- keep IMDb-specific Jellyfin rules strict while allowing TMDB/community fallback only where intended\n- align comparator typing, defensive numeric logging, and parameterized numeric comparison tests with the current code standards
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! 🚀

enoch85 added 3 commits March 9, 2026 18:09
Allow null lastViewedAt/sw_lastWatched values to match BEFORE rules for Plex, Jellyfin, and Tautulli while keeping other null comparisons fail-closed.\n\nThis preserves the numeric-comparison hardening from the PR, avoids making IN_LAST match never-watched items, and adds focused regression coverage for the allowed and disallowed null-date cases.
Promote missing rule operand logging from debug to warn so unexpected null rule values are more visible during rule execution.\n\nUpdate the comparator execution tests to assert on warn-level logging for the fail-closed paths.
Normalize top-level Plex IMDb and TMDB rating slots through the Plex mapper so provider-specific rule lookups continue to use the shared media rating abstraction.\n\nAlso add focused regression coverage and narrow diagnostics when Plex still returns rating-related fields that cannot be resolved to a requested provider rating.
enoch85 added a commit that referenced this pull request Mar 9, 2026
PR #2442 - fix(server): reject null/undefined in numeric rule comparisons
- Treat missing last-view dates as stale
- Raise missing-operand rule logs to warn
- Normalize Plex provider rating slots
@enoch85

enoch85 commented Mar 9, 2026

Copy link
Copy Markdown
Collaborator Author

Latest changes added to jellyfin-dev. 👍

@enoch85 enoch85 force-pushed the fix/null-comparison-in-rule-evaluator branch from 2c1f21e to eeacbf9 Compare March 9, 2026 20:11
@enoch85

enoch85 commented Mar 9, 2026

Copy link
Copy Markdown
Collaborator Author

The latest changes complements the null-comparison fix by addressing the upstream cause of some false null IMDb values from Plex.

During rule evaluation, the Plex getter was fetching metadata without requesting Plex’s external media payload, so provider-specific rating data could be missing even when Plex’s XML view showed an IMDb rating. That meant the comparator fix would correctly fail closed on null, but we were still generating null in cases where Plex actually had enough metadata.

This change updates the Plex metadata fetch path so rule evaluation explicitly requests external rating data for the item and its parent or grandparent lookups, while preserving the existing includeChildren behavior. Tests were added to lock in both the request shape and the getter behavior.

Issue solved: Plex IMDb rating rules should no longer receive false null values just because the richer Plex metadata payload was not requested.

@enoch85

enoch85 commented Mar 9, 2026

Copy link
Copy Markdown
Collaborator Author

@ydkmlt84 I've been on this issue now for the past 6 hours. It became a bigger than I initally had planned for, but it also makes the code leaner, and pushes more code to the shared abstraction layer, which to me is a good thing. The more we can share between the media servers, the better.

I've tested in my prod, and it behaves as expected. I have nothing to add here now. Consider this PR to be done. 👍

image

enoch85 added a commit that referenced this pull request Mar 9, 2026
- Improve missing operand logging with watch state context
- Add isWatchDateRule helper to constants service
@enoch85 enoch85 force-pushed the fix/null-comparison-in-rule-evaluator branch from 9921ecc to 634e897 Compare March 9, 2026 21:47
@enoch85

enoch85 commented Mar 9, 2026

Copy link
Copy Markdown
Collaborator Author

The last commits was an attempt to improve logging, but reverted it and we are now back to 634e897

enoch85 added a commit that referenced this pull request Mar 9, 2026
@enoch85 enoch85 self-assigned this Mar 10, 2026
@enoch85 enoch85 closed this Mar 26, 2026
@enoch85 enoch85 mentioned this pull request Mar 31, 2026
@enoch85 enoch85 deleted the fix/null-comparison-in-rule-evaluator branch March 31, 2026 05:36
@enoch85

enoch85 commented Mar 31, 2026

Copy link
Copy Markdown
Collaborator Author

Replaced by #2528

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.

Plex IMDb Rating Often Not Being Populated

3 participants