feat(rules): add watcher lists since media was added - #3532
Conversation
|
Before I move this out of draft, there are two semantic points where I would appreciate maintainer input. First, targets with no available episodes. The existing Plex Second, manually using Plex's Mark Watched action. These rules currently use actual watch-history rows only, following the existing Plex watcher rules, so manually marked episodes do not count. I briefly considered supporting manual marks only for users who also have at least one genuine post-add view. That would let somebody watch part of the media after it was added and then mark the remainder watched, while still excluding stale watched state from users who have not participated in the new cycle. Unfortunately, determining that state requires retrieving each participating user's Plex access token and probing the metadata separately as that user. I left that out for now because it adds substantial complexity to otherwise straightforward history rules. If manually marked episodes are considered important here, I can bring that approach back into the PR. cc @enoch85 |
Review of #3532 at 09340a6 (expand)Rebased this onto development in a scratch worktree and ran it against the real dev stack: Plex 1.43.3, a second Maintainerr instance on What the history saysI went back through the issues, the board and the commits before reviewing the code, because the premise here has been argued before.
Two things follow. First, #2206 was closed on a wrong premise, so it is fair to reopen the topic. Second, #2206 describes a show-level rule, and as written this PR does not fix that case. The more useful precedent is on #3531 itself. In The same revert also established, by measurement, that Does the cutoff ever fire?History rows outlive the media, but they stay bound to the ids they were written with. I unmatched a movie so its guid changed from So re-added media gets a new id, the old rows stay pointed at the dead one, and the cutoff has nothing to remove:
The one case left is a trash restore, where the item keeps its id but @whitestrake, can you measure that on a real server? Record ratingKey, The cutoff is read off the wrong item
I made one genuine view (episode 13,
Same view, same user, dropped at show level and kept at season level, purely because the parent record is younger than the child. Written the obvious way, Fix is to compare each view against the Smaller things
Your two questionsZero episodes. Align with Jellyfin and Emby, return an empty list, but as its own Manual marks. Your reading is right and I confirmed it: If it comes backPer-item cutoff, all three servers, |
|
I've carried out more detailed lifecycle testing against a real Plex server. The results contradict several of the review's technical conclusions, while other points depend on how the intended scope of the rules is defined. The evidence and a point-by-point response are included below. Detailed response to review itemsTesting and evidence
Core claims
Smaller review items
Use caseConsider a cleanup configuration that:
A user partially watches a show and abandons it. Three years later, the inactivity rule removes it. Another user subsequently requests it again and watches it through to completion. Without the proposed rules, Plex's retained history still places the original viewer in The proposed rules limit both watcher lists to views recorded after the selected show or season was added. Previous-cycle viewers are ignored unless they watch again, allowing the normal completed-viewing cleanup and grace period to proceed. Required decisions
Once those are settled, I can get this PR updated accordingly and address the smaller valid issues the review identified at the same time. |
|
On your three questions:
Keep the Plex zero-episode alignment as its own Claude Opus 4.8: detailed analysis (expand)What I measured today (Plex 1.43.3)
The first row is the one that matters. Item linkage is resolved live from the row, not frozen into it, so re-linking to a replacement item is mechanically possible, and your measurement that Plex does it stands. My earlier unmatch experiment only showed that a guid change on a surviving item keeps the row; it said nothing about re-import. Point by point
Why per episodeOur library has a show at Under the target boundary that produces two answers for one view: kept at season level, dropped at show level. I reproduced that with
The show-level drop is the fail-open direction: Per-episode fixes that and also fixes #2206 at show scope, which the target boundary does not. Cost: 49 already enumerates the episodes and they carry Implementation notes
Infra note for the recorddev-plex cannot currently read any media file ( |
09340a6 to
c134ff1
Compare
|
Excellent. I can go ahead with most of that. I do have to push back a little on the proposed cycle boundary, though, for three reasons:
For those reasons, my recommendation is that each rule use the addition date of the level being evaluated: the show's date for a show target, the season's date for a season target, and the episode's date if episode-level support is included. |
|
Agreed, target level it is: show date for a show target, season date for a season target. The upgrade case settles it. A repack on Jellyfin or Emby creating a new episode item with a newer Two small things:
Please note in the PR description that a show-level rule does not restart on a season re-add (#2206's exact setup), so people know to write that rule at season level. |
c134ff1 to
50a42a3
Compare
|
I've now updated the branch following our discussion and force-pushed it as a clean two-commit history based on current The main commit adds both since-added watcher lists to Plex, Jellyfin and Emby using the same property IDs and names:
Both remain limited to show and season targets. As agreed, each uses the selected target's own addition date as the viewing-cycle boundary, with a strict Missing or malformed addition and watch dates fail closed rather than producing a false empty list. Jellyfin and Emby no longer manufacture a current date when The existing Plex zero-episode behaviour for Everything has been retested from the final branch state, including Playwright tests of both rules, at both target levels, against all three servers. |
|
Accompanying docs PR raised for your consideration at Maintainerr/Maintainerr_docs#169. |
|
Thanks! There are some small fixes I will implement. When committed, please test and check that everything is OK still. |
…roperties Emby now threads the cutoff through getAllEpisodesSeenBy and getSwWatchers the way Plex and Jellyfin do, instead of carrying a second enumeration and its own user mapping beside them. Both dated variants map users exactly like their base property on each server. A completed watch with no date is skipped rather than failing the read, matching sw_lastViewedAtThroughSeason; a malformed date still propagates. addDate and sw_lastEpisodeAddedAt on Jellyfin and Emby no longer pass an invalid Date through when DateCreated is missing. Specs trimmed to the cases that pin the contract.
|
Pushed
|
|
Just had a look into this latest commit. The focused getter tests, complete server suite and repository-wide suite all pass, and the consolidation generally looks good. I noticed three things I think need a quick review:
The missing-versus-malformed timestamp distinction and the existing-property invalid-date fixes otherwise look sound to me. |
…oundary The Emby adapter now answers getDescendantEpisodeWatchHistory like the Jellyfin adapter does: the season and episode walk plus the per-episode reads, walked in batches of EMBY_BATCH_SIZE.EPISODE_WATCH_HISTORY beside its per-user batching. The Emby getter consumes the record map the way the Jellyfin getter does, for sw_allEpisodesSeenBy as well as the dated variants, so the fan-out is bounded in one place. Measured against a real Emby server through a counting proxy: an 8-episode show peaks at 5 reads in flight for ids 12, 49 and 50. Each getter spec pins that the selected show or season's addedAt is the boundary, not a descendant episode's, which is what keeps an upgraded episode on Jellyfin or Emby from resetting a finished season.
11f3a98 to
a3c74f4
Compare
|
Pushed
E2E on a real Emby through a counting proxy, 8-episode series at show level:
21 = 8 episode reads + the show read, 8+1 |
enoch85
left a comment
There was a problem hiding this comment.
Approve in current state.
@whitestrake Please confirm you agree on everything here, and test it yourself please. :)
|
🎉 This PR is included in version 3.27.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
… ➔ 3.27.0) (#121) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/maintainerr/maintainerr](https://github.com/Maintainerr/Maintainerr) | minor | `3.26.0` → `3.27.0` | --- ### Release Notes <details> <summary>Maintainerr/Maintainerr (ghcr.io/maintainerr/maintainerr)</summary> ### [`v3.27.0`](https://github.com/Maintainerr/Maintainerr/blob/HEAD/CHANGELOG.md#3270-2026-09-05) [Compare Source](Maintainerr/Maintainerr@v3.26.0...v3.27.0) #### Highlights - Resolved an issue where items were incorrectly marked as manual in collections, preventing rule-based removal ([#​3636](Maintainerr/Maintainerr#3636)). - Added new watcher list rules for Plex, Jellyfin, and Emby to filter by users who watched media since it was added ([#​3532](Maintainerr/Maintainerr#3532)). #### Fixes - Read the Sonarr episode list fresh before unmonitoring or deleting files ([#​3676](Maintainerr/Maintainerr#3676)). - Increase HTTP timeouts to prevent premature deletion failures during large media server operations ([#​3674](Maintainerr/Maintainerr#3674), [#​3673](Maintainerr/Maintainerr#3673)). - Improve UI error reporting for bulk actions to provide specific failure reasons ([#​3643](Maintainerr/Maintainerr#3643)). - Drop rule-removal markers when a collection stops syncing ([#​3644](Maintainerr/Maintainerr#3644)). - Correctly identify custom collections as siblings when guarding adoption ([#​3641](Maintainerr/Maintainerr#3641)). - Clear cached collection children after a Plex collection mutation ([#​3640](Maintainerr/Maintainerr#3640)). - Treat an unset deletion window as never instead of immediately ([#​3639](Maintainerr/Maintainerr#3639)). - Report additions that failed due to missing media server collections ([#​3638](Maintainerr/Maintainerr#3638)). - Prevent stale-link resync from incorrectly marking manual collection members as manual ([#​3637](Maintainerr/Maintainerr#3637)). - Prevent calendar month skipping when navigating dates ([#​3635](Maintainerr/Maintainerr#3635)). #### Database migrations - Added a `direction` column to the `collection_media_rule_removal` table to track pending removal actions. #### Internal - Refactor bulk outcome reporting to use the outcome object directly ([#​3654](Maintainerr/Maintainerr#3654)). - Refactor rule capability checks to query server capabilities instead of server type ([#​3642](Maintainerr/Maintainerr#3642)). - Transpile NestJS packages to support ESM under Jest ([#​3628](Maintainerr/Maintainerr#3628)). #### Dependencies - Updated 34 dependencies including NestJS, Jest, and various ESLint plugins. </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 CLI](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC41Mi4xIiwidXBkYXRlZEluVmVyIjoiNDQuNTIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsicmVub3ZhdGUvY29udGFpbmVyIiwidHlwZS9taW5vciJdfQ==--> Reviewed-on: https://git.greyrock.io/todd/home-ops/pulls/121
… ➔ 3.27.0) (#2037) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/maintainerr/maintainerr](https://github.com/Maintainerr/Maintainerr) | minor | `3.26.0` → `3.27.0` | --- >⚠️ **Warning** > > Some dependencies could not be looked up. Check the [Dependency Dashboard](issues/567) for more information. --- ### Release Notes <details> <summary>Maintainerr/Maintainerr (ghcr.io/maintainerr/maintainerr)</summary> ### [`v3.27.0`](https://github.com/Maintainerr/Maintainerr/blob/HEAD/CHANGELOG.md#3270-2026-09-05) [Compare Source](Maintainerr/Maintainerr@v3.26.0...v3.27.0) #### Highlights - Resolved an issue where items were incorrectly marked as manual in collections, preventing rule-based removal ([#​3636](Maintainerr/Maintainerr#3636)). - Added new watcher list rules for Plex, Jellyfin, and Emby to filter by users who watched media since it was added ([#​3532](Maintainerr/Maintainerr#3532)). #### Fixes - Read the Sonarr episode list fresh before unmonitoring or deleting files ([#​3676](Maintainerr/Maintainerr#3676)). - Increase HTTP timeouts to prevent premature deletion failures during large media server operations ([#​3674](Maintainerr/Maintainerr#3674), [#​3673](Maintainerr/Maintainerr#3673)). - Improve UI error reporting for bulk actions to provide specific failure reasons ([#​3643](Maintainerr/Maintainerr#3643)). - Drop rule-removal markers when a collection stops syncing ([#​3644](Maintainerr/Maintainerr#3644)). - Correctly identify custom collections as siblings when guarding adoption ([#​3641](Maintainerr/Maintainerr#3641)). - Clear cached collection children after a Plex collection mutation ([#​3640](Maintainerr/Maintainerr#3640)). - Treat an unset deletion window as never instead of immediately ([#​3639](Maintainerr/Maintainerr#3639)). - Report additions that failed due to missing media server collections ([#​3638](Maintainerr/Maintainerr#3638)). - Prevent stale-link resync from incorrectly marking manual collection members as manual ([#​3637](Maintainerr/Maintainerr#3637)). - Prevent calendar month skipping when navigating dates ([#​3635](Maintainerr/Maintainerr#3635)). #### Database migrations - Added a `direction` column to the `collection_media_rule_removal` table to track pending removal actions. #### Internal - Refactor bulk outcome reporting to use the outcome object directly ([#​3654](Maintainerr/Maintainerr#3654)). - Refactor rule capability checks to query server capabilities instead of server type ([#​3642](Maintainerr/Maintainerr#3642)). - Transpile NestJS packages to support ESM under Jest ([#​3628](Maintainerr/Maintainerr#3628)). #### Dependencies - Updated 34 dependencies including NestJS, Jest, and various ESLint plugins. </details> --- ### Configuration 📅 **Schedule**: (in timezone Europe/Paris) - 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 CLI](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC41Mi4xIiwidXBkYXRlZEluVmVyIjoiNDQuNTIuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsicmVub3ZhdGUvY29udGFpbmVyIiwidHlwZS9taW5vciJdfQ==--> Reviewed-on: https://git.erwanleboucher.dev/eleboucher/homelab/pulls/2037
Description & Design
This adds two text-list rule values for show and season targets on Plex, Jellyfin and Emby:
[list] Users that watched at least one episode since added[list] Users that watched every episode since addedThe properties use IDs 49 and 50, with the same IDs and names on all three providers.
Existing watcher lists consider all known watch history. When media is removed and later added again, a username from an earlier availability cycle can remain in those lists indefinitely. Unlike a date value, that username has no threshold through which it will eventually age out. These new values limit the results to the current availability cycle.
Each rule uses the addition date of the selected target as a strict cut-off:
addedAt.addedAt.viewedAt > addedAt.Re-adding a season therefore starts a new cycle when that season is evaluated, but does not restart the enclosing show's cycle if the show itself remained present. The exact surviving-show workflow described in #2206 should use these properties at season level.
The "at least one" value returns the union of users with a qualifying view on any available episode: recognised users on Plex and Emby, while Jellyfin keeps an unresolved account id the way
Users that watched at least one episodedoes. The "every episode" value intersects the qualifying users across every available episode. Targets with no available episodes return an empty list.Missing or invalid target addition dates and malformed watch dates produce a transient unknown result instead of a false empty list. Missing Jellyfin or Emby
DateCreatedvalues are therefore treated as unreadable rather than as newly unwatched media.Plex uses genuine playback-history rows, following its existing watcher rules. Manually marked-watched state is not included. Emby has no central dated history endpoint, so its implementation must read each episode across the available users; episodes are read in batches of five, the way the adapter already batches its per-user reads, and
Users that watched every episodenow uses the same walk.The implementation adds no dependencies, database migration or UI-specific code. Matching property IDs and names allow the existing media-server migration machinery to carry the rules between supported providers.
The existing Plex
Users that watched every episodebehaviour for targets with no episodes is aligned with Jellyfin and Emby in a separatefix:commit, as requested.Related issue
Related to #2206.
AI-Assisted Development
Codex assisted with repository analysis, implementation, focused tests, review passes and local verification.
I reviewed the resulting implementation and its design decisions, including provider parity, show and season scope, target-level viewing cycles, strict timestamp boundaries, malformed-data handling, user mapping and the separate zero-episode fix.
Checklist
How to test
Configure Plex, Jellyfin or Emby and select a show or season with known episode-watch activity.
Confirm that
Users that watched at least one episode since addedincludes users with at least one view strictly after the target's addition date.Confirm that
Users that watched every episode since addedincludes only users with a qualifying view for every available episode.Re-add the selected show or season and confirm that older retained watch activity is excluded until fresh playback occurs.
On Jellyfin or Emby, replace an episode after it has been watched and confirm that the enclosing show or season still uses its own addition date as the cycle boundary.
Run the project checks:
Additional context
The six focused provider suites pass all 433 tests. The complete project suite passes all 3,458 tests, and formatting, linting, typechecking and production builds pass.
I rebuilt the disposable Maintainerr instances from the final commit and exercised both properties at show and season level through Playwright against real Plex, Jellyfin and Emby servers. All twelve provider-and-target rule results contained the expected recognised-user lists. The Jellyfin and Emby runs also covered an upgraded episode whose new item date was later than its retained watch date, confirming that the target-level cut-off preserves the completed-season result.
There were no browser console errors, page errors or Maintainerr error-level log entries during that run.