Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 27 additions & 8 deletions server/lib/scanners/baseScanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ interface ProcessOptions {
externalServiceSlug?: string;
title?: string;
processing?: boolean;
hasFile?: boolean;
}

export interface ProcessableSeason {
Expand Down Expand Up @@ -104,6 +105,7 @@ class BaseScanner<T> {
externalServiceSlug,
processing = false,
title = 'Unknown Title',
hasFile = true,
}: ProcessOptions = {}
): Promise<void> {
const mediaRepository = getRepository(Media);
Expand All @@ -115,15 +117,28 @@ class BaseScanner<T> {
let changedExisting = false;

if (existing[is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE) {
existing[is4k ? 'status4k' : 'status'] = !processing
? MediaStatus.AVAILABLE
: existing[is4k ? 'status4k' : 'status'] === MediaStatus.DELETED
? MediaStatus.DELETED
: MediaStatus.PROCESSING;
if (mediaAddedAt) {
existing.mediaAddedAt = mediaAddedAt;
const statusField = is4k ? 'status4k' : 'status';
const previousStatus = existing[statusField];

existing[statusField] =
!processing && hasFile
? MediaStatus.AVAILABLE
: !processing &&
!hasFile &&
previousStatus === MediaStatus.PROCESSING
? MediaStatus.UNKNOWN
: processing
? previousStatus === MediaStatus.DELETED
? MediaStatus.DELETED
: MediaStatus.PROCESSING
: previousStatus;

if (existing[statusField] !== previousStatus) {
if (mediaAddedAt) {
existing.mediaAddedAt = mediaAddedAt;
}
changedExisting = true;
}
changedExisting = true;
}

if (!changedExisting && !existing.mediaAddedAt && mediaAddedAt) {
Expand Down Expand Up @@ -192,6 +207,10 @@ class BaseScanner<T> {
this.log(`Title already exists and no changes detected for ${title}`);
}
} else {
if (!processing && !hasFile) {
return;
}

const newMedia = new Media();
newMedia.tmdbId = tmdbId;
newMedia.imdbId = imdbId;
Expand Down
106 changes: 95 additions & 11 deletions server/lib/scanners/radarr/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { RadarrMovie } from '@server/api/servarr/radarr';
import RadarrAPI from '@server/api/servarr/radarr';
import { MediaStatus, MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
import type {
RunnableScanner,
StatusBase,
Expand All @@ -21,6 +24,10 @@ class RadarrScanner
private servers: RadarrSettings[];
private currentServer: RadarrSettings;
private radarrApi: RadarrAPI;
private scannedTmdbIds: Set<number> = new Set();
private scanned4kTmdbIds: Set<number> = new Set();
private didScanStandard = false;
private didScan4k = false;

constructor() {
super('Radarr Scan', { bundleSize: 50 });
Expand All @@ -39,6 +46,10 @@ class RadarrScanner
public async run(): Promise<void> {
const settings = getSettings();
const sessionId = this.startRun();
this.scannedTmdbIds.clear();
this.scanned4kTmdbIds.clear();
this.didScanStandard = false;
this.didScan4k = false;

try {
this.servers = uniqWith(settings.radarr, (radarrA, radarrB) => {
Expand All @@ -64,12 +75,38 @@ class RadarrScanner

this.items = await this.radarrApi.getMovies();

const server4k = this.enable4kMovie && server.is4k;
if (server4k) {
this.didScan4k = true;
} else {
this.didScanStandard = true;
}

await this.loop(this.processRadarrMovie.bind(this), { sessionId });
} else {
this.log(`Sync not enabled. Skipping Radarr server: ${server.name}`);
}
}

// Only run cleanup if all servers of this profile type have sync enabled.
// If any server is skipped, we can't distinguish truly orphaned media from
// media that exists on an unscanned server (e.g. separate instances for
// anime, regional content, or different languages).
const allStandardScanned = this.servers
.filter((s) => !this.enable4kMovie || !s.is4k)
.every((s) => s.syncEnabled);
const all4kScanned = this.servers
.filter((s) => this.enable4kMovie && s.is4k)
.every((s) => s.syncEnabled);

if (!allStandardScanned) {
this.didScanStandard = false;
}
if (!all4kScanned) {
this.didScan4k = false;
}

await this.cleanupOrphanedMovies();
this.log('Radarr scan complete', 'info');
} catch (e) {
this.log('Scan interrupted', 'error', { errorMessage: e.message });
Expand All @@ -79,26 +116,22 @@ class RadarrScanner
}

private async processRadarrMovie(radarrMovie: RadarrMovie): Promise<void> {
if (!radarrMovie.monitored && !radarrMovie.hasFile) {
this.log(
'Title is unmonitored and has not been downloaded. Skipping item.',
'debug',
{
title: radarrMovie.title,
}
);
return;
const server4k = this.enable4kMovie && this.currentServer.is4k;
if (server4k) {
this.scanned4kTmdbIds.add(radarrMovie.tmdbId);
} else {
this.scannedTmdbIds.add(radarrMovie.tmdbId);
}

try {
const server4k = this.enable4kMovie && this.currentServer.is4k;
await this.processMovie(radarrMovie.tmdbId, {
is4k: server4k,
serviceId: this.currentServer.id,
externalServiceId: radarrMovie.id,
externalServiceSlug: radarrMovie.titleSlug,
title: radarrMovie.title,
processing: !radarrMovie.hasFile,
processing: !radarrMovie.hasFile && radarrMovie.monitored,
hasFile: radarrMovie.hasFile,
});
} catch (e) {
this.log('Failed to process Radarr media', 'error', {
Expand All @@ -107,6 +140,57 @@ class RadarrScanner
});
}
}

private async cleanupOrphanedMovies(): Promise<void> {
const mediaRepository = getRepository(Media);

if (this.didScanStandard) {
const processingMovies = await mediaRepository.find({
where: { mediaType: MediaType.MOVIE, status: MediaStatus.PROCESSING },
});

for (const media of processingMovies) {
if (!this.scannedTmdbIds.has(media.tmdbId)) {
media.status = MediaStatus.UNKNOWN;
await mediaRepository.save(media);
this.log(
`Movie ${media.tmdbId} not found in any Radarr server. Status reset to UNKNOWN.`,
'info'
);
}
}
} else {
this.log(
'Skipping orphaned movie cleanup: no standard Radarr servers were scanned.',
'info'
);
}

if (this.didScan4k) {
const processing4kMovies = await mediaRepository.find({
where: {
mediaType: MediaType.MOVIE,
status4k: MediaStatus.PROCESSING,
},
});

for (const media of processing4kMovies) {
if (!this.scanned4kTmdbIds.has(media.tmdbId)) {
media.status4k = MediaStatus.UNKNOWN;
await mediaRepository.save(media);
this.log(
`Movie ${media.tmdbId} not found in any 4K Radarr server. 4K status reset to UNKNOWN.`,
'info'
);
}
}
} else if (this.enable4kMovie) {
this.log(
'Skipping orphaned 4K movie cleanup: no 4K Radarr servers were scanned.',
'info'
);
}
}
}

export const radarrScanner = new RadarrScanner();
Loading
Loading