From 2b4effa428137029fd5cf9a8eae8f95ca8f91287 Mon Sep 17 00:00:00 2001 From: Gavin Fuller Date: Thu, 19 Dec 2024 18:26:56 -0600 Subject: [PATCH 1/3] 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)) && ( Date: Thu, 19 Dec 2024 18:52:37 -0600 Subject: [PATCH 2/3] fix(en.json): ran command as requested by todo --- src/i18n/locale/en.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 599106e91..da9f201d5 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -533,6 +533,8 @@ "components.RequestModal.SearchByNameModal.notvdbiddescription": "We were unable to automatically match this series. Please select the correct match below.", "components.RequestModal.alreadyrequested": "Already Requested", "components.RequestModal.approve": "Approve Request", + "components.RequestModal.autoRequestNewSeasons": "Automatically Request New Seasons", + "components.RequestModal.autoRequestNewSeasonsDescription": "New seasons will be requested automatically when they become available", "components.RequestModal.autoapproval": "Automatic Approval", "components.RequestModal.cancel": "Cancel Request", "components.RequestModal.edit": "Edit Request", @@ -559,6 +561,8 @@ "components.RequestModal.requestseasons4k": "Request {seasonCount} {seasonCount, plural, one {Season} other {Seasons}} in 4K", "components.RequestModal.requestseries4ktitle": "Request Series in 4K", "components.RequestModal.requestseriestitle": "Request Series", + "components.RequestModal.searchAutomatically": "Search Automatically", + "components.RequestModal.searchAutomaticallyDescription": "Automatically search for this movie in Radarr after the request is approved.", "components.RequestModal.season": "Season", "components.RequestModal.seasonnumber": "Season {number}", "components.RequestModal.selectmovies": "Select Movie(s)", @@ -1100,7 +1104,7 @@ "components.Setup.finishing": "Finishing…", "components.Setup.servertype": "Choose Server Type", "components.Setup.setup": "Setup", - "components.Setup.signin": "Sign In", + "components.Setup.signin": "Sign in to your account", "components.Setup.signinMessage": "Get started by signing in", "components.Setup.signinWithEmby": "Enter your Emby details", "components.Setup.signinWithJellyfin": "Enter your Jellyfin details", From f1aee0e65ed4b105dcd7a53998c9c983958afa78 Mon Sep 17 00:00:00 2001 From: Gavin Fuller Date: Thu, 19 Dec 2024 22:43:34 -0600 Subject: [PATCH 3/3] fix(database migration): ran database migration --- ...-AddAutoRequestNewSeasonsToMediaRequest.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 1734669224886-AddAutoRequestNewSeasonsToMediaRequest.ts diff --git a/1734669224886-AddAutoRequestNewSeasonsToMediaRequest.ts b/1734669224886-AddAutoRequestNewSeasonsToMediaRequest.ts new file mode 100644 index 000000000..cf95c0ea1 --- /dev/null +++ b/1734669224886-AddAutoRequestNewSeasonsToMediaRequest.ts @@ -0,0 +1,33 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddAutoRequestNewSeasonsToMediaRequest1734669224886 + implements MigrationInterface +{ + name = 'AddAutoRequestNewSeasonsToMediaRequest1734669224886'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "temporary_media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "is4k" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, "languageProfileId" integer, "tags" text, "isAutoRequest" boolean NOT NULL DEFAULT (0), "mediaId" integer NOT NULL, "requestedById" integer, "modifiedById" integer, "autoRequestNewSeasons" boolean DEFAULT (1), CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_media_request"("id", "status", "createdAt", "updatedAt", "type", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest", "mediaId", "requestedById", "modifiedById") SELECT "id", "status", "createdAt", "updatedAt", "type", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest", "mediaId", "requestedById", "modifiedById" FROM "media_request"` + ); + await queryRunner.query(`DROP TABLE "media_request"`); + await queryRunner.query( + `ALTER TABLE "temporary_media_request" RENAME TO "media_request"` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "media_request" RENAME TO "temporary_media_request"` + ); + await queryRunner.query( + `CREATE TABLE "media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "is4k" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, "languageProfileId" integer, "tags" text, "isAutoRequest" boolean NOT NULL DEFAULT (0), "mediaId" integer NOT NULL, "requestedById" integer, "modifiedById" integer, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "media_request"("id", "status", "createdAt", "updatedAt", "type", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest", "mediaId", "requestedById", "modifiedById") SELECT "id", "status", "createdAt", "updatedAt", "type", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId", "tags", "isAutoRequest", "mediaId", "requestedById", "modifiedById" FROM "temporary_media_request"` + ); + await queryRunner.query(`DROP TABLE "temporary_media_request"`); + } +}