From 40dcb183aa078127b044144cd1ffe3e2ab588036 Mon Sep 17 00:00:00 2001 From: Ben Scobie Date: Thu, 28 Nov 2024 23:06:24 +0000 Subject: [PATCH 1/5] feat: Add "Original Language" rule to Sonarr & Radarr --- .../interfaces/radarr.interface.ts | 6 ++++ .../interfaces/sonarr.interface.ts | 6 ++++ .../rules/constants/rules.constants.ts | 14 ++++++++++ .../rules/getter/radarr-getter.service.ts | 28 +++++++++++-------- .../rules/getter/sonarr-getter.service.ts | 5 ++++ 5 files changed, 47 insertions(+), 12 deletions(-) diff --git a/server/src/modules/api/servarr-api/interfaces/radarr.interface.ts b/server/src/modules/api/servarr-api/interfaces/radarr.interface.ts index 46976eaf..6aa1af64 100644 --- a/server/src/modules/api/servarr-api/interfaces/radarr.interface.ts +++ b/server/src/modules/api/servarr-api/interfaces/radarr.interface.ts @@ -14,6 +14,7 @@ export interface RadarrMovieOptions { export interface RadarrMovie { id: number; title: string; + originalLanguage: RadarrLanguage; isAvailable: boolean; monitored: boolean; tmdbId: number; @@ -34,6 +35,11 @@ export interface RadarrMovie { tags: number[]; } +export interface RadarrLanguage { + id: number; + name: string | null; +} + export interface RadarrInfo { appName: string; version: string; diff --git a/server/src/modules/api/servarr-api/interfaces/sonarr.interface.ts b/server/src/modules/api/servarr-api/interfaces/sonarr.interface.ts index bae6851c..f47b212b 100644 --- a/server/src/modules/api/servarr-api/interfaces/sonarr.interface.ts +++ b/server/src/modules/api/servarr-api/interfaces/sonarr.interface.ts @@ -83,6 +83,7 @@ export interface SonarrEpisode { export interface SonarrSeries { title: string; sortTitle: string; + originalLanguage: SonarrLanguage; seasonCount: number; status: string; overview: string; @@ -131,6 +132,11 @@ export interface SonarrSeries { ended?: boolean; } +export interface SonarrLanguage { + id: number; + name: string | null; +} + export interface AddSeriesOptions { tvdbid: number; title: string; diff --git a/server/src/modules/rules/constants/rules.constants.ts b/server/src/modules/rules/constants/rules.constants.ts index d37ac18d..37e05209 100644 --- a/server/src/modules/rules/constants/rules.constants.ts +++ b/server/src/modules/rules/constants/rules.constants.ts @@ -430,6 +430,13 @@ export class RuleConstants { mediaType: MediaType.MOVIE, type: RuleType.TEXT, } as Property, + { + id: 13, + name: 'originalLanguage', + humanName: 'Original language', + mediaType: MediaType.MOVIE, + type: RuleType.TEXT, + } as Property, ], }, { @@ -555,6 +562,13 @@ export class RuleConstants { mediaType: MediaType.SHOW, type: RuleType.TEXT, } as Property, + { + id: 15, + name: 'originalLanguage', + humanName: 'Original language', + mediaType: MediaType.SHOW, + type: RuleType.TEXT, + } as Property, ], }, { diff --git a/server/src/modules/rules/getter/radarr-getter.service.ts b/server/src/modules/rules/getter/radarr-getter.service.ts index f82dc878..dc8925f5 100644 --- a/server/src/modules/rules/getter/radarr-getter.service.ts +++ b/server/src/modules/rules/getter/radarr-getter.service.ts @@ -56,27 +56,27 @@ export class RadarrGetterService { return movieResponse.added ? new Date(movieResponse.added) : null; } case 'fileDate': { - return movieResponse?.movieFile?.dateAdded + return movieResponse.movieFile?.dateAdded ? new Date(movieResponse.movieFile.dateAdded) : null; } case 'filePath': { - return movieResponse?.movieFile?.path + return movieResponse.movieFile?.path ? movieResponse.movieFile.path : null; } case 'fileQuality': { - return movieResponse?.movieFile?.quality?.quality?.resolution + return movieResponse.movieFile?.quality?.quality?.resolution ? movieResponse.movieFile.quality.quality.resolution : null; } case 'fileAudioChannels': { - return movieResponse?.movieFile + return movieResponse.movieFile ? movieResponse.movieFile.mediaInfo?.audioChannels : null; } case 'runTime': { - if (movieResponse?.movieFile?.mediaInfo?.runTime) { + if (movieResponse.movieFile?.mediaInfo?.runTime) { const hms = movieResponse.movieFile.mediaInfo.runTime; const splitted = hms.split(':'); return +splitted[0] * 60 + +splitted[1]; @@ -84,35 +84,34 @@ export class RadarrGetterService { return null; } case 'monitored': { - return movieResponse?.monitored + return movieResponse.monitored ? movieResponse.monitored ? 1 : 0 : null; } case 'tags': { - const movieTags = movieResponse?.tags; + const movieTags = movieResponse.tags; return (await radarrApiClient.getTags()) ?.filter((el) => movieTags.includes(el.id)) .map((el) => el.label); } case 'profile': { - const movieProfile = movieResponse?.qualityProfileId; + const movieProfile = movieResponse.qualityProfileId; return (await radarrApiClient.getProfiles())?.find( (el) => el.id === movieProfile, ).name; } case 'fileSize': { - return movieResponse?.sizeOnDisk + return movieResponse.sizeOnDisk ? Math.round(movieResponse.sizeOnDisk / 1048576) : movieResponse.movieFile?.size ? Math.round(movieResponse.movieFile.size / 1048576) : null; } case 'releaseDate': { - return movieResponse?.physicalRelease && - movieResponse?.digitalRelease + return movieResponse.physicalRelease && movieResponse.digitalRelease ? (await new Date(movieResponse.physicalRelease)) > new Date(movieResponse.digitalRelease) ? new Date(movieResponse.digitalRelease) @@ -124,10 +123,15 @@ export class RadarrGetterService { : null; } case 'inCinemas': { - return movieResponse?.inCinemas + return movieResponse.inCinemas ? new Date(movieResponse.inCinemas) : null; } + case 'originalLanguage': { + return movieResponse.originalLanguage?.name + ? movieResponse.originalLanguage.name + : null; + } } } else { this.logger.debug( diff --git a/server/src/modules/rules/getter/sonarr-getter.service.ts b/server/src/modules/rules/getter/sonarr-getter.service.ts index 6bf9af86..5c0cbf36 100644 --- a/server/src/modules/rules/getter/sonarr-getter.service.ts +++ b/server/src/modules/rules/getter/sonarr-getter.service.ts @@ -263,6 +263,11 @@ export class SonarrGetterService { : false; } } + case 'originalLanguage': { + return showResponse.originalLanguage?.name + ? showResponse.originalLanguage.name + : null; + } } } else return null; } else { From ed33a5e9787e3085518578d827024a2d655d724f Mon Sep 17 00:00:00 2001 From: Ben Scobie Date: Thu, 28 Nov 2024 23:10:23 +0000 Subject: [PATCH 2/5] fix: Cache reset check failing in some situations The array was being accessed by index instead of the ID. Radarr props index & id do not align due to skipping id 1. --- server/src/modules/rules/rules.service.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/src/modules/rules/rules.service.ts b/server/src/modules/rules/rules.service.ts index 25ac6a63..df4c4790 100644 --- a/server/src/modules/rules/rules.service.ts +++ b/server/src/modules/rules/rules.service.ts @@ -1014,7 +1014,9 @@ export class RulesService { ); //test first value - const first = firstValApplication.props[parsedRule.firstVal[1]]; + const first = firstValApplication.props.find( + (x) => x.id == parsedRule.firstVal[1], + ); result = first.cacheReset ? true : result; @@ -1023,7 +1025,9 @@ export class RulesService { : undefined; // test second value - const second = secondValApplication?.props[parsedRule.lastVal[1]]; + const second = secondValApplication?.props.find( + (x) => x.id == parsedRule.lastVal[1], + ); result = second?.cacheReset ? true : result; } From d50b19743df5cfb757c84285cdd2d1d64177b1e0 Mon Sep 17 00:00:00 2001 From: Ben Scobie Date: Thu, 28 Nov 2024 23:11:47 +0000 Subject: [PATCH 3/5] fix: Radarr & Sonarr server selection not persisting on initial rule creation --- server/src/modules/rules/rules.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/modules/rules/rules.service.ts b/server/src/modules/rules/rules.service.ts index df4c4790..34354986 100644 --- a/server/src/modules/rules/rules.service.ts +++ b/server/src/modules/rules/rules.service.ts @@ -238,6 +238,8 @@ export class RulesService { forceOverseerr: params.forceOverseerr ? params.forceOverseerr : false, tautulliWatchedPercentOverride: params.tautulliWatchedPercentOverride ?? null, + radarrSettingsId: params.radarrSettingsId ?? null, + sonarrSettingsId: params.sonarrSettingsId ?? null, visibleOnRecommended: params.collection?.visibleOnRecommended, visibleOnHome: params.collection?.visibleOnHome, deleteAfterDays: +params.collection?.deleteAfterDays, From 7c3ca0f57f7f26e707b901aa41a9ff0a391dbc4c Mon Sep 17 00:00:00 2001 From: Ben Scobie Date: Thu, 5 Dec 2024 00:39:55 +0000 Subject: [PATCH 4/5] fix: Incorrect initial state for new rules - also remove the default server option for *arr settings as this is no longer used. --- ...733357722729-Remove-default-arr-servers.ts | 58 ++++++++ .../modules/api/plex-api/plex-api.service.ts | 6 +- .../settings/dto's/radarr-setting.dto.ts | 2 - .../settings/dto's/sonarr-setting.dto.ts | 2 - .../entities/radarr_settings.entities.ts | 3 - .../entities/sonarr_settings.entities.ts | 3 - .../src/modules/settings/settings.service.ts | 124 +++++------------- .../RuleGroup/AddModal/ArrAction/index.tsx | 42 +++--- .../Rules/RuleGroup/AddModal/index.tsx | 87 +++++++----- .../Settings/Radarr/SettingsModal/index.tsx | 21 --- ui/src/components/Settings/Radarr/index.tsx | 21 +-- .../Settings/Sonarr/SettingsModal/index.tsx | 21 --- ui/src/components/Settings/Sonarr/index.tsx | 21 +-- 13 files changed, 173 insertions(+), 238 deletions(-) create mode 100644 server/src/database/migrations/1733357722729-Remove-default-arr-servers.ts diff --git a/server/src/database/migrations/1733357722729-Remove-default-arr-servers.ts b/server/src/database/migrations/1733357722729-Remove-default-arr-servers.ts new file mode 100644 index 00000000..d4241d85 --- /dev/null +++ b/server/src/database/migrations/1733357722729-Remove-default-arr-servers.ts @@ -0,0 +1,58 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemoveDefaultArrServers1733357722729 + implements MigrationInterface +{ + name = 'RemoveDefaultArrServers1733357722729'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE "temporary_sonarr_settings" ( + "id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, + "serverName" varchar NOT NULL, + "url" varchar, + "apiKey" varchar + ) + `); + await queryRunner.query(` + INSERT INTO "temporary_sonarr_settings"("id", "serverName", "url", "apiKey") + SELECT "id", + "serverName", + "url", + "apiKey" + FROM "sonarr_settings" + `); + await queryRunner.query(` + DROP TABLE "sonarr_settings" + `); + await queryRunner.query(` + ALTER TABLE "temporary_sonarr_settings" + RENAME TO "sonarr_settings" + `); + await queryRunner.query(` + CREATE TABLE "temporary_radarr_settings" ( + "id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, + "serverName" varchar NOT NULL, + "url" varchar, + "apiKey" varchar + ) + `); + await queryRunner.query(` + INSERT INTO "temporary_radarr_settings"("id", "serverName", "url", "apiKey") + SELECT "id", + "serverName", + "url", + "apiKey" + FROM "radarr_settings" + `); + await queryRunner.query(` + DROP TABLE "radarr_settings" + `); + await queryRunner.query(` + ALTER TABLE "temporary_radarr_settings" + RENAME TO "radarr_settings" + `); + } + + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/server/src/modules/api/plex-api/plex-api.service.ts b/server/src/modules/api/plex-api/plex-api.service.ts index dad8e489..724200bc 100644 --- a/server/src/modules/api/plex-api/plex-api.service.ts +++ b/server/src/modules/api/plex-api/plex-api.service.ts @@ -443,13 +443,13 @@ export class PlexApiService { } public async deleteMediaFromDisk(plexId: number | string): Promise { - this.logger.log( - `[Plex] Removed media with ID ${plexId} from Plex library.`, - ); try { await this.plexClient.deleteQuery({ uri: `/library/metadata/${plexId}`, }); + this.logger.log( + `[Plex] Removed media with ID ${plexId} from Plex library.`, + ); } catch (e) { this.logger.warn('Something went wrong while removing media from Plex.', { label: 'Plex API', diff --git a/server/src/modules/settings/dto's/radarr-setting.dto.ts b/server/src/modules/settings/dto's/radarr-setting.dto.ts index d6586c28..1c0ebd56 100644 --- a/server/src/modules/settings/dto's/radarr-setting.dto.ts +++ b/server/src/modules/settings/dto's/radarr-setting.dto.ts @@ -8,8 +8,6 @@ export type RadarrSettingDto = { url: string; apiKey: string; - - isDefault: boolean; }; export type RadarrSettingRawDto = Omit; diff --git a/server/src/modules/settings/dto's/sonarr-setting.dto.ts b/server/src/modules/settings/dto's/sonarr-setting.dto.ts index 74ea6510..8ede6d05 100644 --- a/server/src/modules/settings/dto's/sonarr-setting.dto.ts +++ b/server/src/modules/settings/dto's/sonarr-setting.dto.ts @@ -8,8 +8,6 @@ export type SonarrSettingDto = { url: string; apiKey: string; - - isDefault: boolean; }; export type SonarrSettingRawDto = Omit; diff --git a/server/src/modules/settings/entities/radarr_settings.entities.ts b/server/src/modules/settings/entities/radarr_settings.entities.ts index 6fd7382f..ea7e036a 100644 --- a/server/src/modules/settings/entities/radarr_settings.entities.ts +++ b/server/src/modules/settings/entities/radarr_settings.entities.ts @@ -15,9 +15,6 @@ export class RadarrSettings { @Column({ nullable: true }) apiKey: string; - @Column({ default: false }) - isDefault: boolean; - @OneToMany(() => Collection, (collection) => collection.radarrSettings) collections: Collection[]; } diff --git a/server/src/modules/settings/entities/sonarr_settings.entities.ts b/server/src/modules/settings/entities/sonarr_settings.entities.ts index 7c877808..640aa5ea 100644 --- a/server/src/modules/settings/entities/sonarr_settings.entities.ts +++ b/server/src/modules/settings/entities/sonarr_settings.entities.ts @@ -15,9 +15,6 @@ export class SonarrSettings { @Column({ nullable: true }) apiKey: string; - @Column({ default: false }) - isDefault: boolean; - @OneToMany(() => Collection, (collection) => collection.sonarrSettings) collections: Collection[]; } diff --git a/server/src/modules/settings/settings.service.ts b/server/src/modules/settings/settings.service.ts index 35115bc5..ca99bf52 100644 --- a/server/src/modules/settings/settings.service.ts +++ b/server/src/modules/settings/settings.service.ts @@ -1,7 +1,7 @@ import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { randomUUID } from 'crypto'; -import { Not, Repository } from 'typeorm'; +import { Repository } from 'typeorm'; import { isValidCron } from 'cron-validator'; import { BasicResponseDto } from '../api/external-api/dto/basic-response.dto'; import { OverseerrApiService } from '../api/overseerr-api/overseerr-api.service'; @@ -154,25 +154,15 @@ export class SettingsService implements SettingDto { try { settings.url = settings.url.toLowerCase(); - return this.radarrSettingsRepo.manager.transaction(async (manager) => { - if (settings.isDefault) { - await manager - .withRepository(this.radarrSettingsRepo) - .update({ isDefault: true }, { isDefault: false }); - } - - const savedSetting = await manager - .withRepository(this.radarrSettingsRepo) - .save(settings); + const savedSetting = await this.radarrSettingsRepo.save(settings); - this.logger.log('Radarr setting added'); - return { - data: savedSetting, - status: 'OK', - code: 1, - message: 'Success', - }; - }); + this.logger.log('Radarr setting added'); + return { + data: savedSetting, + status: 'OK', + code: 1, + message: 'Success', + }; } catch (e) { this.logger.error('Error while adding Radarr setting: ', e); return { status: 'NOK', code: 0, message: 'Failure' }; @@ -185,33 +175,16 @@ export class SettingsService implements SettingDto { try { settings.url = settings.url.toLowerCase(); - const data = await this.radarrSettingsRepo.manager.transaction( - async (manager) => { - const settingsDb = await manager - .withRepository(this.radarrSettingsRepo) - .findOne({ - where: { id: settings.id }, - }); - - const data = { - ...settingsDb, - ...settings, - }; - - await manager.withRepository(this.radarrSettingsRepo).save(data); - - if (settings.isDefault) { - await manager - .withRepository(this.radarrSettingsRepo) - .update( - { isDefault: true, id: Not(settings.id) }, - { isDefault: false }, - ); - } + const settingsDb = await this.radarrSettingsRepo.findOne({ + where: { id: settings.id }, + }); - return data; - }, - ); + const data = { + ...settingsDb, + ...settings, + }; + + await this.radarrSettingsRepo.save(data); this.servarr.deleteCachedRadarrApiClient(settings.id); this.logger.log('Radarr settings updated'); @@ -284,25 +257,15 @@ export class SettingsService implements SettingDto { try { settings.url = settings.url.toLowerCase(); - return this.sonarrSettingsRepo.manager.transaction(async (manager) => { - if (settings.isDefault) { - await manager - .withRepository(this.sonarrSettingsRepo) - .update({ isDefault: true }, { isDefault: false }); - } - - const savedSetting = await manager - .withRepository(this.sonarrSettingsRepo) - .save(settings); + const savedSetting = await this.sonarrSettingsRepo.save(settings); - this.logger.log('Sonarr setting added'); - return { - data: savedSetting, - status: 'OK', - code: 1, - message: 'Success', - }; - }); + this.logger.log('Sonarr setting added'); + return { + data: savedSetting, + status: 'OK', + code: 1, + message: 'Success', + }; } catch (e) { this.logger.error('Error while adding Sonarr setting: ', e); return { status: 'NOK', code: 0, message: 'Failure' }; @@ -315,33 +278,16 @@ export class SettingsService implements SettingDto { try { settings.url = settings.url.toLowerCase(); - const data = await this.sonarrSettingsRepo.manager.transaction( - async (manager) => { - const settingsDb = await manager - .withRepository(this.sonarrSettingsRepo) - .findOne({ - where: { id: settings.id }, - }); - - const data = { - ...settingsDb, - ...settings, - }; - - await manager.withRepository(this.sonarrSettingsRepo).save(data); - - if (settings.isDefault) { - await manager - .withRepository(this.sonarrSettingsRepo) - .update( - { isDefault: true, id: Not(settings.id) }, - { isDefault: false }, - ); - } + const settingsDb = await this.sonarrSettingsRepo.findOne({ + where: { id: settings.id }, + }); - return data; - }, - ); + const data = { + ...settingsDb, + ...settings, + }; + + await this.sonarrSettingsRepo.save(data); this.servarr.deleteCachedSonarrApiClient(settings.id); diff --git a/ui/src/components/Rules/RuleGroup/AddModal/ArrAction/index.tsx b/ui/src/components/Rules/RuleGroup/AddModal/ArrAction/index.tsx index b8a518df..7613f4d2 100644 --- a/ui/src/components/Rules/RuleGroup/AddModal/ArrAction/index.tsx +++ b/ui/src/components/Rules/RuleGroup/AddModal/ArrAction/index.tsx @@ -8,9 +8,9 @@ type ArrType = 'Radarr' | 'Sonarr' interface ArrActionProps { type: ArrType arrAction?: number - settingId?: number + settingId?: number | null // null for when the user has selected 'None', undefined for when this is a new rule options?: Option[] - onUpdate: (value: number, settingId?: number) => void + onUpdate: (value: number, settingId?: number | null) => void } interface Option { @@ -19,14 +19,15 @@ interface Option { } const ArrAction = (props: ArrActionProps) => { - const [prevType, setPrevType] = useState(props.type) + const selectedSetting = + props.settingId === undefined ? '-1' : (props.settingId?.toString() ?? '') const [settings, setSettings] = useState<(IRadarrSetting | ISonarrSetting)[]>( [], ) const [loading, setLoading] = useState(true) const action = props.arrAction ? props.arrAction : 0 - const handleSelectedSettingIdChange = (id?: number) => { + const handleSelectedSettingIdChange = (id?: number | null) => { props.onUpdate(action, id) } @@ -40,19 +41,15 @@ const ArrAction = (props: ArrActionProps) => { const settingsResponse = await GetApiHandler( `/settings/${type.toLowerCase()}`, ) - setPrevType(type) setSettings(settingsResponse) setLoading(false) - if (!props.settingId || type != prevType) { - if (settingsResponse.length > 0) { - const defaultServer = settingsResponse.find((s) => s.isDefault) - handleSelectedSettingIdChange( - defaultServer ? defaultServer.id : settingsResponse[0]?.id, - ) - } else { - handleSelectedSettingIdChange(undefined) - } + // The selected server does not exist anymore (old client data potentially) so deselect + if ( + props.settingId && + settingsResponse.find((x) => x.id === props.settingId) == null + ) { + handleSelectedSettingIdChange(undefined) } } @@ -81,18 +78,24 @@ const ArrAction = (props: ArrActionProps) => {
setIsDefault(e.target.checked)} - /> -
-
-
-