Skip to content

Commit caaf13e

Browse files
committed
feat(scanners): remove requests of unmonitored movies/seasons during scan
When a movie / show - which was monitored before - is unmonitored, it won't appear as "requested" in Jellyseerr anymore, if removeUnmonitoredEnabled option is set to true re #695
1 parent 89831f7 commit caaf13e

File tree

11 files changed

+131
-11
lines changed

11 files changed

+131
-11
lines changed

cypress/config/settings.cypress.json

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"trustProxy": false,
2323
"mediaServerType": 1,
2424
"partialRequestsEnabled": true,
25+
"removeUnmonitoredEnabled": false,
2526
"locale": "en"
2627
},
2728
"plex": {

overseerr-api.yml

+3
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,9 @@ components:
176176
partialRequestsEnabled:
177177
type: boolean
178178
example: false
179+
removeUnmonitoredEnabled:
180+
type: boolean
181+
example: false
179182
localLogin:
180183
type: boolean
181184
example: true

server/interfaces/api/settingsInterfaces.ts

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export interface PublicSettingsResponse {
3737
originalLanguage: string;
3838
mediaServerType: number;
3939
partialRequestsEnabled: boolean;
40+
removeUnmonitoredEnabled: boolean;
4041
cacheImages: boolean;
4142
vapidPublic: string;
4243
enablePushRegistration: boolean;

server/lib/scanners/baseScanner.ts

+68-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import type { SonarrSeason } from '@server/api/servarr/sonarr';
12
import TheMovieDb from '@server/api/themoviedb';
23
import { MediaStatus, MediaType } from '@server/constants/media';
34
import { getRepository } from '@server/datasource';
45
import Media from '@server/entity/Media';
6+
import { MediaRequest } from '@server/entity/MediaRequest';
57
import Season from '@server/entity/Season';
8+
import SeasonRequest from '@server/entity/SeasonRequest';
69
import { getSettings } from '@server/lib/settings';
710
import logger from '@server/logger';
811
import AsyncLock from '@server/utils/asyncLock';
@@ -48,6 +51,7 @@ export interface ProcessableSeason {
4851
episodes4k: number;
4952
is4kOverride?: boolean;
5053
processing?: boolean;
54+
monitored?: boolean;
5155
}
5256

5357
class BaseScanner<T> {
@@ -211,7 +215,7 @@ class BaseScanner<T> {
211215

212216
/**
213217
* processShow takes a TMDB ID and an array of ProcessableSeasons, which
214-
* should include the total episodes a sesaon has + the total available
218+
* should include the total episodes a season has + the total available
215219
* episodes that each season currently has. Unlike processMovie, this method
216220
* does not take an `is4k` option. We handle both the 4k _and_ non 4k status
217221
* in one method.
@@ -234,6 +238,7 @@ class BaseScanner<T> {
234238
}: ProcessOptions = {}
235239
): Promise<void> {
236240
const mediaRepository = getRepository(Media);
241+
const settings = getSettings();
237242

238243
await this.asyncLock.dispatch(tmdbId, async () => {
239244
const media = await this.getExisting(tmdbId, MediaType.TV);
@@ -277,25 +282,35 @@ class BaseScanner<T> {
277282
// force it to stay available (to avoid competing scanners)
278283
existingSeason.status =
279284
(season.totalEpisodes === season.episodes && season.episodes > 0) ||
280-
existingSeason.status === MediaStatus.AVAILABLE
285+
(existingSeason.status === MediaStatus.AVAILABLE &&
286+
season.episodes > 0)
281287
? MediaStatus.AVAILABLE
282288
: season.episodes > 0
283289
? MediaStatus.PARTIALLY_AVAILABLE
284290
: !season.is4kOverride && season.processing
285291
? MediaStatus.PROCESSING
292+
: settings.main.removeUnmonitoredEnabled &&
293+
!season.monitored &&
294+
season.episodes == 0
295+
? MediaStatus.UNKNOWN
286296
: existingSeason.status;
287297

288298
// Same thing here, except we only do updates if 4k is enabled
289299
existingSeason.status4k =
290300
(this.enable4kShow &&
291301
season.episodes4k === season.totalEpisodes &&
292302
season.episodes4k > 0) ||
293-
existingSeason.status4k === MediaStatus.AVAILABLE
303+
(existingSeason.status4k === MediaStatus.AVAILABLE &&
304+
season.episodes > 0)
294305
? MediaStatus.AVAILABLE
295306
: this.enable4kShow && season.episodes4k > 0
296307
? MediaStatus.PARTIALLY_AVAILABLE
297308
: season.is4kOverride && season.processing
298309
? MediaStatus.PROCESSING
310+
: settings.main.removeUnmonitoredEnabled &&
311+
!season.monitored &&
312+
season.episodes4k == 0
313+
? MediaStatus.UNKNOWN
299314
: existingSeason.status4k;
300315
} else {
301316
newSeasons.push(
@@ -618,6 +633,56 @@ class BaseScanner<T> {
618633
get protectedBundleSize(): number {
619634
return this.bundleSize;
620635
}
636+
637+
protected async processUnmonitoredMovie(tmdbId: number): Promise<void> {
638+
const mediaRepository = getRepository(Media);
639+
await this.asyncLock.dispatch(tmdbId, async () => {
640+
const existing = await this.getExisting(tmdbId, MediaType.MOVIE);
641+
if (existing && existing.status === MediaStatus.PROCESSING) {
642+
existing.status = MediaStatus.UNKNOWN;
643+
await mediaRepository.save(existing);
644+
this.log(
645+
`Movie TMDB ID ${tmdbId} unmonitored from Radarr. Media status set to UNKNOWN.`,
646+
'info'
647+
);
648+
}
649+
});
650+
}
651+
652+
protected async processUnmonitoredSeason(
653+
tmdbId: number,
654+
season: SonarrSeason
655+
): Promise<void> {
656+
// Remove unmonitored seasons from Requests
657+
const requestRepository = getRepository(MediaRequest);
658+
const seasonRequestRepository = getRepository(SeasonRequest);
659+
660+
const existingRequests = await requestRepository
661+
.createQueryBuilder('request')
662+
.innerJoinAndSelect('request.media', 'media')
663+
.innerJoinAndSelect('request.seasons', 'seasons')
664+
.where('media.tmdbId = :tmdbId', { tmdbId: tmdbId })
665+
.andWhere('media.mediaType = :mediaType', {
666+
mediaType: MediaType.TV,
667+
})
668+
.andWhere('seasons.seasonNumber = :seasonNumber', {
669+
seasonNumber: season.seasonNumber,
670+
})
671+
.getMany();
672+
673+
if (existingRequests && existingRequests.length > 0) {
674+
for (const existingRequest of existingRequests) {
675+
for (const requestedSeason of existingRequest.seasons) {
676+
if (requestedSeason.seasonNumber === season.seasonNumber) {
677+
this.log(
678+
`Removing request for Season ${season.seasonNumber} of tmdbId ${tmdbId} as it is unmonitored`
679+
);
680+
await seasonRequestRepository.remove(requestedSeason);
681+
}
682+
}
683+
}
684+
}
685+
}
621686
}
622687

623688
export default BaseScanner;

server/lib/scanners/radarr/index.ts

+7-8
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,13 @@ class RadarrScanner
7979
}
8080

8181
private async processRadarrMovie(radarrMovie: RadarrMovie): Promise<void> {
82-
if (!radarrMovie.monitored && !radarrMovie.hasFile) {
83-
this.log(
84-
'Title is unmonitored and has not been downloaded. Skipping item.',
85-
'debug',
86-
{
87-
title: radarrMovie.title,
88-
}
89-
);
82+
const settings = getSettings();
83+
if (
84+
settings.main.removeUnmonitoredEnabled &&
85+
!radarrMovie.monitored &&
86+
!radarrMovie.hasFile
87+
) {
88+
this.processUnmonitoredMovie(radarrMovie.tmdbId);
9089
return;
9190
}
9291

server/lib/scanners/sonarr/index.ts

+10
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ class SonarrScanner
8585
private async processSonarrSeries(sonarrSeries: SonarrSeries) {
8686
try {
8787
const mediaRepository = getRepository(Media);
88+
const settings = getSettings();
8889
const server4k = this.enable4kShow && this.currentServer.is4k;
8990
const processableSeasons: ProcessableSeason[] = [];
9091
let tvShow: TmdbTvDetails;
@@ -110,13 +111,22 @@ class SonarrScanner
110111
for (const season of filteredSeasons) {
111112
const totalAvailableEpisodes = season.statistics?.episodeFileCount ?? 0;
112113

114+
if (
115+
settings.main.removeUnmonitoredEnabled &&
116+
season.monitored === false &&
117+
totalAvailableEpisodes === 0
118+
) {
119+
this.processUnmonitoredSeason(tmdbId, season);
120+
}
121+
113122
processableSeasons.push({
114123
seasonNumber: season.seasonNumber,
115124
episodes: !server4k ? totalAvailableEpisodes : 0,
116125
episodes4k: server4k ? totalAvailableEpisodes : 0,
117126
totalEpisodes: season.statistics?.totalEpisodeCount ?? 0,
118127
processing: season.monitored && totalAvailableEpisodes === 0,
119128
is4kOverride: server4k,
129+
monitored: season.monitored,
120130
});
121131
}
122132

server/lib/settings/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ export interface MainSettings {
130130
trustProxy: boolean;
131131
mediaServerType: number;
132132
partialRequestsEnabled: boolean;
133+
removeUnmonitoredEnabled: boolean;
133134
locale: string;
134135
proxy: ProxySettings;
135136
}
@@ -153,6 +154,7 @@ interface FullPublicSettings extends PublicSettings {
153154
jellyfinForgotPasswordUrl?: string;
154155
jellyfinServerName?: string;
155156
partialRequestsEnabled: boolean;
157+
removeUnmonitoredEnabled: boolean;
156158
cacheImages: boolean;
157159
vapidPublic: string;
158160
enablePushRegistration: boolean;
@@ -341,6 +343,7 @@ class Settings {
341343
trustProxy: false,
342344
mediaServerType: MediaServerType.NOT_CONFIGURED,
343345
partialRequestsEnabled: true,
346+
removeUnmonitoredEnabled: false,
344347
locale: 'en',
345348
proxy: {
346349
enabled: false,
@@ -584,6 +587,7 @@ class Settings {
584587
originalLanguage: this.data.main.originalLanguage,
585588
mediaServerType: this.main.mediaServerType,
586589
partialRequestsEnabled: this.data.main.partialRequestsEnabled,
590+
removeUnmonitoredEnabled: this.data.main.removeUnmonitoredEnabled,
587591
cacheImages: this.data.main.cacheImages,
588592
vapidPublic: this.vapidPublic,
589593
enablePushRegistration: this.data.notifications.agents.webpush.enabled,

src/components/Settings/SettingsMain/index.tsx

+34
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ const messages = defineMessages('components.Settings.SettingsMain', {
5656
validationApplicationUrl: 'You must provide a valid URL',
5757
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
5858
partialRequestsEnabled: 'Allow Partial Series Requests',
59+
removeUnmonitoredEnabled: 'Remove Unmonitored Media',
60+
removeUnmonitoredExplanation:
61+
'Remove Movies/Seasons from Jellyseerr that are not available and have been un-monitored since',
5962
locale: 'Display Language',
6063
proxyEnabled: 'HTTP(S) Proxy',
6164
proxyHostname: 'Proxy Hostname',
@@ -158,6 +161,7 @@ const SettingsMain = () => {
158161
originalLanguage: data?.originalLanguage,
159162
streamingRegion: data?.streamingRegion,
160163
partialRequestsEnabled: data?.partialRequestsEnabled,
164+
removeUnmonitoredEnabled: data?.removeUnmonitoredEnabled,
161165
trustProxy: data?.trustProxy,
162166
cacheImages: data?.cacheImages,
163167
proxyEnabled: data?.proxy?.enabled,
@@ -188,6 +192,7 @@ const SettingsMain = () => {
188192
streamingRegion: values.streamingRegion,
189193
originalLanguage: values.originalLanguage,
190194
partialRequestsEnabled: values.partialRequestsEnabled,
195+
removeUnmonitoredEnabled: values.removeUnmonitoredEnabled,
191196
trustProxy: values.trustProxy,
192197
cacheImages: values.cacheImages,
193198
proxy: {
@@ -498,6 +503,35 @@ const SettingsMain = () => {
498503
/>
499504
</div>
500505
</div>
506+
<div className="form-row">
507+
<label
508+
htmlFor="removeUnmonitoredEnabled"
509+
className="checkbox-label"
510+
>
511+
<span className="mr-2">
512+
{intl.formatMessage(messages.removeUnmonitoredEnabled)}
513+
</span>
514+
<SettingsBadge badgeType="experimental" />
515+
<span className="label-tip">
516+
{intl.formatMessage(
517+
messages.removeUnmonitoredExplanation
518+
)}
519+
</span>
520+
</label>
521+
<div className="form-input-area">
522+
<Field
523+
type="checkbox"
524+
id="removeUnmonitoredEnabled"
525+
name="removeUnmonitoredEnabled"
526+
onChange={() => {
527+
setFieldValue(
528+
'removeUnmonitoredEnabled',
529+
!values.removeUnmonitoredEnabled
530+
);
531+
}}
532+
/>
533+
</div>
534+
</div>
501535
<div className="form-row">
502536
<label htmlFor="proxyEnabled" className="checkbox-label">
503537
<span className="mr-2">

src/context/SettingsContext.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const defaultSettings = {
2121
originalLanguage: '',
2222
mediaServerType: MediaServerType.NOT_CONFIGURED,
2323
partialRequestsEnabled: true,
24+
removeUnmonitoredEnabled: false,
2425
cacheImages: false,
2526
vapidPublic: '',
2627
enablePushRegistration: false,

src/i18n/locale/en.json

+1
Original file line numberDiff line numberDiff line change
@@ -901,6 +901,7 @@
901901
"components.Settings.SettingsMain.proxyUser": "Proxy Username",
902902
"components.Settings.SettingsMain.streamingRegion": "Streaming Region",
903903
"components.Settings.SettingsMain.streamingRegionTip": "Show streaming sites by regional availability",
904+
"components.Settings.SettingsMain.removeUnmonitoredFromRequestsEnabled": "Remove Request for Movies/Seasons that have been un-monitored since",
904905
"components.Settings.SettingsMain.toastApiKeyFailure": "Something went wrong while generating a new API key.",
905906
"components.Settings.SettingsMain.toastApiKeySuccess": "New API key generated successfully!",
906907
"components.Settings.SettingsMain.toastSettingsFailure": "Something went wrong while saving settings.",

src/pages/_app.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ CoreApp.getInitialProps = async (initialProps) => {
197197
originalLanguage: '',
198198
mediaServerType: MediaServerType.NOT_CONFIGURED,
199199
partialRequestsEnabled: true,
200+
removeUnmonitoredEnabled: false,
200201
cacheImages: false,
201202
vapidPublic: '',
202203
enablePushRegistration: false,

0 commit comments

Comments
 (0)