Skip to content

Commit 2e0e4ce

Browse files
authored
fix(scanners): decline orphaned requests when media is removed from servarr (#3191)
1 parent 74c8db4 commit 2e0e4ce

5 files changed

Lines changed: 668 additions & 28 deletions

File tree

server/lib/scanners/baseScanner.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import TheMovieDb from '@server/api/themoviedb';
2-
import { MediaStatus, MediaType } from '@server/constants/media';
2+
import {
3+
MediaRequestStatus,
4+
MediaStatus,
5+
MediaType,
6+
} from '@server/constants/media';
37
import { getRepository } from '@server/datasource';
48
import Media from '@server/entity/Media';
9+
import MediaRequest from '@server/entity/MediaRequest';
510
import Season from '@server/entity/Season';
611
import { getSettings } from '@server/lib/settings';
712
import logger from '@server/logger';
@@ -637,6 +642,43 @@ class BaseScanner<T> {
637642
});
638643
}
639644

645+
/**
646+
* Declines APPROVED requests bound to media that has been orphaned before completion.
647+
* DECLINED clears the duplicate-request guard so the user can re-request it.
648+
* Callers must load the requests relation on the media.
649+
*/
650+
protected async declineOrphanedRequests(
651+
media: Media,
652+
is4k: boolean
653+
): Promise<void> {
654+
if (media.requests === undefined) {
655+
throw new Error(
656+
`declineOrphanedRequests called for media ${media.id} without the 'requests' relation loaded`
657+
);
658+
}
659+
660+
const requestRepository = getRepository(MediaRequest);
661+
662+
const orphanedRequests = (media.requests ?? []).filter(
663+
(request) =>
664+
request.is4k === is4k && request.status === MediaRequestStatus.APPROVED
665+
);
666+
667+
for (const request of orphanedRequests) {
668+
request.status = MediaRequestStatus.DECLINED;
669+
// Ensure that the media relation is set so the AfterUpdate
670+
// notification hook can resolve it
671+
request.media = media;
672+
await requestRepository.save(request);
673+
this.log(
674+
`Declined orphaned ${
675+
media.mediaType === MediaType.MOVIE ? 'movie' : 'series'
676+
} request ${request.id} for ${media.tmdbId} not found in any Sonarr/Radarr server.`,
677+
'info'
678+
);
679+
}
680+
}
681+
640682
/**
641683
* Call startRun from child class whenever a run is starting to
642684
* ensure required values are set

server/lib/scanners/radarr/index.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ class RadarrScanner
2828
private scanned4kTmdbIds: Set<number> = new Set();
2929
private didScanStandard = false;
3030
private didScan4k = false;
31+
private serverReturnedEmpty = false;
32+
private server4kReturnedEmpty = false;
3133

3234
constructor() {
3335
super('Radarr Scan', { bundleSize: 50 });
@@ -50,6 +52,8 @@ class RadarrScanner
5052
this.scanned4kTmdbIds.clear();
5153
this.didScanStandard = false;
5254
this.didScan4k = false;
55+
this.serverReturnedEmpty = false;
56+
this.server4kReturnedEmpty = false;
5357

5458
try {
5559
this.servers = uniqWith(settings.radarr, (radarrA, radarrB) => {
@@ -82,6 +86,18 @@ class RadarrScanner
8286
this.didScanStandard = true;
8387
}
8488

89+
if (this.items.length === 0) {
90+
if (server4k) {
91+
this.server4kReturnedEmpty = true;
92+
} else {
93+
this.serverReturnedEmpty = true;
94+
}
95+
this.log(
96+
`Radarr server ${server.name} returned no movies. Orphan cleanup for this profile type will be skipped.`,
97+
'warn'
98+
);
99+
}
100+
85101
await this.loop(this.processRadarrMovie.bind(this), { sessionId });
86102
} else {
87103
this.log(`Sync not enabled. Skipping Radarr server: ${server.name}`);
@@ -106,6 +122,13 @@ class RadarrScanner
106122
this.didScan4k = false;
107123
}
108124

125+
if (this.serverReturnedEmpty) {
126+
this.didScanStandard = false;
127+
}
128+
if (this.server4kReturnedEmpty) {
129+
this.didScan4k = false;
130+
}
131+
109132
await this.cleanupOrphanedMovies();
110133
this.log('Radarr scan complete', 'info');
111134
} catch (e) {
@@ -147,12 +170,14 @@ class RadarrScanner
147170
if (this.didScanStandard) {
148171
const processingMovies = await mediaRepository.find({
149172
where: { mediaType: MediaType.MOVIE, status: MediaStatus.PROCESSING },
173+
relations: { requests: true },
150174
});
151175

152176
for (const media of processingMovies) {
153177
if (!this.scannedTmdbIds.has(media.tmdbId)) {
154178
media.status = MediaStatus.UNKNOWN;
155179
await mediaRepository.save(media);
180+
await this.declineOrphanedRequests(media, false);
156181
this.log(
157182
`Movie ${media.tmdbId} not found in any Radarr server. Status reset to UNKNOWN.`,
158183
'info'
@@ -172,12 +197,14 @@ class RadarrScanner
172197
mediaType: MediaType.MOVIE,
173198
status4k: MediaStatus.PROCESSING,
174199
},
200+
relations: { requests: true },
175201
});
176202

177203
for (const media of processing4kMovies) {
178204
if (!this.scanned4kTmdbIds.has(media.tmdbId)) {
179205
media.status4k = MediaStatus.UNKNOWN;
180206
await mediaRepository.save(media);
207+
await this.declineOrphanedRequests(media, true);
181208
this.log(
182209
`Movie ${media.tmdbId} not found in any 4K Radarr server. 4K status reset to UNKNOWN.`,
183210
'info'

0 commit comments

Comments
 (0)