From 2b4effa428137029fd5cf9a8eae8f95ca8f91287 Mon Sep 17 00:00:00 2001 From: Gavin Fuller Date: Thu, 19 Dec 2024 18:26:56 -0600 Subject: [PATCH] feat: added slider to allow end users to determine if new seasons should be monitored in Sonarr feat #1047 --- pnpm-lock.yaml | 5 +- server/api/servarr/sonarr.ts | 50 +++++++++++-------- server/entity/MediaRequest.ts | 11 ++++ server/interfaces/api/requestInterfaces.ts | 1 + server/routes/media.ts | 2 +- src/components/Common/SlideCheckbox/index.tsx | 2 +- .../RequestModal/MovieRequestModal.tsx | 3 ++ .../RequestModal/TvRequestModal.tsx | 26 ++++++++++ 8 files changed, 75 insertions(+), 25 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 016a4b2ce..4c8869efa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8393,6 +8393,7 @@ packages: sudo-prompt@9.2.1: resolution: {integrity: sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} @@ -14765,7 +14766,7 @@ snapshots: debug: 4.3.5(supports-color@8.1.1) enhanced-resolve: 5.17.0 eslint: 8.35.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.35.0))(eslint@8.35.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.35.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.54.0(eslint@8.35.0)(typescript@4.9.5))(eslint@8.35.0) fast-glob: 3.3.2 get-tsconfig: 4.7.5 @@ -14787,7 +14788,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.35.0))(eslint@8.35.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.35.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.35.0): dependencies: debug: 3.2.7(supports-color@5.5.0) optionalDependencies: diff --git a/server/api/servarr/sonarr.ts b/server/api/servarr/sonarr.ts index 8ae054edb..e933939dc 100644 --- a/server/api/servarr/sonarr.ts +++ b/server/api/servarr/sonarr.ts @@ -13,8 +13,11 @@ export interface SonarrSeason { percentOfEpisodes: number; }; } + interface EpisodeResult { seriesId: number; + episodeId: number; + episode: EpisodeResult; episodeFileId: number; seasonNumber: number; episodeNumber: number; @@ -99,6 +102,7 @@ export interface AddSeriesOptions { seriesType: SonarrSeries['seriesType']; monitored?: boolean; searchNow?: boolean; + autoRequestNewSeasons?: boolean; } export interface LanguageProfile { @@ -185,7 +189,11 @@ class SonarrAPI extends ServarrBase<{ if (series.id) { series.monitored = options.monitored ?? series.monitored; series.tags = options.tags ?? series.tags; - series.seasons = this.buildSeasonList(options.seasons, series.seasons); + series.seasons = this.buildSeasonList( + options.seasons, + series.seasons, + options.autoRequestNewSeasons + ); const newSeriesData = await this.put( '/series', @@ -226,9 +234,9 @@ class SonarrAPI extends ServarrBase<{ options.seasons, series.seasons.map((season) => ({ seasonNumber: season.seasonNumber, - // We force all seasons to false if its the first request - monitored: false, - })) + monitored: false, // Initialize all seasons as unmonitored + })), + options.autoRequestNewSeasons ), tags: options.tags, seasonFolder: options.seasonFolder, @@ -236,7 +244,7 @@ class SonarrAPI extends ServarrBase<{ rootFolderPath: options.rootFolderPath, seriesType: options.seriesType, addOptions: { - ignoreEpisodesWithFiles: true, + monitor: options.autoRequestNewSeasons ? 'future' : 'none', searchForMissingEpisodes: options.searchNow, }, } as Partial); @@ -248,7 +256,7 @@ class SonarrAPI extends ServarrBase<{ movie: createdSeriesData, }); } else { - logger.error('Failed to add movie to Sonarr', { + logger.error('Failed to add series to Sonarr', { label: 'Sonarr', options, }); @@ -318,39 +326,39 @@ class SonarrAPI extends ServarrBase<{ private buildSeasonList( seasons: number[], - existingSeasons?: SonarrSeason[] + existingSeasons?: SonarrSeason[], + autoRequestNewSeasons?: boolean ): SonarrSeason[] { if (existingSeasons) { - const newSeasons = existingSeasons.map((season) => { + return existingSeasons.map((season) => { + // Monitor requested seasons if (seasons.includes(season.seasonNumber)) { season.monitored = true; + } else { + // Set future seasons' monitoring based on autoRequestNewSeasons + season.monitored = autoRequestNewSeasons !== false; } return season; }); - - return newSeasons; } - const newSeasons = seasons.map( - (seasonNumber): SonarrSeason => ({ - seasonNumber, - monitored: true, - }) - ); - - return newSeasons; + // If no existing seasons, monitor only the requested seasons + return seasons.map((seasonNumber) => ({ + seasonNumber, + monitored: true, + })); } - public removeSerie = async (serieId: number): Promise => { + public removeSeries = async (serieId: number): Promise => { try { const { id, title } = await this.getSeriesByTvdbId(serieId); await this.delete(`/series/${id}`, { deleteFiles: 'true', addImportExclusion: 'false', }); - logger.info(`[Radarr] Removed serie ${title}`); + logger.info(`[Sonarr] Removed series ${title}`); } catch (e) { - throw new Error(`[Radarr] Failed to remove serie: ${e.message}`); + throw new Error(`[Sonarr] Failed to remove series: ${e.message}`); } }; } diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts index ea2efbec4..332c00996 100644 --- a/server/entity/MediaRequest.ts +++ b/server/entity/MediaRequest.ts @@ -205,6 +205,11 @@ export class MediaRequest { } } + const autoRequestNewSeasonsValue = + typeof requestBody.autoRequestNewSeasons === 'boolean' + ? requestBody.autoRequestNewSeasons + : true; + if (requestBody.mediaType === MediaType.MOVIE) { await mediaRepository.save(media); @@ -247,6 +252,7 @@ export class MediaRequest { rootFolder: requestBody.rootFolder, tags: requestBody.tags, isAutoRequest: options.isAutoRequest ?? false, + autoRequestNewSeasons: autoRequestNewSeasonsValue, }); await requestRepository.save(request); @@ -369,6 +375,7 @@ export class MediaRequest { }) ), isAutoRequest: options.isAutoRequest ?? false, + autoRequestNewSeasons: autoRequestNewSeasonsValue, }); await requestRepository.save(request); @@ -470,6 +477,9 @@ export class MediaRequest { @Column({ default: false }) public isAutoRequest: boolean; + @Column({ nullable: true, default: true }) + public autoRequestNewSeasons?: boolean; + constructor(init?: Partial) { Object.assign(this, init); } @@ -1112,6 +1122,7 @@ export class MediaRequest { tags, monitored: true, searchNow: !sonarrSettings.preventSearch, + autoRequestNewSeasons: this.autoRequestNewSeasons, }; // Run this asynchronously so we don't wait for it on the UI side diff --git a/server/interfaces/api/requestInterfaces.ts b/server/interfaces/api/requestInterfaces.ts index 88b1201de..aeae4ead5 100644 --- a/server/interfaces/api/requestInterfaces.ts +++ b/server/interfaces/api/requestInterfaces.ts @@ -19,4 +19,5 @@ export type MediaRequestBody = { languageProfileId?: number; userId?: number; tags?: number[]; + autoRequestNewSeasons?: boolean; }; diff --git a/server/routes/media.ts b/server/routes/media.ts index 60191e5de..57515a8d7 100644 --- a/server/routes/media.ts +++ b/server/routes/media.ts @@ -251,7 +251,7 @@ mediaRoutes.delete( if (!tvdbId) { throw new Error('TVDB ID not found'); } - await (service as SonarrAPI).removeSerie(tvdbId); + await (service as SonarrAPI).removeSeries(tvdbId); } return res.status(204).send(); diff --git a/src/components/Common/SlideCheckbox/index.tsx b/src/components/Common/SlideCheckbox/index.tsx index 320dd667f..48a8f8311 100644 --- a/src/components/Common/SlideCheckbox/index.tsx +++ b/src/components/Common/SlideCheckbox/index.tsx @@ -8,7 +8,7 @@ const SlideCheckbox = ({ onClick, checked = false }: SlideCheckboxProps) => { { onClick(); }} diff --git a/src/components/RequestModal/MovieRequestModal.tsx b/src/components/RequestModal/MovieRequestModal.tsx index 85af7aef4..b27b73613 100644 --- a/src/components/RequestModal/MovieRequestModal.tsx +++ b/src/components/RequestModal/MovieRequestModal.tsx @@ -34,6 +34,9 @@ const messages = defineMessages('components.RequestModal', { requestApproved: 'Request for {title} approved!', requesterror: 'Something went wrong while submitting the request.', pendingapproval: 'Your request is pending approval.', + searchAutomatically: 'Search Automatically', + searchAutomaticallyDescription: + 'Automatically search for this movie in Radarr after the request is approved.', }); interface RequestModalProps extends React.HTMLAttributes { diff --git a/src/components/RequestModal/TvRequestModal.tsx b/src/components/RequestModal/TvRequestModal.tsx index 10c9c7db8..4ee4e0c6f 100644 --- a/src/components/RequestModal/TvRequestModal.tsx +++ b/src/components/RequestModal/TvRequestModal.tsx @@ -1,6 +1,7 @@ import Alert from '@app/components/Common/Alert'; import Badge from '@app/components/Common/Badge'; import Modal from '@app/components/Common/Modal'; +import SlideCheckbox from '@app/components/Common/SlideCheckbox'; import type { RequestOverrides } from '@app/components/RequestModal/AdvancedRequester'; import AdvancedRequester from '@app/components/RequestModal/AdvancedRequester'; import QuotaDisplay from '@app/components/RequestModal/QuotaDisplay'; @@ -49,6 +50,9 @@ const messages = defineMessages('components.RequestModal', { autoapproval: 'Automatic Approval', requesterror: 'Something went wrong while submitting the request.', pendingapproval: 'Your request is pending approval.', + autoRequestNewSeasons: 'Automatically Request New Seasons', + autoRequestNewSeasonsDescription: + 'New seasons will be requested automatically when they become available', }); interface RequestModalProps extends React.HTMLAttributes { @@ -70,6 +74,7 @@ const TvRequestModal = ({ }: RequestModalProps) => { const settings = useSettings(); const { addToast } = useToasts(); + const [autoRequestNewSeasons, setAutoRequestNewSeasons] = useState(true); const editingSeasons: number[] = (editRequest?.seasons ?? []).map( (season) => season.seasonNumber ); @@ -124,6 +129,7 @@ const TvRequestModal = ({ userId: requestOverrides?.user?.id, tags: requestOverrides?.tags, seasons: selectedSeasons, + autoRequestNewSeasons, }), }); if (!res.ok) throw new Error(); @@ -213,6 +219,7 @@ const TvRequestModal = ({ tvdbId: tvdbId ?? data?.externalIds.tvdbId, mediaType: 'tv', is4k, + autoRequestNewSeasons, seasons: settings.currentSettings.partialRequestsEnabled ? selectedSeasons : getAllSeasons().filter( @@ -710,6 +717,25 @@ const TvRequestModal = ({ + + {/* Add auto-request checkbox after the seasons table */} + {!editRequest && ( +
+ setAutoRequestNewSeasons(!autoRequestNewSeasons)} + /> +
+ + {intl.formatMessage(messages.autoRequestNewSeasons)} + + + {intl.formatMessage(messages.autoRequestNewSeasonsDescription)} + +
+
+ )} + {(hasPermission(Permission.REQUEST_ADVANCED) || hasPermission(Permission.MANAGE_REQUESTS)) && (