diff --git a/cypress/config/settings.cypress.json b/cypress/config/settings.cypress.json index 7a4bbef5d6..cd045a468d 100644 --- a/cypress/config/settings.cypress.json +++ b/cypress/config/settings.cypress.json @@ -20,6 +20,7 @@ "originalLanguage": "", "trustProxy": false, "partialRequestsEnabled": true, + "hideSpecials": false, "locale": "en" }, "plex": { diff --git a/overseerr-api.yml b/overseerr-api.yml index c48b6575f7..13aa55a9e6 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -126,6 +126,9 @@ components: partialRequestsEnabled: type: boolean example: false + hideSpecials: + type: boolean + example: false localLogin: type: boolean example: true diff --git a/server/interfaces/api/settingsInterfaces.ts b/server/interfaces/api/settingsInterfaces.ts index 0cd2f171ae..84b39de923 100644 --- a/server/interfaces/api/settingsInterfaces.ts +++ b/server/interfaces/api/settingsInterfaces.ts @@ -32,6 +32,7 @@ export interface PublicSettingsResponse { originalLanguage: string; partialRequestsEnabled: boolean; cacheImages: boolean; + hideSpecials: boolean; vapidPublic: string; enablePushRegistration: boolean; locale: string; diff --git a/server/lib/settings.ts b/server/lib/settings.ts index 63daf17f36..afa1f3039b 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -104,6 +104,7 @@ export interface MainSettings { trustProxy: boolean; partialRequestsEnabled: boolean; locale: string; + hideSpecials: boolean; } interface PublicSettings { @@ -126,6 +127,7 @@ interface FullPublicSettings extends PublicSettings { locale: string; emailEnabled: boolean; newPlexLogin: boolean; + hideSpecials: boolean; } export interface NotificationAgentConfig { @@ -301,6 +303,7 @@ class Settings { trustProxy: false, partialRequestsEnabled: true, locale: 'en', + hideSpecials: false, }, plex: { name: '', @@ -507,6 +510,7 @@ class Settings { originalLanguage: this.data.main.originalLanguage, partialRequestsEnabled: this.data.main.partialRequestsEnabled, cacheImages: this.data.main.cacheImages, + hideSpecials: this.data.main.hideSpecials, vapidPublic: this.vapidPublic, enablePushRegistration: this.data.notifications.agents.webpush.enabled, locale: this.data.main.locale, diff --git a/src/components/ManageSlideOver/index.tsx b/src/components/ManageSlideOver/index.tsx index 4487cf8138..ec2457b508 100644 --- a/src/components/ManageSlideOver/index.tsx +++ b/src/components/ManageSlideOver/index.tsx @@ -97,7 +97,9 @@ const ManageSlideOver = ({ await axios.post(`/api/v1/media/${data.mediaInfo?.id}/available`, { is4k, ...(mediaType === 'tv' && { - seasons: data.seasons.filter((season) => season.seasonNumber !== 0), + seasons: data.seasons.filter((season) => + settings.currentSettings.hideSpecials ? season.seasonNumber !== 0 : true + ), }), }); revalidate(); diff --git a/src/components/RequestModal/TvRequestModal.tsx b/src/components/RequestModal/TvRequestModal.tsx index 3b4273e9e7..33f369c7be 100644 --- a/src/components/RequestModal/TvRequestModal.tsx +++ b/src/components/RequestModal/TvRequestModal.tsx @@ -201,7 +201,7 @@ const TvRequestModal = ({ ? selectedSeasons : getAllSeasons().filter( (season) => - !getAllRequestedSeasons().includes(season) && season !== 0 + !getAllRequestedSeasons().includes(season) && (!settings.currentSettings.hideSpecials || season !== 0) ), ...overrideParams, }); @@ -235,7 +235,7 @@ const TvRequestModal = ({ const getAllSeasons = (): number[] => { return (data?.seasons ?? []) - .filter((season) => season.episodeCount !== 0) + .filter((season) => season.episodeCount !== 0 && (!settings.currentSettings.hideSpecials || season.seasonNumber !== 0)) .map((season) => season.seasonNumber); }; @@ -568,11 +568,15 @@ const TvRequestModal = ({ {data?.seasons - .filter((season) => - !settings.currentSettings.partialRequestsEnabled - ? season.episodeCount !== 0 && season.seasonNumber !== 0 - : season.episodeCount !== 0 - ) + .filter((season) => { + if (settings.currentSettings.hideSpecials && season.seasonNumber === 0) { + return false; + } + if (!settings.currentSettings.partialRequestsEnabled) { + return season.episodeCount !== 0 && season.seasonNumber !== 0; + } + return 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 62f26d49a2..52f7ab5807 100644 --- a/src/components/Settings/SettingsMain/index.tsx +++ b/src/components/Settings/SettingsMain/index.tsx @@ -54,6 +54,8 @@ const messages = defineMessages({ validationApplicationUrl: 'You must provide a valid URL', validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash', partialRequestsEnabled: 'Allow Partial Series Requests', + hideSpecials: 'Hide Specials Season', + hideSpecialsTip: 'Hide specials season and prevent requesting specials', locale: 'Display Language', }); @@ -132,6 +134,7 @@ const SettingsMain = () => { region: data?.region, originalLanguage: data?.originalLanguage, partialRequestsEnabled: data?.partialRequestsEnabled, + hideSpecials: data?.hideSpecials, trustProxy: data?.trustProxy, cacheImages: data?.cacheImages, }} @@ -148,6 +151,7 @@ const SettingsMain = () => { region: values.region, originalLanguage: values.originalLanguage, partialRequestsEnabled: values.partialRequestsEnabled, + hideSpecials: values.hideSpecials, trustProxy: values.trustProxy, cacheImages: values.cacheImages, }); @@ -401,6 +405,31 @@ const SettingsMain = () => { onChange={() => { setFieldValue('hideAvailable', !values.hideAvailable); }} + /> + +
+ +
+ { + setFieldValue( + 'hideSpecials', + !values.hideSpecials + ); + }} />
diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx index ad1b834fdb..b1c477f66d 100644 --- a/src/components/TvDetails/index.tsx +++ b/src/components/TvDetails/index.tsx @@ -202,7 +202,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => { // Does NOT include "Specials" const seasonCount = data.seasons.filter( - (season) => season.seasonNumber !== 0 && season.episodeCount !== 0 + (season) => (settings.currentSettings.hideSpecials ? season.seasonNumber !== 0 : true) && season.episodeCount !== 0 ).length; if (seasonCount) { @@ -259,17 +259,13 @@ const TvDetails = ({ tv }: TvDetailsProps) => { return [...requestedSeasons, ...availableSeasons]; }; - const showHasSpecials = data.seasons.some( - (season) => season.seasonNumber === 0 - ); - - const isComplete = - (showHasSpecials ? seasonCount + 1 : seasonCount) <= - getAllRequestedSeasons(false).length; - - const is4kComplete = - (showHasSpecials ? seasonCount + 1 : seasonCount) <= - getAllRequestedSeasons(true).length; + // Calculate total seasons to consider for completion status + const totalSeasons = settings.currentSettings.hideSpecials + ? seasonCount + : (data.seasons.some(s => s.seasonNumber === 0) ? seasonCount + 1 : seasonCount); + + const isComplete = totalSeasons <= getAllRequestedSeasons(false).length; + const is4kComplete = totalSeasons <= getAllRequestedSeasons(true).length; const streamingProviders = data?.watchProviders?.find((provider) => provider.iso_3166_1 === region) @@ -530,6 +526,7 @@ const TvDetails = ({ tv }: TvDetailsProps) => {

{intl.formatMessage(messages.seasonstitle)}

{data.seasons + .filter((season) => !settings.currentSettings.hideSpecials || season.seasonNumber !== 0) .slice() .reverse() .map((season) => { diff --git a/src/context/SettingsContext.tsx b/src/context/SettingsContext.tsx index d50add4db7..6ba9cfa900 100644 --- a/src/context/SettingsContext.tsx +++ b/src/context/SettingsContext.tsx @@ -24,6 +24,7 @@ const defaultSettings = { locale: 'en', emailEnabled: false, newPlexLogin: true, + hideSpecials: false, }; export const SettingsContext = React.createContext({ diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index e9f3b41187..38278a797d 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -804,6 +804,8 @@ "components.Settings.SettingsMain.generalsettings": "General Settings", "components.Settings.SettingsMain.generalsettingsDescription": "Configure global and default settings for Overseerr.", "components.Settings.SettingsMain.hideAvailable": "Hide Available Media", + "components.Settings.SettingsMain.hideSpecials": "Hide Specials Season", + "components.Settings.SettingsMain.hideSpecialsTip": "Hide specials season and prevent requesting specials", "components.Settings.SettingsMain.locale": "Display Language", "components.Settings.SettingsMain.originallanguage": "Discover Language", "components.Settings.SettingsMain.originallanguageTip": "Filter content by original language", diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 091bb03c7d..fbb67b546a 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -242,6 +242,7 @@ CoreApp.getInitialProps = async (initialProps) => { locale: 'en', emailEnabled: false, newPlexLogin: true, + hideSpecials: false, }; if (ctx.res) {