Skip to content

feat(episodes): add episode availability tracking and sync#1671

Open
0xSysR3ll wants to merge 18 commits intoseerr-team:developfrom
0xSysR3ll:feat-episode-availability
Open

feat(episodes): add episode availability tracking and sync#1671
0xSysR3ll wants to merge 18 commits intoseerr-team:developfrom
0xSysR3ll:feat-episode-availability

Conversation

@0xSysR3ll
Copy link
Contributor

@0xSysR3ll 0xSysR3ll commented May 19, 2025

Description

This allows Jellyseerr to track the availability status of individual episodes, enabling better status reporting for partially available seasons.

Screenshot (if UI-related)

image

image

image

To-Dos

  • Successful build pnpm build
  • Translation keys pnpm i18n:extract
  • Database migration (if required)

Issues Fixed or Closed

Summary by CodeRabbit

  • New Features

    • Toggle to enable episode availability tracking in Settings with helper text and localization
    • Episode availability badges shown on TV season/episode views when enabled
    • Public settings now expose episode availability and metadata provider options
  • Chores

    • Database and models updated to store and surface per-episode availability
    • Background sync updated to fetch, cache and persist per-episode availability
    • API responses include availability mapping for seasons
  • Migrations

    • Added migration to create episode table for availability storage

@0xSysR3ll 0xSysR3ll marked this pull request as ready for review May 19, 2025 22:14
@fallenbagel
Copy link
Collaborator

fallenbagel commented May 19, 2025

This will not work properly when theres a mismatch between tvdb and tmdb right? Thats why our original plan was to wait till tvdb pr is merged until this feature.

Unless you have some other ideas? (Haven't gone through the code yet)

@0xSysR3ll
Copy link
Contributor Author

0xSysR3ll commented May 20, 2025

This will not work properly when theres a mismatch between tvdb and tmdb right? Thats why our original plan was to wait till tvdb pr is merged until this feature.

Unless you have some other ideas? (Haven't gone through the code yet)

You’re absolutely right about the TVDB/TMDB mismatch concern - I considered that during implementation. Instead of relying on direct TMDB ID mapping, I leveraged the existing Sonarr connection via externalServiceId and match episodes based on season and episode numbers. This approach works well for the majority of shows where episode numbering aligns across databases.

That said, I recognize there will be edge cases where TVDB and TMDB have inconsistent episode structures. I saw this as a pragmatic first step that covers most use cases.

I thought about a few options moving forward:
• Merge this as-is to support the majority of shows now.
• Hold off until the TVDB PR lands to enable more precise mapping.
• Make this behavior optional until we have more robust TVDB integration.

Wdyt ?

@fallenbagel
Copy link
Collaborator

Hold off until the TVDB PR lands to enable more precise mapping.

I think this would be the better idea. Or it will cause an influx of support requests saying why is this episode not available when it is etc etc

@fallenbagel fallenbagel added the blocked This issue can't be solved for now label Aug 19, 2025
@github-actions github-actions bot added the merge conflict Cannot merge due to merge conflicts label Sep 2, 2025
@github-actions
Copy link

github-actions bot commented Sep 2, 2025

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

@Gauvino
Copy link
Contributor

Gauvino commented Sep 2, 2025

Can a preview be made for this PR when the merge conflicts are resolved?

@0xSysR3ll
Copy link
Contributor Author

Can a preview be made for this PR when the merge conflicts are resolved?

It still needs reviews and the merge of tvdb implementation's PR.

@Gauvino
Copy link
Contributor

Gauvino commented Sep 2, 2025

The merge of the tvdb is done

@0xSysR3ll 0xSysR3ll force-pushed the feat-episode-availability branch from 1b35dd4 to 84dc865 Compare September 3, 2025 21:14
@github-actions github-actions bot removed the merge conflict Cannot merge due to merge conflicts label Sep 3, 2025
@0xSysR3ll
Copy link
Contributor Author

The merge of the tvdb is done

Now it is yes

@gauthier-th gauthier-th removed the blocked This issue can't be solved for now label Sep 4, 2025
@gauthier-th
Copy link
Member

Following up on the previous comments about episode mismatch with TMDB:
Do you think it would be a good idea to be able to enable this feature ONLY when TVDB is configured as metadata provider?
As said above, people will not understand why there are a lot of mismatch when they're using TMDB (the default metadata provider) and will constantly ask why there is a "bug".

@0xSysR3ll
Copy link
Contributor Author

0xSysR3ll commented Sep 4, 2025

Do you think it would be a good idea to be able to enable this feature ONLY when TVDB is configured as metadata provider?

Yep, I think so. But we should be pretty clear about this - with a tooltip or something.

@0xSysR3ll 0xSysR3ll force-pushed the feat-episode-availability branch from 6624144 to 5e586d6 Compare September 5, 2025 19:46
@0xSysR3ll
Copy link
Contributor Author

@gauthier-th Just added a setting to make it optional. And sync only works when TVDB is set as metadata provider for either tv or anime.

@gauthier-th
Copy link
Member

Isn't it weird to display an "error" badge when the episode is not available but not released yet? Or not requested yet?

@0xSysR3ll
Copy link
Contributor Author

0xSysR3ll commented Sep 8, 2025

Isn't it weird to display an "error" badge when the episode is not available but not released yet? Or not requested yet?

Would an airdate check be enough? IMO the "Unavailable" badge should still appear even if the episode hasn't been requested. This could be changed later as part of the "per-episode request feature" ?

@gauthier-th
Copy link
Member

Isn't it weird to display an "error" badge when the episode is not available but not released yet? Or not requested yet?

Would an airdate check be enough? IMO the '‘Unavailable" badge should still appear even if the episode hasn't been requested yet.

I mean, we don't write the status of a season when it's not requested. We only write "Requested" or "Available"/"Partially Available". Not a status in the error color, implying an error when it's "just" missing

@0xSysR3ll
Copy link
Contributor Author

I mean, we don't write the status of a season when it's not requested. We only write "Requested" or "Available"/"Partially Available". Not a status in the error color, implying an error when it's "just" missing

Ah, that makes sense - my mistake. I thought it might be a small UX improvement, but you're right, it could be misleading.

@0xSysR3ll 0xSysR3ll force-pushed the feat-episode-availability branch from 5e586d6 to 2a62eb7 Compare September 8, 2025 19:26
@gauthier-th gauthier-th mentioned this pull request Sep 10, 2025
38 tasks
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
server/api/servarr/sonarr.ts (1)

16-30: ⚠️ Potential issue | 🟡 Minor

Fix Prettier formatting in this file (CI failure).
Pipeline reports a Prettier issue here; please run prettier --write on this file.

server/models/Tv.ts (1)

118-155: ⚠️ Potential issue | 🟡 Minor

CI formatting failure in this file.

CI reports a formatter issue; please run the repo formatter to resolve it.

server/lib/availabilitySync.ts (1)

6-35: ⚠️ Potential issue | 🟡 Minor

CI formatting failure in this file.

CI reports a formatter issue; please run the repo formatter to resolve it.

🤖 Fix all issues with AI agents
In `@server/routes/tv.ts`:
- Around line 86-118: The file fails CI formatting; run the project's formatter
(e.g., eslint --fix / prettier / yarn format as configured) on
server/routes/tv.ts to fix formatting inconsistencies around the block that
defines availableMap, computes shouldTrackEpisodes using getSettings and
MetadataProviderType, and returns mapSeasonWithEpisodes(season, availableMap);
ensure whitespace, indentation, and trailing commas match the repo's formatter
rules and re-run CI.
- Around line 6-10: The response currently includes availableMap even when
episode availability tracking is disabled, causing all episodes to be marked
unavailable; modify the TV route handler in server/routes/tv.ts to check the
tracking setting via getSettings() (or the appropriate flag exposed by
getSettings) and only attach/pass the availableMap variable when that setting is
enabled (i.e., when tracking is true), otherwise omit availableMap from the
response payload so episodes aren’t incorrectly marked available: false; locate
where availableMap is created/added and wrap that logic and its inclusion in the
response with the settings check (use the existing availableMap identifier and
the route handler function in this file).
🧹 Nitpick comments (2)
server/entity/Media.ts (1)

73-94: Gate episode relations behind the feature flag to reduce DB load.
getMedia now pulls all episodes for any TV lookup. If episode tracking is disabled, this adds unnecessary query cost. Consider checking the setting before including seasons.episodes.

♻️ Suggested adjustment
-      // Only load seasons for TV shows
-      if (mediaType === MediaType.TV) {
+      const { main } = getSettings();
+      // Only load episodes for TV shows when availability tracking is enabled
+      if (mediaType === MediaType.TV && main.enableEpisodeAvailability) {
         relations.seasons = {
           episodes: true,
         };
       }
server/migration/postgres/1747690625482-AddEpisodeTable.ts (1)

7-9: Consider adding a unique constraint on (seasonId, episodeNumber).

Without a unique constraint, duplicate episodes with the same episodeNumber can exist for a single season. If this is unintended, adding a unique index would prevent data integrity issues.

💡 Suggested fix to add unique constraint
     await queryRunner.query(
       `CREATE TABLE "episode" ("id" SERIAL NOT NULL, "episodeNumber" integer NOT NULL, "status" integer NOT NULL DEFAULT '1', "status4k" integer NOT NULL DEFAULT '1', "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "seasonId" integer, CONSTRAINT "PK_7258b95d6d2bf7f621845a0e143" PRIMARY KEY ("id"))`
     );
+    await queryRunner.query(
+      `CREATE UNIQUE INDEX "UQ_episode_seasonId_episodeNumber" ON "episode" ("seasonId", "episodeNumber")`
+    );
     await queryRunner.query(

And update the down method accordingly:

   public async down(queryRunner: QueryRunner): Promise<void> {
     await queryRunner.query(
       `ALTER TABLE "episode" DROP CONSTRAINT "FK_e73d28c1e5e3c85125163f7c9cd"`
     );
+    await queryRunner.query(
+      `DROP INDEX "UQ_episode_seasonId_episodeNumber"`
+    );
     await queryRunner.query(`DROP TABLE "episode"`);
   }

Note: If you add this constraint, ensure the SQLite migration (server/migration/sqlite/1747690625482-AddEpisodeTable.ts) is updated consistently.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
server/routes/tv.ts (1)

88-88: Remove unnecessary awaitgetSettings() is synchronous.

The getSettings() function returns a Settings object directly, not a Promise. The await is harmless but misleading.

✏️ Proposed fix
-    const settings = await getSettings();
+    const settings = getSettings();
server/lib/availabilitySync.ts (2)

696-700: Consider computing shouldTrackEpisodes once and passing it down.

The same settings check is duplicated in mediaExistsInSonarr (lines 696-700) and seasonExistsInSonarr (lines 806-810). While getSettings() returns a singleton and is cheap, computing this once in run() and passing it as a parameter would reduce duplication and make the code easier to maintain.

Also applies to: 804-810


858-913: N+1 query pattern — consider batching episode lookups.

Each episode triggers a separate findOne query (line 862) followed by a save (lines 874, 891). For a season with many episodes, this results in 2N database round-trips. Consider:

  1. Fetch all existing episodes for the season in one query before the loop
  2. Collect new/updated episodes and batch-save them after the loop

This is a performance optimization rather than a correctness issue, so it can be deferred if sync performance is acceptable.

♻️ Sketch of batched approach
           if (episodes) {
             try {
+              // Fetch all existing episodes for this season in one query
+              const existingEpisodes = await episodeRepository.find({
+                where: { season: { id: season.id } },
+                relations: ['season'],
+              });
+              const existingMap = new Map(
+                existingEpisodes.map((e) => [e.episodeNumber, e])
+              );
+              const toSave: Episode[] = [];
+
               for (const ep of episodes) {
                 if (ep.seasonNumber === season.seasonNumber) {
-                  const existingEpisode = await episodeRepository.findOne({
-                    where: {
-                      episodeNumber: ep.episodeNumber,
-                      season: { id: season.id },
-                    },
-                    relations: ['season'],
-                  });
+                  const existingEpisode = existingMap.get(ep.episodeNumber);

                   if (existingEpisode) {
                     existingEpisode[is4k ? 'status4k' : 'status'] = ep.hasFile
                       ? MediaStatus.AVAILABLE
                       : MediaStatus.UNKNOWN;
-                    await episodeRepository.save(existingEpisode);
+                    toSave.push(existingEpisode);
                   } else {
                     // ... create newEpisode ...
-                    try {
-                      await episodeRepository.save(newEpisode);
-                    } catch (saveError) { ... }
+                    toSave.push(newEpisode);
                   }
                 }
               }
+              // Batch save all episodes
+              await episodeRepository.save(toSave);
             } catch (err) { ... }
           }

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
server/lib/availabilitySync.ts (1)

862-898: Consider breaking after first successful episode sync to avoid redundant processing.

The episode synchronization runs inside a loop over all Sonarr servers. If multiple servers contain the same show, episodes will be fetched from the database, processed, and saved multiple times per season. This is inefficient and could cause unnecessary database writes.

♻️ Proposed fix to break after successful episode sync
               if (toSave.length > 0) {
                 await episodeRepository.save(toSave);
               }
+              break; // Episode sync complete for this season, no need to check other servers
             } catch (err) {
               logger.error('Failed to update episode availability', {

@github-actions github-actions bot added the merge conflict Cannot merge due to merge conflicts label Feb 16, 2026
@github-actions
Copy link

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

This allows Jellyseerr to track the availability status of individual
episodes, enabling better status reporting for partially available seasons.
The PR should not fix the issue reported in recent tvdb PR.

Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
…ading

Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
…ilitySync to use getEpisodes

Signed-off-by: 0xsysr3ll <0xsysr3ll@pm.me>
@0xSysR3ll 0xSysR3ll force-pushed the feat-episode-availability branch from e0b0529 to f75741c Compare February 16, 2026 18:18
@github-actions github-actions bot removed the merge conflict Cannot merge due to merge conflicts label Feb 16, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@server/lib/availabilitySync.ts`:
- Around line 518-526: Prettier is failing due to formatting in the long
template literal passed to logger.info in availabilitySync.ts; reformat that
string (or run the formatter) so it matches project style—either break the
template into multiple concatenated pieces or run `prettier --write` to
normalize formatting for the logger.info call that references is4k,
media.mediaType, media.tmdbId, mediaServerType and MediaServerType; ensure
indentation and line breaks follow Prettier rules so CI passes.
- Around line 825-899: The per-server loop in shouldTrackEpisodes uses
this.sonarrEpisodesCache and updates episodes immediately, causing later Sonarr
servers to overwrite earlier availability; instead, aggregate hasFile across all
Sonarr servers per season (build maps like anyHasFileByEpisode and
anyHasFile4kByEpisode keyed by episodeNumber from sonarrEpisodesCache entries)
then after the server loop fetch existing episodes via episodeRepository.find,
merge aggregated availability into existing Episode entities (and create new
Episode instances for missing numbers) setting status/status4k based on the
aggregated boolean using MediaStatus.AVAILABLE or MediaStatus.UNKNOWN, and
finally call episodeRepository.save once per season; update error logging around
this flow (where logger.error is used) to reflect the new single-save operation.

Copy link
Member

@gauthier-th gauthier-th left a comment

Choose a reason for hiding this comment

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

From what I'm seeing TypeORM migrations are 6+ months old. Please re-generate them.

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.

See which episode of a series is available and which not

6 participants