From b6e2e6ce615cb94cea8d2335140fe245a0ca2d8a Mon Sep 17 00:00:00 2001 From: Gauthier Date: Sun, 29 Dec 2024 22:03:49 +0100 Subject: [PATCH] feat: add a setting for special episodes (#1193) * feat: add a setting for special episodes This PR adds a separate setting for special episodes and disables them by default, to avoid unwanted library status updates. * refactor(settings): re-order setting for allow specials request --------- Co-authored-by: fallenbagel <98979876+Fallenbagel@users.noreply.github.com> --- cypress/config/settings.cypress.json | 1 + overseerr-api.yml | 3 + server/entity/MediaRequest.ts | 7 ++- server/interfaces/api/settingsInterfaces.ts | 1 + server/lib/scanners/plex/index.ts | 7 ++- server/lib/scanners/sonarr/index.ts | 7 ++- server/lib/settings/index.ts | 4 ++ src/components/RequestCard/index.tsx | 8 ++- .../RequestList/RequestItem/index.tsx | 8 ++- .../RequestModal/TvRequestModal.tsx | 17 +++-- .../Settings/SettingsMain/index.tsx | 62 +++++++++++++------ src/components/TvDetails/index.tsx | 9 ++- src/context/SettingsContext.tsx | 1 + src/i18n/locale/en.json | 1 + src/pages/_app.tsx | 1 + 15 files changed, 108 insertions(+), 29 deletions(-) diff --git a/cypress/config/settings.cypress.json b/cypress/config/settings.cypress.json index f45bcbc0e..e3d31cc11 100644 --- a/cypress/config/settings.cypress.json +++ b/cypress/config/settings.cypress.json @@ -22,6 +22,7 @@ "trustProxy": false, "mediaServerType": 1, "partialRequestsEnabled": true, + "enableSpecialEpisodes": false, "locale": "en" }, "plex": { diff --git a/overseerr-api.yml b/overseerr-api.yml index 6a387a6b6..06a7523d0 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -188,6 +188,9 @@ components: defaultPermissions: type: number example: 32 + enableSpecialEpisodes: + type: boolean + example: false PlexLibrary: type: object properties: diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts index 61b82c0e4..6cc808c30 100644 --- a/server/entity/MediaRequest.ts +++ b/server/entity/MediaRequest.ts @@ -58,6 +58,7 @@ export class MediaRequest { const mediaRepository = getRepository(Media); const requestRepository = getRepository(MediaRequest); const userRepository = getRepository(User); + const settings = getSettings(); let requestUser = user; @@ -258,7 +259,11 @@ export class MediaRequest { >; const requestedSeasons = requestBody.seasons === 'all' - ? tmdbMediaShow.seasons.map((season) => season.season_number) + ? settings.main.enableSpecialEpisodes + ? tmdbMediaShow.seasons.map((season) => season.season_number) + : tmdbMediaShow.seasons + .map((season) => season.season_number) + .filter((sn) => sn > 0) : (requestBody.seasons as number[]); let existingSeasons: number[] = []; diff --git a/server/interfaces/api/settingsInterfaces.ts b/server/interfaces/api/settingsInterfaces.ts index 29a81d5ed..017eef856 100644 --- a/server/interfaces/api/settingsInterfaces.ts +++ b/server/interfaces/api/settingsInterfaces.ts @@ -37,6 +37,7 @@ export interface PublicSettingsResponse { originalLanguage: string; mediaServerType: number; partialRequestsEnabled: boolean; + enableSpecialEpisodes: boolean; cacheImages: boolean; vapidPublic: string; enablePushRegistration: boolean; diff --git a/server/lib/scanners/plex/index.ts b/server/lib/scanners/plex/index.ts index e4af7a1f7..9dee904aa 100644 --- a/server/lib/scanners/plex/index.ts +++ b/server/lib/scanners/plex/index.ts @@ -277,8 +277,13 @@ class PlexScanner const seasons = tvShow.seasons; const processableSeasons: ProcessableSeason[] = []; + const settings = getSettings(); - for (const season of seasons) { + const filteredSeasons = settings.main.enableSpecialEpisodes + ? seasons + : seasons.filter((sn) => sn.season_number !== 0); + + for (const season of filteredSeasons) { const matchedPlexSeason = metadata.Children?.Metadata.find( (md) => Number(md.index) === season.season_number ); diff --git a/server/lib/scanners/sonarr/index.ts b/server/lib/scanners/sonarr/index.ts index 5d28e0144..88f6a324c 100644 --- a/server/lib/scanners/sonarr/index.ts +++ b/server/lib/scanners/sonarr/index.ts @@ -102,9 +102,12 @@ class SonarrScanner } const tmdbId = tvShow.id; + const settings = getSettings(); - const filteredSeasons = sonarrSeries.seasons.filter((sn) => - tvShow.seasons.find((s) => s.season_number === sn.seasonNumber) + const filteredSeasons = sonarrSeries.seasons.filter( + (sn) => + tvShow.seasons.find((s) => s.season_number === sn.seasonNumber) && + (!settings.main.partialRequestsEnabled ? sn.seasonNumber !== 0 : true) ); for (const season of filteredSeasons) { diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index 4613486f9..cd8ebb974 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -131,6 +131,7 @@ export interface MainSettings { trustProxy: boolean; mediaServerType: number; partialRequestsEnabled: boolean; + enableSpecialEpisodes: boolean; locale: string; proxy: ProxySettings; } @@ -154,6 +155,7 @@ interface FullPublicSettings extends PublicSettings { jellyfinForgotPasswordUrl?: string; jellyfinServerName?: string; partialRequestsEnabled: boolean; + enableSpecialEpisodes: boolean; cacheImages: boolean; vapidPublic: string; enablePushRegistration: boolean; @@ -343,6 +345,7 @@ class Settings { trustProxy: false, mediaServerType: MediaServerType.NOT_CONFIGURED, partialRequestsEnabled: true, + enableSpecialEpisodes: false, locale: 'en', proxy: { enabled: false, @@ -587,6 +590,7 @@ class Settings { originalLanguage: this.data.main.originalLanguage, mediaServerType: this.main.mediaServerType, partialRequestsEnabled: this.data.main.partialRequestsEnabled, + enableSpecialEpisodes: this.data.main.enableSpecialEpisodes, cacheImages: this.data.main.cacheImages, vapidPublic: this.vapidPublic, enablePushRegistration: this.data.notifications.agents.webpush.enabled, diff --git a/src/components/RequestCard/index.tsx b/src/components/RequestCard/index.tsx index e737c7330..7f08044e4 100644 --- a/src/components/RequestCard/index.tsx +++ b/src/components/RequestCard/index.tsx @@ -5,6 +5,7 @@ import Tooltip from '@app/components/Common/Tooltip'; import RequestModal from '@app/components/RequestModal'; import StatusBadge from '@app/components/StatusBadge'; import useDeepLinks from '@app/hooks/useDeepLinks'; +import useSettings from '@app/hooks/useSettings'; import { Permission, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; @@ -219,6 +220,7 @@ interface RequestCardProps { } const RequestCard = ({ request, onTitleData }: RequestCardProps) => { + const settings = useSettings(); const { ref, inView } = useInView({ triggerOnce: true, }); @@ -411,7 +413,11 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => { {intl.formatMessage(messages.seasons, { seasonCount: - title.seasons.length === request.seasons.length + (settings.currentSettings.enableSpecialEpisodes + ? title.seasons.length + : title.seasons.filter( + (season) => season.seasonNumber !== 0 + ).length) === request.seasons.length ? 0 : request.seasons.length, })} diff --git a/src/components/RequestList/RequestItem/index.tsx b/src/components/RequestList/RequestItem/index.tsx index 7f64039cb..0f8a5a242 100644 --- a/src/components/RequestList/RequestItem/index.tsx +++ b/src/components/RequestList/RequestItem/index.tsx @@ -5,6 +5,7 @@ import ConfirmButton from '@app/components/Common/ConfirmButton'; import RequestModal from '@app/components/RequestModal'; import StatusBadge from '@app/components/StatusBadge'; import useDeepLinks from '@app/hooks/useDeepLinks'; +import useSettings from '@app/hooks/useSettings'; import { Permission, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; @@ -294,6 +295,7 @@ interface RequestItemProps { } const RequestItem = ({ request, revalidateList }: RequestItemProps) => { + const settings = useSettings(); const { ref, inView } = useInView({ triggerOnce: true, }); @@ -481,7 +483,11 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => { {intl.formatMessage(messages.seasons, { seasonCount: - title.seasons.length === request.seasons.length + (settings.currentSettings.enableSpecialEpisodes + ? title.seasons.length + : title.seasons.filter( + (season) => season.seasonNumber !== 0 + ).length) === request.seasons.length ? 0 : request.seasons.length, })} diff --git a/src/components/RequestModal/TvRequestModal.tsx b/src/components/RequestModal/TvRequestModal.tsx index 10c9c7db8..18579d645 100644 --- a/src/components/RequestModal/TvRequestModal.tsx +++ b/src/components/RequestModal/TvRequestModal.tsx @@ -253,9 +253,13 @@ const TvRequestModal = ({ }; const getAllSeasons = (): number[] => { - return (data?.seasons ?? []) - .filter((season) => season.episodeCount !== 0) - .map((season) => season.seasonNumber); + let allSeasons = (data?.seasons ?? []).filter( + (season) => season.episodeCount !== 0 + ); + if (!settings.currentSettings.partialRequestsEnabled) { + allSeasons = allSeasons.filter((season) => season.seasonNumber !== 0); + } + return allSeasons.map((season) => season.seasonNumber); }; const getAllRequestedSeasons = (): number[] => { @@ -577,7 +581,12 @@ const TvRequestModal = ({ {data?.seasons - .filter((season) => season.episodeCount !== 0) + .filter( + (season) => + (!settings.currentSettings.enableSpecialEpisodes + ? season.seasonNumber !== 0 + : true) && season.episodeCount !== 0 + ) .map((season) => { const seasonRequest = getSeasonRequest( season.seasonNumber diff --git a/src/components/Settings/SettingsMain/index.tsx b/src/components/Settings/SettingsMain/index.tsx index e2c50cc13..d5f116c1a 100644 --- a/src/components/Settings/SettingsMain/index.tsx +++ b/src/components/Settings/SettingsMain/index.tsx @@ -56,6 +56,7 @@ const messages = defineMessages('components.Settings.SettingsMain', { validationApplicationUrl: 'You must provide a valid URL', validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash', partialRequestsEnabled: 'Allow Partial Series Requests', + enableSpecialEpisodes: 'Allow Special Episodes Requests', locale: 'Display Language', proxyEnabled: 'HTTP(S) Proxy', proxyHostname: 'Proxy Hostname', @@ -158,6 +159,7 @@ const SettingsMain = () => { originalLanguage: data?.originalLanguage, streamingRegion: data?.streamingRegion, partialRequestsEnabled: data?.partialRequestsEnabled, + enableSpecialEpisodes: data?.enableSpecialEpisodes, trustProxy: data?.trustProxy, cacheImages: data?.cacheImages, proxyEnabled: data?.proxy?.enabled, @@ -188,6 +190,7 @@ const SettingsMain = () => { streamingRegion: values.streamingRegion, originalLanguage: values.originalLanguage, partialRequestsEnabled: values.partialRequestsEnabled, + enableSpecialEpisodes: values.enableSpecialEpisodes, trustProxy: values.trustProxy, cacheImages: values.cacheImages, proxy: { @@ -498,6 +501,47 @@ const SettingsMain = () => { /> +
+ +
+ { + setFieldValue( + 'enableSpecialEpisodes', + !values.enableSpecialEpisodes + ); + }} + /> +
+
+
+
+ + + +
+
)} -
-
- - - -
-
); }} diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx index 4d50f1b87..770285952 100644 --- a/src/components/TvDetails/index.tsx +++ b/src/components/TvDetails/index.tsx @@ -301,7 +301,9 @@ const TvDetails = ({ tv }: TvDetailsProps) => { }; const showHasSpecials = data.seasons.some( - (season) => season.seasonNumber === 0 + (season) => + season.seasonNumber === 0 && + settings.currentSettings.partialRequestsEnabled ); const isComplete = @@ -799,6 +801,11 @@ const TvDetails = ({ tv }: TvDetailsProps) => { {data.seasons .slice() .reverse() + .filter( + (season) => + settings.currentSettings.enableSpecialEpisodes || + season.seasonNumber !== 0 + ) .map((season) => { const show4k = settings.currentSettings.series4kEnabled && diff --git a/src/context/SettingsContext.tsx b/src/context/SettingsContext.tsx index 5579940a2..6a286d8a1 100644 --- a/src/context/SettingsContext.tsx +++ b/src/context/SettingsContext.tsx @@ -21,6 +21,7 @@ const defaultSettings = { originalLanguage: '', mediaServerType: MediaServerType.NOT_CONFIGURED, partialRequestsEnabled: true, + enableSpecialEpisodes: false, cacheImages: false, vapidPublic: '', enablePushRegistration: false, diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 3fce7abd7..ded40f9f8 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -919,6 +919,7 @@ "components.Settings.SettingsMain.csrfProtectionTip": "Set external API access to read-only (requires HTTPS)", "components.Settings.SettingsMain.discoverRegion": "Discover Region", "components.Settings.SettingsMain.discoverRegionTip": "Filter content by regional availability", + "components.Settings.SettingsMain.enableSpecialEpisodes": "Allow Special Episodes Requests", "components.Settings.SettingsMain.general": "General", "components.Settings.SettingsMain.generalsettings": "General Settings", "components.Settings.SettingsMain.generalsettingsDescription": "Configure global and default settings for Jellyseerr.", diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index d0fbbfa9d..facb3a44e 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -199,6 +199,7 @@ CoreApp.getInitialProps = async (initialProps) => { originalLanguage: '', mediaServerType: MediaServerType.NOT_CONFIGURED, partialRequestsEnabled: true, + enableSpecialEpisodes: false, cacheImages: false, vapidPublic: '', enablePushRegistration: false,