From 81f34aec92ffebec506bae0542ddc718f42cdf9b Mon Sep 17 00:00:00 2001 From: fallenbagel <98979876+Fallenbagel@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:18:44 +0800 Subject: [PATCH] fix(availability-sync): add caching for expisode exist checks for plex/jellyfin --- server/lib/availabilitySync.ts | 71 ++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/server/lib/availabilitySync.ts b/server/lib/availabilitySync.ts index 471f8497d..cdbe80b54 100644 --- a/server/lib/availabilitySync.ts +++ b/server/lib/availabilitySync.ts @@ -21,9 +21,11 @@ class AvailabilitySync { public running = false; private plexClient: PlexAPI; private plexSeasonsCache: Record; + private plexEpisodeExistsCache: Record; private jellyfinClient: JellyfinAPI; private jellyfinSeasonsCache: Record; + private jellyfinEpisodeExistsCache: Record; private sonarrSeasonsCache: Record; private radarrServers: RadarrSettings[]; @@ -34,7 +36,9 @@ class AvailabilitySync { const mediaServerType = getSettings().main.mediaServerType; this.running = true; this.plexSeasonsCache = {}; + this.plexEpisodeExistsCache = {}; this.jellyfinSeasonsCache = {}; + this.jellyfinEpisodeExistsCache = {}; this.sonarrSeasonsCache = {}; this.radarrServers = settings.radarr.filter((server) => server.syncEnabled); this.sonarrServers = settings.sonarr.filter((server) => server.syncEnabled); @@ -939,7 +943,6 @@ class AvailabilitySync { const ratingKey4k = media.ratingKey4k; let seasonExistsInPlex = false; - // Check each plex instance to see if the season exists let plexSeasons: PlexMetadata[] | undefined; if (ratingKey && !is4k) { @@ -955,18 +958,27 @@ class AvailabilitySync { ); if (seasonMeta) { - // Season metadata exists, but verify it has actual episode files. - // This is to ensure we don't keep seasons marked as available - // when all episode files have been removed in Plex. - try { - const episodes = await this.plexClient?.getChildrenMetadata( - seasonMeta.ratingKey - ); + const cacheKey = seasonMeta.ratingKey; - seasonExistsInPlex = episodes?.length > 0; - } catch { - // If we can't fetch episodes, assume the season exists to avoid false removal - seasonExistsInPlex = true; + if (cacheKey in this.plexEpisodeExistsCache) { + seasonExistsInPlex = this.plexEpisodeExistsCache[cacheKey]; + } else { + try { + // Season metadata exists, but we need to verify it has actual + // episode files. Plex can keep empty season entries. + const episodes = await this.plexClient?.getChildrenMetadata( + seasonMeta.ratingKey + ); + + seasonExistsInPlex = + episodes?.some((episode) => episode.Media?.length > 0) ?? false; + } catch { + // If we can't fetch episodes, assume the season exists + // to avoid false removal + seasonExistsInPlex = true; + } + + this.plexEpisodeExistsCache[cacheKey] = seasonExistsInPlex; } } @@ -1068,7 +1080,6 @@ class AvailabilitySync { const ratingKey4k = media.jellyfinMediaId4k; let seasonExistsInJellyfin = false; - // Check each jellyfin instance to see if the season exists let jellyfinSeasons: JellyfinLibraryItem[] | undefined; if (ratingKey && !is4k) { @@ -1084,22 +1095,32 @@ class AvailabilitySync { ); if (seasonMeta) { - // Season metadata exists, but we need to verify it has actual episode files. - // Sometimes Jellyfin keeps season entries even after all episodes are deleted. - // getEpisodes already filters out virtual episodes. const seriesId = is4k ? ratingKey4k : ratingKey; if (seriesId) { - try { - const episodes = await this.jellyfinClient.getEpisodes( - seriesId, - seasonMeta.Id - ); + const cacheKey = `${seriesId}-${seasonMeta.Id}`; - seasonExistsInJellyfin = episodes.length > 0; - } catch { - // If we can't fetch episodes, assume the season exists to avoid false removal - seasonExistsInJellyfin = true; + if (cacheKey in this.jellyfinEpisodeExistsCache) { + seasonExistsInJellyfin = this.jellyfinEpisodeExistsCache[cacheKey]; + } else { + try { + // Season metadata exists, but we need to verify it has actual + // episode files. Jellyfin keeps season entries even after all + // episodes are deleted. getEpisodes already filters out + // virtual episodes. + const episodes = await this.jellyfinClient.getEpisodes( + seriesId, + seasonMeta.Id + ); + + seasonExistsInJellyfin = episodes.length > 0; + } catch { + // If we can't fetch episodes, assume the season exists + // to avoid false removal + seasonExistsInJellyfin = true; + } + + this.jellyfinEpisodeExistsCache[cacheKey] = seasonExistsInJellyfin; } } }