mirror of
https://github.com/seerr-team/seerr.git
synced 2026-06-02 13:20:22 -04:00
400 lines
12 KiB
TypeScript
400 lines
12 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { beforeEach, describe, it } from 'node:test';
|
|
|
|
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 { RadarrSettings } from '@server/lib/settings';
|
|
import { getSettings } from '@server/lib/settings';
|
|
import { setupTestDb } from '@server/test/db';
|
|
|
|
let getMoviesImpl: () => Promise<RadarrMovie[]> = async () => [];
|
|
Object.defineProperty(RadarrAPI.prototype, 'getMovies', {
|
|
set() {},
|
|
get() {
|
|
return async () => getMoviesImpl();
|
|
},
|
|
configurable: true,
|
|
});
|
|
|
|
import { radarrScanner } from '@server/lib/scanners/radarr';
|
|
|
|
setupTestDb();
|
|
|
|
function configureRadarr(overrides: Partial<RadarrSettings>[] = [{}]): void {
|
|
const settings = getSettings();
|
|
settings.radarr = overrides.map((o, i) => ({
|
|
id: i,
|
|
name: `Radarr ${i}`,
|
|
hostname: 'localhost',
|
|
port: 7878,
|
|
apiKey: 'test-key',
|
|
baseUrl: '',
|
|
useSsl: false,
|
|
activeProfileId: 1,
|
|
activeDirectory: '/movies',
|
|
is4k: false,
|
|
minimumAvailability: 'released',
|
|
tags: [],
|
|
isDefault: i === 0,
|
|
syncEnabled: true,
|
|
preventSearch: false,
|
|
externalUrl: '',
|
|
...o,
|
|
})) as RadarrSettings[];
|
|
settings.sonarr = [];
|
|
}
|
|
|
|
function fakeRadarrMovie(overrides: Partial<RadarrMovie> = {}): RadarrMovie {
|
|
return {
|
|
tmdbId: 550,
|
|
id: 1,
|
|
title: 'Test Movie',
|
|
titleSlug: 'test-movie',
|
|
monitored: true,
|
|
hasFile: true,
|
|
isAvailable: true,
|
|
imdbId: 'tt0137523',
|
|
folderName: '/movies/Test Movie (2024)',
|
|
path: '/movies/Test Movie (2024)',
|
|
profileId: 1,
|
|
qualityProfileId: 1,
|
|
added: '2024-01-01T00:00:00Z',
|
|
tags: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('Radarr Scanner', () => {
|
|
beforeEach(() => {
|
|
getMoviesImpl = async () => [];
|
|
});
|
|
|
|
describe('unmonitored movie handling', () => {
|
|
it('resets PROCESSING to UNKNOWN when movie is unmonitored and has no file', async () => {
|
|
const mediaRepository = getRepository(Media);
|
|
|
|
const media = new Media();
|
|
media.tmdbId = 550;
|
|
media.mediaType = MediaType.MOVIE;
|
|
media.status = MediaStatus.PROCESSING;
|
|
await mediaRepository.save(media);
|
|
|
|
configureRadarr([{ syncEnabled: true }]);
|
|
getMoviesImpl = async () => [
|
|
fakeRadarrMovie({ monitored: false, hasFile: false }),
|
|
];
|
|
|
|
await radarrScanner.run();
|
|
|
|
const updated = await mediaRepository.findOneOrFail({
|
|
where: { tmdbId: 550 },
|
|
});
|
|
assert.strictEqual(updated.status, MediaStatus.UNKNOWN);
|
|
});
|
|
|
|
it('does not create new media entry when movie is unmonitored and has no file', async () => {
|
|
const mediaRepository = getRepository(Media);
|
|
configureRadarr([{ syncEnabled: true }]);
|
|
|
|
getMoviesImpl = async () => [
|
|
fakeRadarrMovie({ tmdbId: 777, monitored: false, hasFile: false }),
|
|
];
|
|
|
|
await radarrScanner.run();
|
|
|
|
const media = await mediaRepository.findOne({
|
|
where: { tmdbId: 777 },
|
|
});
|
|
assert.strictEqual(media, null);
|
|
});
|
|
|
|
it('sets AVAILABLE when movie has a file', async () => {
|
|
const mediaRepository = getRepository(Media);
|
|
|
|
const media = new Media();
|
|
media.tmdbId = 551;
|
|
media.mediaType = MediaType.MOVIE;
|
|
media.status = MediaStatus.PROCESSING;
|
|
await mediaRepository.save(media);
|
|
|
|
configureRadarr([{ syncEnabled: true }]);
|
|
getMoviesImpl = async () => [
|
|
fakeRadarrMovie({ tmdbId: 551, monitored: true, hasFile: true }),
|
|
];
|
|
|
|
await radarrScanner.run();
|
|
|
|
const updated = await mediaRepository.findOneOrFail({
|
|
where: { tmdbId: 551 },
|
|
});
|
|
assert.strictEqual(updated.status, MediaStatus.AVAILABLE);
|
|
});
|
|
|
|
it('sets PROCESSING when movie is monitored but has no file', async () => {
|
|
const mediaRepository = getRepository(Media);
|
|
|
|
const media = new Media();
|
|
media.tmdbId = 552;
|
|
media.mediaType = MediaType.MOVIE;
|
|
media.status = MediaStatus.UNKNOWN;
|
|
await mediaRepository.save(media);
|
|
|
|
configureRadarr([{ syncEnabled: true }]);
|
|
getMoviesImpl = async () => [
|
|
fakeRadarrMovie({ tmdbId: 552, monitored: true, hasFile: false }),
|
|
];
|
|
|
|
await radarrScanner.run();
|
|
|
|
const updated = await mediaRepository.findOneOrFail({
|
|
where: { tmdbId: 552 },
|
|
});
|
|
assert.strictEqual(updated.status, MediaStatus.PROCESSING);
|
|
});
|
|
|
|
it('preserves DELETED status when movie is monitored but has no file', async () => {
|
|
const mediaRepository = getRepository(Media);
|
|
|
|
const media = new Media();
|
|
media.tmdbId = 553;
|
|
media.mediaType = MediaType.MOVIE;
|
|
media.status = MediaStatus.DELETED;
|
|
await mediaRepository.save(media);
|
|
|
|
configureRadarr([{ syncEnabled: true }]);
|
|
getMoviesImpl = async () => [
|
|
fakeRadarrMovie({ tmdbId: 553, monitored: true, hasFile: false }),
|
|
];
|
|
|
|
await radarrScanner.run();
|
|
|
|
const updated = await mediaRepository.findOneOrFail({
|
|
where: { tmdbId: 553 },
|
|
});
|
|
assert.strictEqual(updated.status, MediaStatus.DELETED);
|
|
});
|
|
|
|
it('keeps AVAILABLE status even when movie is unmonitored', async () => {
|
|
const mediaRepository = getRepository(Media);
|
|
|
|
const media = new Media();
|
|
media.tmdbId = 554;
|
|
media.mediaType = MediaType.MOVIE;
|
|
media.status = MediaStatus.AVAILABLE;
|
|
await mediaRepository.save(media);
|
|
|
|
configureRadarr([{ syncEnabled: true }]);
|
|
getMoviesImpl = async () => [
|
|
fakeRadarrMovie({ tmdbId: 554, monitored: false, hasFile: true }),
|
|
];
|
|
|
|
await radarrScanner.run();
|
|
|
|
const updated = await mediaRepository.findOneOrFail({
|
|
where: { tmdbId: 554 },
|
|
});
|
|
assert.strictEqual(updated.status, MediaStatus.AVAILABLE);
|
|
});
|
|
});
|
|
|
|
describe('orphaned movie cleanup', () => {
|
|
it('skips cleanup when a standard server has sync disabled', async () => {
|
|
const mediaRepository = getRepository(Media);
|
|
|
|
const media = new Media();
|
|
media.tmdbId = 950;
|
|
media.mediaType = MediaType.MOVIE;
|
|
media.status = MediaStatus.PROCESSING;
|
|
await mediaRepository.save(media);
|
|
|
|
configureRadarr([
|
|
{ syncEnabled: true, id: 0, hostname: 'server-a' },
|
|
{ syncEnabled: false, id: 1, hostname: 'server-b' },
|
|
]);
|
|
|
|
getMoviesImpl = async () => [];
|
|
|
|
await radarrScanner.run();
|
|
|
|
const updated = await mediaRepository.findOneOrFail({
|
|
where: { tmdbId: 950 },
|
|
});
|
|
assert.strictEqual(updated.status, MediaStatus.PROCESSING);
|
|
});
|
|
|
|
it('resets PROCESSING to UNKNOWN when movie is not in any Radarr server', async () => {
|
|
const mediaRepository = getRepository(Media);
|
|
|
|
const media = new Media();
|
|
media.tmdbId = 999;
|
|
media.mediaType = MediaType.MOVIE;
|
|
media.status = MediaStatus.PROCESSING;
|
|
await mediaRepository.save(media);
|
|
|
|
configureRadarr([{ syncEnabled: true }]);
|
|
// Radarr returns empty meaning movie was deleted
|
|
getMoviesImpl = async () => [];
|
|
|
|
await radarrScanner.run();
|
|
|
|
const updated = await mediaRepository.findOneOrFail({
|
|
where: { tmdbId: 999 },
|
|
});
|
|
assert.strictEqual(updated.status, MediaStatus.UNKNOWN);
|
|
});
|
|
|
|
it('does not reset AVAILABLE movie when missing from Radarr', async () => {
|
|
const mediaRepository = getRepository(Media);
|
|
|
|
const media = new Media();
|
|
media.tmdbId = 888;
|
|
media.mediaType = MediaType.MOVIE;
|
|
media.status = MediaStatus.AVAILABLE;
|
|
await mediaRepository.save(media);
|
|
|
|
configureRadarr([{ syncEnabled: true }]);
|
|
getMoviesImpl = async () => [];
|
|
|
|
await radarrScanner.run();
|
|
|
|
const updated = await mediaRepository.findOneOrFail({
|
|
where: { tmdbId: 888 },
|
|
});
|
|
assert.strictEqual(updated.status, MediaStatus.AVAILABLE);
|
|
});
|
|
|
|
it('does not reset PROCESSING movie that still exists in Radarr', async () => {
|
|
const mediaRepository = getRepository(Media);
|
|
|
|
const media = new Media();
|
|
media.tmdbId = 700;
|
|
media.mediaType = MediaType.MOVIE;
|
|
media.status = MediaStatus.PROCESSING;
|
|
await mediaRepository.save(media);
|
|
|
|
configureRadarr([{ syncEnabled: true }]);
|
|
getMoviesImpl = async () => [
|
|
fakeRadarrMovie({ tmdbId: 700, monitored: true, hasFile: false }),
|
|
];
|
|
|
|
await radarrScanner.run();
|
|
|
|
const updated = await mediaRepository.findOneOrFail({
|
|
where: { tmdbId: 700 },
|
|
});
|
|
assert.strictEqual(updated.status, MediaStatus.PROCESSING);
|
|
});
|
|
|
|
it('does not reset TV media that is missing from Radarr', async () => {
|
|
const mediaRepository = getRepository(Media);
|
|
|
|
// TV show stuck in processing so Radarr scanner should not touch it
|
|
const media = new Media();
|
|
media.tmdbId = 800;
|
|
media.mediaType = MediaType.TV;
|
|
media.status = MediaStatus.PROCESSING;
|
|
await mediaRepository.save(media);
|
|
|
|
configureRadarr([{ syncEnabled: true }]);
|
|
getMoviesImpl = async () => [];
|
|
|
|
await radarrScanner.run();
|
|
|
|
const updated = await mediaRepository.findOneOrFail({
|
|
where: { tmdbId: 800 },
|
|
});
|
|
assert.strictEqual(updated.status, MediaStatus.PROCESSING);
|
|
});
|
|
|
|
it('only resets orphaned movies not found across all servers', async () => {
|
|
const mediaRepository = getRepository(Media);
|
|
|
|
const orphan = new Media();
|
|
orphan.tmdbId = 901;
|
|
orphan.mediaType = MediaType.MOVIE;
|
|
orphan.status = MediaStatus.PROCESSING;
|
|
await mediaRepository.save(orphan);
|
|
|
|
const existing = new Media();
|
|
existing.tmdbId = 902;
|
|
existing.mediaType = MediaType.MOVIE;
|
|
existing.status = MediaStatus.PROCESSING;
|
|
await mediaRepository.save(existing);
|
|
|
|
// Two servers but movie exists on server 1 only
|
|
configureRadarr([
|
|
{ syncEnabled: true, id: 0, hostname: 'server-a' },
|
|
{ syncEnabled: true, id: 1, hostname: 'server-b' },
|
|
]);
|
|
|
|
let callCount = 0;
|
|
getMoviesImpl = async () => {
|
|
callCount++;
|
|
if (callCount === 1) {
|
|
return [fakeRadarrMovie({ tmdbId: 902, id: 10 })];
|
|
}
|
|
return [];
|
|
};
|
|
|
|
await radarrScanner.run();
|
|
|
|
const updatedOrphan = await mediaRepository.findOneOrFail({
|
|
where: { tmdbId: 901 },
|
|
});
|
|
assert.strictEqual(updatedOrphan.status, MediaStatus.UNKNOWN);
|
|
|
|
const updatedExisting = await mediaRepository.findOneOrFail({
|
|
where: { tmdbId: 902 },
|
|
});
|
|
assert.strictEqual(updatedExisting.status, MediaStatus.AVAILABLE);
|
|
});
|
|
});
|
|
|
|
describe('4k orphaned movie cleanup', () => {
|
|
it('resets 4k PROCESSING to UNKNOWN when movie is not in any Radarr server', async () => {
|
|
const mediaRepository = getRepository(Media);
|
|
|
|
const media = new Media();
|
|
media.tmdbId = 960;
|
|
media.mediaType = MediaType.MOVIE;
|
|
media.status = MediaStatus.UNKNOWN;
|
|
media.status4k = MediaStatus.PROCESSING;
|
|
await mediaRepository.save(media);
|
|
|
|
configureRadarr([{ syncEnabled: true, is4k: true }]);
|
|
getMoviesImpl = async () => [];
|
|
|
|
await radarrScanner.run();
|
|
|
|
const updated = await mediaRepository.findOneOrFail({
|
|
where: { tmdbId: 960 },
|
|
});
|
|
assert.strictEqual(updated.status4k, MediaStatus.UNKNOWN);
|
|
});
|
|
|
|
it('does not reset 4k AVAILABLE when movie is missing from Radarr', async () => {
|
|
const mediaRepository = getRepository(Media);
|
|
|
|
const media = new Media();
|
|
media.tmdbId = 961;
|
|
media.mediaType = MediaType.MOVIE;
|
|
media.status = MediaStatus.UNKNOWN;
|
|
media.status4k = MediaStatus.AVAILABLE;
|
|
await mediaRepository.save(media);
|
|
|
|
configureRadarr([{ syncEnabled: true, is4k: true }]);
|
|
getMoviesImpl = async () => [];
|
|
|
|
await radarrScanner.run();
|
|
|
|
const updated = await mediaRepository.findOneOrFail({
|
|
where: { tmdbId: 961 },
|
|
});
|
|
assert.strictEqual(updated.status4k, MediaStatus.AVAILABLE);
|
|
});
|
|
});
|
|
});
|