diff --git a/cypress/config/settings.cypress.json b/cypress/config/settings.cypress.json index 45e38a29e..8ba919357 100644 --- a/cypress/config/settings.cypress.json +++ b/cypress/config/settings.cypress.json @@ -21,6 +21,7 @@ "trustProxy": false, "mediaServerType": 1, "partialRequestsEnabled": true, + "removeUnmonitoredFromRequestsEnabled": false, "locale": "en" }, "plex": { diff --git a/overseerr-api.yml b/overseerr-api.yml index ef3ccf8b3..970a9528f 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -174,6 +174,9 @@ components: partialRequestsEnabled: type: boolean example: false + removeUnmonitoredFromRequestsEnabled: + type: boolean + example: false localLogin: type: boolean example: true diff --git a/server/interfaces/api/settingsInterfaces.ts b/server/interfaces/api/settingsInterfaces.ts index 579f11093..878e4c6ac 100644 --- a/server/interfaces/api/settingsInterfaces.ts +++ b/server/interfaces/api/settingsInterfaces.ts @@ -36,6 +36,7 @@ export interface PublicSettingsResponse { originalLanguage: string; mediaServerType: number; partialRequestsEnabled: boolean; + removeUnmonitoredFromRequestsEnabled: boolean; cacheImages: boolean; vapidPublic: string; enablePushRegistration: boolean; diff --git a/server/lib/scanners/baseScanner.ts b/server/lib/scanners/baseScanner.ts index 5bede1cea..64eba633e 100644 --- a/server/lib/scanners/baseScanner.ts +++ b/server/lib/scanners/baseScanner.ts @@ -48,6 +48,7 @@ export interface ProcessableSeason { episodes4k: number; is4kOverride?: boolean; processing?: boolean; + monitored?: boolean; } class BaseScanner { @@ -234,6 +235,7 @@ class BaseScanner { }: ProcessOptions = {} ): Promise { const mediaRepository = getRepository(Media); + const settings = getSettings(); await this.asyncLock.dispatch(tmdbId, async () => { const media = await this.getExisting(tmdbId, MediaType.TV); @@ -283,6 +285,8 @@ class BaseScanner { ? MediaStatus.PARTIALLY_AVAILABLE : !season.is4kOverride && season.processing ? MediaStatus.PROCESSING + : settings.main.removeUnmonitoredFromRequestsEnabled && !season.monitored && season.episodes == 0 + ? MediaStatus.UNKNOWN : existingSeason.status; // Same thing here, except we only do updates if 4k is enabled @@ -296,6 +300,8 @@ class BaseScanner { ? MediaStatus.PARTIALLY_AVAILABLE : season.is4kOverride && season.processing ? MediaStatus.PROCESSING + : settings.main.removeUnmonitoredFromRequestsEnabled && !season.monitored && season.episodes4k == 0 + ? MediaStatus.UNKNOWN : existingSeason.status4k; } else { newSeasons.push( @@ -623,7 +629,6 @@ class BaseScanner { const mediaRepository = getRepository(Media); await this.asyncLock.dispatch(tmdbId, async () => { const existing = await this.getExisting(tmdbId, MediaType.MOVIE); - // For some reason the status of missing movies isn't PENDING but PROCESSING if (existing && existing.status === MediaStatus.PROCESSING) { existing.status = MediaStatus.UNKNOWN; await mediaRepository.save(existing); diff --git a/server/lib/scanners/radarr/index.ts b/server/lib/scanners/radarr/index.ts index 8d529ebef..980e999ba 100644 --- a/server/lib/scanners/radarr/index.ts +++ b/server/lib/scanners/radarr/index.ts @@ -79,7 +79,8 @@ class RadarrScanner } private async processRadarrMovie(radarrMovie: RadarrMovie): Promise { - if (!radarrMovie.monitored && !radarrMovie.hasFile) { + const settings = getSettings(); + if (settings.main.removeUnmonitoredFromRequestsEnabled && !radarrMovie.monitored && !radarrMovie.hasFile) { this.processUnmonitoredMovie(radarrMovie.tmdbId); return; } diff --git a/server/lib/scanners/sonarr/index.ts b/server/lib/scanners/sonarr/index.ts index 3256c9482..0e75e8a1f 100644 --- a/server/lib/scanners/sonarr/index.ts +++ b/server/lib/scanners/sonarr/index.ts @@ -1,8 +1,11 @@ import type { SonarrSeries } from '@server/api/servarr/sonarr'; import SonarrAPI from '@server/api/servarr/sonarr'; import type { TmdbTvDetails } from '@server/api/themoviedb/interfaces'; +import { MediaType } from '@server/constants/media'; import { getRepository } from '@server/datasource'; import Media from '@server/entity/Media'; +import { MediaRequest } from '@server/entity/MediaRequest'; +import SeasonRequest from '@server/entity/SeasonRequest'; import type { ProcessableSeason, RunnableScanner, @@ -85,6 +88,7 @@ class SonarrScanner private async processSonarrSeries(sonarrSeries: SonarrSeries) { try { const mediaRepository = getRepository(Media); + const settings = getSettings(); const server4k = this.enable4kShow && this.currentServer.is4k; const processableSeasons: ProcessableSeason[] = []; let tvShow: TmdbTvDetails; @@ -112,6 +116,34 @@ class SonarrScanner for (const season of filteredSeasons) { const totalAvailableEpisodes = season.statistics?.episodeFileCount ?? 0; + if (settings.main.removeUnmonitoredFromRequestsEnabled && season.monitored === false && totalAvailableEpisodes === 0) { + // Remove unmonitored seasons from Requests + const requestRepository = getRepository(MediaRequest); + const seasonRequestRepository = getRepository(SeasonRequest); + + const existingRequests = await requestRepository + .createQueryBuilder('request') + .innerJoinAndSelect('request.media', 'media') + .innerJoinAndSelect('request.seasons', 'seasons') + .where('media.tmdbId = :tmdbId', { tmdbId: tmdbId }) + .andWhere('media.mediaType = :mediaType', { + mediaType: MediaType.TV + }) + .andWhere('seasons.seasonNumber = :seasonNumber', { seasonNumber: season.seasonNumber }) + .getMany(); + + if (existingRequests && existingRequests.length > 0) { + existingRequests.forEach((existingRequest) => { + existingRequest.seasons.forEach(async (requestedSeason) => { + if (requestedSeason.seasonNumber === season.seasonNumber) { + this.log(`Removing request for Season ${season.seasonNumber} of ${sonarrSeries.title} as it is unmonitored`); + await seasonRequestRepository.remove(requestedSeason); + } + }); + }); + } + } + processableSeasons.push({ seasonNumber: season.seasonNumber, episodes: !server4k ? totalAvailableEpisodes : 0, @@ -119,6 +151,7 @@ class SonarrScanner totalEpisodes: season.statistics?.totalEpisodeCount ?? 0, processing: season.monitored && totalAvailableEpisodes === 0, is4kOverride: server4k, + monitored: season.monitored, }); } diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index 29447f534..eb20870b3 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -129,6 +129,7 @@ export interface MainSettings { trustProxy: boolean; mediaServerType: number; partialRequestsEnabled: boolean; + removeUnmonitoredFromRequestsEnabled: boolean; locale: string; proxy: ProxySettings; } @@ -151,6 +152,7 @@ interface FullPublicSettings extends PublicSettings { jellyfinForgotPasswordUrl?: string; jellyfinServerName?: string; partialRequestsEnabled: boolean; + removeUnmonitoredFromRequestsEnabled: boolean; cacheImages: boolean; vapidPublic: string; enablePushRegistration: boolean; @@ -336,6 +338,7 @@ class Settings { trustProxy: false, mediaServerType: MediaServerType.NOT_CONFIGURED, partialRequestsEnabled: true, + removeUnmonitoredFromRequestsEnabled: false, locale: 'en', proxy: { enabled: false, @@ -574,6 +577,7 @@ class Settings { originalLanguage: this.data.main.originalLanguage, mediaServerType: this.main.mediaServerType, partialRequestsEnabled: this.data.main.partialRequestsEnabled, + removeUnmonitoredFromRequestsEnabled: this.data.main.removeUnmonitoredFromRequestsEnabled, cacheImages: this.data.main.cacheImages, vapidPublic: this.vapidPublic, enablePushRegistration: this.data.notifications.agents.webpush.enabled, diff --git a/src/components/Settings/SettingsMain/index.tsx b/src/components/Settings/SettingsMain/index.tsx index 2d1e0219f..6aa11cbf6 100644 --- a/src/components/Settings/SettingsMain/index.tsx +++ b/src/components/Settings/SettingsMain/index.tsx @@ -54,6 +54,7 @@ const messages = defineMessages('components.Settings.SettingsMain', { validationApplicationUrl: 'You must provide a valid URL', validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash', partialRequestsEnabled: 'Allow Partial Series Requests', + removeUnmonitoredFromRequestsEnabled: 'Remove Request for Movies/Seasons that have been un-monitored since', locale: 'Display Language', proxyEnabled: 'HTTP(S) Proxy', proxyHostname: 'Proxy Hostname', @@ -152,6 +153,7 @@ const SettingsMain = () => { region: data?.region, originalLanguage: data?.originalLanguage, partialRequestsEnabled: data?.partialRequestsEnabled, + removeUnmonitoredFromRequestsEnabled: data?.removeUnmonitoredFromRequestsEnabled, trustProxy: data?.trustProxy, cacheImages: data?.cacheImages, proxyEnabled: data?.proxy?.enabled, @@ -181,6 +183,7 @@ const SettingsMain = () => { region: values.region, originalLanguage: values.originalLanguage, partialRequestsEnabled: values.partialRequestsEnabled, + removeUnmonitoredFromRequestsEnabled: values.removeUnmonitoredFromRequestsEnabled, trustProxy: values.trustProxy, cacheImages: values.cacheImages, proxy: { @@ -472,6 +475,29 @@ const SettingsMain = () => { /> +
+ +
+ { + setFieldValue( + 'removeUnmonitoredFromRequestsEnabled', + !values.removeUnmonitoredFromRequestsEnabled + ); + }} + /> +
+