feat(episodes): add episode availability tracking and sync#1671
feat(episodes): add episode availability tracking and sync#16710xSysR3ll wants to merge 18 commits intoseerr-team:developfrom
Conversation
|
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 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: Wdyt ? |
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 |
|
This pull request has merge conflicts. Please resolve the conflicts so the PR can be successfully reviewed and merged. |
|
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. |
|
The merge of the tvdb is done |
1b35dd4 to
84dc865
Compare
Now it is yes |
|
Following up on the previous comments about episode mismatch with TMDB: |
Yep, I think so. But we should be pretty clear about this - with a tooltip or something. |
6624144 to
5e586d6
Compare
|
@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. |
|
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" ? |
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. |
5e586d6 to
2a62eb7
Compare
There was a problem hiding this comment.
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 | 🟡 MinorFix Prettier formatting in this file (CI failure).
Pipeline reports a Prettier issue here; please runprettier --writeon this file.server/models/Tv.ts (1)
118-155:⚠️ Potential issue | 🟡 MinorCI 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 | 🟡 MinorCI 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.
getMedianow pulls all episodes for any TV lookup. If episode tracking is disabled, this adds unnecessary query cost. Consider checking the setting before includingseasons.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
episodeNumbercan 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
downmethod 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.
There was a problem hiding this comment.
🧹 Nitpick comments (3)
server/routes/tv.ts (1)
88-88: Remove unnecessaryawait—getSettings()is synchronous.The
getSettings()function returns aSettingsobject directly, not a Promise. Theawaitis harmless but misleading.✏️ Proposed fix
- const settings = await getSettings(); + const settings = getSettings();server/lib/availabilitySync.ts (2)
696-700: Consider computingshouldTrackEpisodesonce and passing it down.The same settings check is duplicated in
mediaExistsInSonarr(lines 696-700) andseasonExistsInSonarr(lines 806-810). WhilegetSettings()returns a singleton and is cheap, computing this once inrun()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
findOnequery (line 862) followed by asave(lines 874, 891). For a season with many episodes, this results in 2N database round-trips. Consider:
- Fetch all existing episodes for the season in one query before the loop
- 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) { ... } }
There was a problem hiding this comment.
🧹 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', {
|
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>
e0b0529 to
f75741c
Compare
There was a problem hiding this comment.
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.
gauthier-th
left a comment
There was a problem hiding this comment.
From what I'm seeing TypeORM migrations are 6+ months old. Please re-generate them.
Description
This allows Jellyseerr to track the availability status of individual episodes, enabling better status reporting for partially available seasons.
Screenshot (if UI-related)
To-Dos
pnpm buildpnpm i18n:extractIssues Fixed or Closed
Summary by CodeRabbit
New Features
Chores
Migrations