Skip to content

feat: track episode availability from media servers scanners - #3402

Open
0xSysR3ll wants to merge 1 commit into
feat-episode-availability-scannersfrom
feat-episode-availability-media-scanners
Open

feat: track episode availability from media servers scanners#3402
0xSysR3ll wants to merge 1 commit into
feat-episode-availability-scannersfrom
feat-episode-availability-media-scanners

Conversation

@0xSysR3ll

Copy link
Copy Markdown
Contributor

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:

  • I have read and followed the contribution guidelines.
  • Disclosed any use of AI (see our policy)
  • I have updated the documentation accordingly.
  • All new and existing tests passed.
  • Successful build pnpm build
  • Translation keys pnpm i18n:extract
  • Database migration (if required)

@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 46b61641-e182-4caa-83ce-62a2109c70bc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@0xSysR3ll
0xSysR3ll force-pushed the feat-episode-availability-media-scanners branch from b686913 to 31c93b5 Compare August 18, 2026 17:14
@github-actions github-actions Bot added the merge conflict Cannot merge due to merge conflicts label Aug 18, 2026
@github-actions

This comment has been minimized.

@0xSysR3ll
0xSysR3ll force-pushed the feat-episode-availability-media-scanners branch 2 times, most recently from 5d9fd87 to 5d061b0 Compare August 18, 2026 19:38
@github-actions github-actions Bot removed the merge conflict Cannot merge due to merge conflicts label Aug 18, 2026
@github-actions github-actions Bot added the merge conflict Cannot merge due to merge conflicts label Aug 18, 2026
@github-actions

Copy link
Copy Markdown

This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.

@0xSysR3ll
0xSysR3ll force-pushed the feat-episode-availability-media-scanners branch from 5d061b0 to 6e41f8f Compare August 18, 2026 19:58
@github-actions github-actions Bot removed the merge conflict Cannot merge due to merge conflicts label Aug 18, 2026
@0xSysR3ll
0xSysR3ll force-pushed the feat-episode-availability-media-scanners branch from 6e41f8f to fcdb7aa Compare August 31, 2026 17:02
@0xSysR3ll
0xSysR3ll force-pushed the feat-episode-availability-media-scanners branch from fcdb7aa to 737842d Compare August 31, 2026 17:10
@github-actions github-actions Bot added the merge conflict Cannot merge due to merge conflicts label Aug 31, 2026
@github-actions

Copy link
Copy Markdown

This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged.

@github-actions github-actions Bot removed the merge conflict Cannot merge due to merge conflicts label Aug 31, 2026
@0xSysR3ll
0xSysR3ll marked this pull request as ready for review August 31, 2026 17:21
@0xSysR3ll
0xSysR3ll requested a review from a team as a code owner August 31, 2026 17:21

@fallenbagel fallenbagel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Comment on lines 1253 to 1263
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;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Comment on lines +1490 to +1495
} else {
const episodes = await this.jellyfinClient.getEpisodes(
seriesId,
seasonMeta.Id,
{ includeMediaInfo: true }
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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}`;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Comment on lines +351 to +365
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;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Comment on lines +346 to +349
const episodeDetails: ProcessableEpisode[] | undefined = settings.main
.enableEpisodeAvailability
? []
: undefined;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Comment on lines 714 to +723
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);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same entity gets pushed twice when both flags are set.

Comment on lines 404 to +423
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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Comment on lines +1265 to +1302
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
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

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.

2 participants