feat: track episode availability from media servers scanners - #3402
feat: track episode availability from media servers scanners#34020xSysR3ll wants to merge 1 commit into
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
b686913 to
31c93b5
Compare
This comment has been minimized.
This comment has been minimized.
5d9fd87 to
5d061b0
Compare
|
This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged. |
5d061b0 to
6e41f8f
Compare
6e41f8f to
fcdb7aa
Compare
fcdb7aa to
737842d
Compare
|
This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged. |
fallenbagel
left a comment
There was a problem hiding this comment.
Two changes in here aren't about episode badges. The Jellyfin cache key fix should go straight to develop on its own as it's fixing a bug live today. The Plex 4K detection switch should either come out or land as its own PR covering the movie path too, since right now the file is half on videoResolution and half on width and there is no explanation as to why. The Jellyfin season-existence change is fine but keys off enable4kShow rather than the tracking setting, so it needs a gate or this will change behaviour for people who do not use this setting. The worst blocker is the per-source demotion since, as written, each source deletes what it can't see and the sources disagree.
| if (is4k) { | ||
| seasonExistsInPlex = episodeVersions.some( | ||
| (mediaItem) => (mediaItem.width ?? 0) >= 2000 | ||
| (mediaItem) => mediaItem.videoResolution === '4k' | ||
| ); | ||
| } else if (this.enable4kShow) { | ||
| seasonExistsInPlex = episodeVersions.some( | ||
| (mediaItem) => (mediaItem.width ?? 0) < 2000 | ||
| (mediaItem) => mediaItem.videoResolution !== '4k' | ||
| ); | ||
| } else { | ||
| seasonExistsInPlex = episodeVersions.length > 0; | ||
| } |
There was a problem hiding this comment.
This changes when a 4K season counts as present, independent of episode tracking. Same switch at line 1134-1138, while the movie path at 1097 and 1116 stays on width. This should be split out so it lands with the movie path.
| } else { | ||
| const episodes = await this.jellyfinClient.getEpisodes( | ||
| seriesId, | ||
| seasonMeta.Id, | ||
| { includeMediaInfo: true } | ||
| ); |
There was a problem hiding this comment.
This runs for every 4K instance whether or not episode tracking is on. The branch splits on enable4kShow, not shouldTrackEpisodes, so the 4K pass stops treating a season as present just because episodes exist and starts requiring a stream over 2000 wide. That's the right behavior and it lines up with what the scanner already does, but it changes 4K season removal for anyone with a 4K Sonarr configured whether or not they turned tracking on. Gate it on shouldTrackEpisodes and let the existing behavior stand for everyone else.
|
|
||
| if (seriesId) { | ||
| const cacheKey = `${seriesId}-${seasonMeta.Id}`; | ||
| const cacheKey = `${is4k ? '4k' : 'std'}-${seriesId}-${seasonMeta.Id}`; |
There was a problem hiding this comment.
This is a good catch since the Plex cache already scopes by 4K and this one didn't. But imo, this should go to develop on its own rather than sit behind this stack because its a live bug in develop.
| for (const episode of episodes) { | ||
| const versions = episode.Media ?? []; | ||
| const hasStandard = this.enable4kShow | ||
| ? versions.some((media) => media.videoResolution !== '4k') | ||
| : versions.length > 0; | ||
| const has4k = | ||
| this.enable4kShow && | ||
| versions.some((media) => media.videoResolution === '4k'); | ||
|
|
||
| if (hasStandard) { | ||
| totalStandard += 1; | ||
| } | ||
| if (has4k) { | ||
| total4k += 1; | ||
| } |
There was a problem hiding this comment.
With 4K off, an episode Plex returns without a Media array which used to count toward totalStandard and now doesn't. Probably fine since it is typed non-pption but hasStandard = this.enable4kShow ? versions.some(...) : true keeps the count identical and the ?? [] still does its job in the 4K branch.
| const episodeDetails: ProcessableEpisode[] | undefined = settings.main | ||
| .enableEpisodeAvailability | ||
| ? [] | ||
| : undefined; |
There was a problem hiding this comment.
I think we need to make this very clear. Either in settings or settings that leads to docs or whatever, Plex agents are per-library, so an install with one TMDB-agent library and one TVDB-agent library can't satisfy the provider-matching requirement for both. Same with jellyfin
| if (existingEpisode) { | ||
| if (episodeDetail.hasFile) { | ||
| existingEpisode[is4k ? 'status4k' : 'status'] = | ||
| MediaStatus.AVAILABLE; | ||
| toSave.push(existingEpisode); | ||
| } | ||
| if (this.enable4kShow && episodeDetail.hasFile4k) { | ||
| existingEpisode.status4k = MediaStatus.AVAILABLE; | ||
| toSave.push(existingEpisode); | ||
| } |
There was a problem hiding this comment.
Same entity gets pushed twice when both flags are set.
| if (tvdbSeasonFromAnidb) { | ||
| let show = this.processedAnidbSeason.get(tvShow.id); | ||
|
|
||
| if (!show) { | ||
| show = new Map([[season.season_number, totalStandard]]); | ||
| this.processedAnidbSeason.set(tvShow.id, show); | ||
| } else { | ||
| const currentCount = show.get(season.season_number) ?? 0; | ||
| const newCount = currentCount + totalStandard; | ||
| show.set(season.season_number, newCount); | ||
| totalStandard = newCount; | ||
| } | ||
| } | ||
|
|
||
| processableSeasons.push({ | ||
| seasonNumber: season.season_number, | ||
| totalEpisodes: season.episode_count, | ||
| episodes: totalStandard, | ||
| episodes4k: total4k, | ||
| episodeDetails, |
There was a problem hiding this comment.
The AniDB path sums counts across entries because each entry restarts at episode 1 and there's no offset to map with, animelist.ts only reads defaulttvdbseason. episodeDetails carries raw IndexNumber through anyway, so cour 2's episode 1 writes a row for episode 1 of the TMDB season and collides with cour 1 on the unique constraint. We would have to skip episodeDetails when tvdbSeasonFromAnidb is set.
| if (shouldTrackEpisodes) { | ||
| const existingEpisodes = dbEpisodesBySeasonId.get(season.id) ?? []; | ||
| const episodeHasFileByNumber = new Map( | ||
| existingEpisodes.map((episode) => [episode.episodeNumber, false]) | ||
| ); | ||
|
|
||
| for (const episode of episodes ?? []) { | ||
| if (episode.index == null) { | ||
| continue; | ||
| } | ||
|
|
||
| const versions = episode.Media ?? []; | ||
| let hasFile = false; | ||
|
|
||
| if (is4k) { | ||
| hasFile = versions.some( | ||
| (mediaItem) => mediaItem.videoResolution === '4k' | ||
| ); | ||
| } else if (this.enable4kShow) { | ||
| hasFile = versions.some( | ||
| (mediaItem) => mediaItem.videoResolution !== '4k' | ||
| ); | ||
| } else { | ||
| hasFile = versions.length > 0; | ||
| } | ||
|
|
||
| if (hasFile) { | ||
| episodeHasFileByNumber.set(episode.index, true); | ||
| } | ||
| } | ||
|
|
||
| await this.unmarkMissingEpisodes( | ||
| media, | ||
| season, | ||
| is4k, | ||
| episodeHasFileByNumber, | ||
| existingEpisodes | ||
| ); |
There was a problem hiding this comment.
Every source demotes on its own. episodeHasFileByNumber starts with every known row seeded false, so anything this pass doesn't see gets marked DELETED regardless of what the other sources know. The Sonarr pass clears an episode Plex has, the next Plex scan promotes it back, and the badge flips every cycle. Plex makes it worse. A combined S01E01-E02 file comes back as one item with index 1, since PlexMetadata has no IndexNumberEnd, so this pass deletes episode 2 that Sonarr correctly wrote as available. Seasons and media already resolve exists-in-Plex-or-Sonarr before demoting. Episodes need the same union.
Description
Part 3 of #1671, stacked on #3355.
Wires episode availability into the media server scanners so media-server-only users get per-episode badges without Sonarr.
Episode numbers come from the library agent, so Seerr's metadata provider must match that agent (TMDB or TVDB).
Sonarr tracking stays TVDB-only.
How Has This Been Tested?
Only unit tests for now.
Screenshots / Logs (if applicable)
Checklist:
pnpm buildpnpm i18n:extract