From cee1b88d7048e1b5b0522399699e68c8edbf7e37 Mon Sep 17 00:00:00 2001 From: Keir Innes <78006756+kinn81@users.noreply.github.com> Date: Tue, 22 Sep 2026 10:31:28 +1200 Subject: [PATCH] fix(radarr): initiate Radarr movie search only if movie is available --- server/api/servarr/radarr.test.ts | 140 ++++++++++++++++++++++++++++++ server/api/servarr/radarr.ts | 13 ++- 2 files changed, 149 insertions(+), 4 deletions(-) diff --git a/server/api/servarr/radarr.test.ts b/server/api/servarr/radarr.test.ts index f3aaed3848..fff3984bcc 100644 --- a/server/api/servarr/radarr.test.ts +++ b/server/api/servarr/radarr.test.ts @@ -3,6 +3,7 @@ import { afterEach, describe, it, mock } from 'node:test'; import type { AxiosInstance } from 'axios'; +import type { RadarrMovieOptions } from '@server/api/servarr/radarr'; import RadarrAPI from '@server/api/servarr/radarr'; function buildRadarr(): RadarrAPI { @@ -117,3 +118,142 @@ describe('RadarrAPI getMovieByTmdbId', () => { }); }); }); + +describe('RadarrAPI addMovie', () => { + afterEach(() => mock.restoreAll()); + + it('does not trigger a search for an existing, monitored movie that is not yet available', async () => { + const radarr = buildRadarr(); + const options: RadarrMovieOptions = { + title: 'Test Movie', + qualityProfileId: 1, + minimumAvailability: 'Released', + tags: [], + profileId: 1, + year: 2026, + rootFolderPath: '/movies', + tmdbId: 7, + searchNow: true, + }; + mock.method(RadarrAPI.prototype, 'getMovieByTmdbId', async () => ({ + id: 7, + title: 'Test Movie', + monitored: true, + hasFile: false, + isAvailable: false, + })); + + const search = mock.method( + RadarrAPI.prototype, + 'searchMovie', + async () => {} + ); + await radarr.addMovie(options); + + assert.strictEqual(search.mock.callCount(), 0); + }); + + it('triggers a search for an existing, monitored movie that is already available', async () => { + const radarr = buildRadarr(); + const options: RadarrMovieOptions = { + title: 'Test Movie', + qualityProfileId: 1, + minimumAvailability: 'Released', + tags: [], + profileId: 1, + year: 2026, + rootFolderPath: '/movies', + tmdbId: 7, + searchNow: true, + }; + mock.method(RadarrAPI.prototype, 'getMovieByTmdbId', async () => ({ + id: 7, + title: 'Test Movie', + monitored: true, + hasFile: false, + isAvailable: true, + })); + + const search = mock.method( + RadarrAPI.prototype, + 'searchMovie', + async () => {} + ); + await radarr.addMovie(options); + + assert.strictEqual(search.mock.callCount(), 1); + }); + + it('does not trigger a search for an existing, previously unmonitored movie that is not yet available', async () => { + const radarr = buildRadarr(); + const options: RadarrMovieOptions = { + title: 'Test Movie', + qualityProfileId: 1, + minimumAvailability: 'Released', + tags: [], + profileId: 1, + year: 2026, + rootFolderPath: '/movies', + tmdbId: 7, + monitored: true, + searchNow: true, + }; + mock.method(RadarrAPI.prototype, 'getMovieByTmdbId', async () => ({ + id: 7, + title: 'Test Movie', + monitored: false, + hasFile: false, + tags: [], + })); + const put = mock.method(getAxios(radarr), 'put', async () => ({ + data: { id: 7, title: 'Test Movie', monitored: true, isAvailable: false }, + })); + + const search = mock.method( + RadarrAPI.prototype, + 'searchMovie', + async () => {} + ); + await radarr.addMovie(options); + + assert.strictEqual(put.mock.callCount(), 1); + assert.strictEqual(search.mock.callCount(), 0); + }); + + it('triggers a search for an existing, previously unmonitored movie that is already available', async () => { + const radarr = buildRadarr(); + const options: RadarrMovieOptions = { + title: 'Test Movie', + qualityProfileId: 1, + minimumAvailability: 'Released', + tags: [], + profileId: 1, + year: 2026, + rootFolderPath: '/movies', + tmdbId: 7, + monitored: true, + searchNow: true, + }; + mock.method(RadarrAPI.prototype, 'getMovieByTmdbId', async () => ({ + id: 7, + title: 'Test Movie', + monitored: false, + hasFile: false, + tags: [], + })); + const put = mock.method(getAxios(radarr), 'put', async () => ({ + data: { id: 7, title: 'Test Movie', monitored: true, isAvailable: true }, + })); + + const search = mock.method( + RadarrAPI.prototype, + 'searchMovie', + async () => {} + ); + await radarr.addMovie(options); + + assert.strictEqual(put.mock.callCount(), 1); + assert.strictEqual(search.mock.callCount(), 1); + assert.strictEqual(search.mock.calls[0].arguments[0], 7); + }); +}); diff --git a/server/api/servarr/radarr.ts b/server/api/servarr/radarr.ts index e3b231cb03..611dd26e03 100644 --- a/server/api/servarr/radarr.ts +++ b/server/api/servarr/radarr.ts @@ -184,7 +184,12 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> { movie: response.data, }); - if (options.searchNow) { + if (options.searchNow && response.data.isAvailable) { + logger.info('Movie is available, triggering Radarr search.', { + label: 'Radarr', + movieId: response.data.id, + movieTitle: response.data.title, + }); this.searchMovie(response.data.id); } @@ -207,10 +212,10 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> { hasFile: movie.hasFile, }); - // If searchNow is requested and movie doesn't have a file, trigger search - if (options.searchNow && !movie.hasFile) { + // If searchNow is requested, movie doesn't have a file, and Radarr considers the movie 'available' then trigger search + if (options.searchNow && !movie.hasFile && movie.isAvailable) { logger.info( - 'Triggering search for existing monitored movie without file', + 'Triggering search for existing, available, monitored movie without file', { label: 'Radarr', movieId: movie.id,