mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-01 04:08:45 -05:00
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>
This commit is contained in:
@@ -22,6 +22,7 @@
|
|||||||
"trustProxy": false,
|
"trustProxy": false,
|
||||||
"mediaServerType": 1,
|
"mediaServerType": 1,
|
||||||
"partialRequestsEnabled": true,
|
"partialRequestsEnabled": true,
|
||||||
|
"enableSpecialEpisodes": false,
|
||||||
"locale": "en"
|
"locale": "en"
|
||||||
},
|
},
|
||||||
"plex": {
|
"plex": {
|
||||||
|
|||||||
@@ -188,6 +188,9 @@ components:
|
|||||||
defaultPermissions:
|
defaultPermissions:
|
||||||
type: number
|
type: number
|
||||||
example: 32
|
example: 32
|
||||||
|
enableSpecialEpisodes:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
PlexLibrary:
|
PlexLibrary:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ export class MediaRequest {
|
|||||||
const mediaRepository = getRepository(Media);
|
const mediaRepository = getRepository(Media);
|
||||||
const requestRepository = getRepository(MediaRequest);
|
const requestRepository = getRepository(MediaRequest);
|
||||||
const userRepository = getRepository(User);
|
const userRepository = getRepository(User);
|
||||||
|
const settings = getSettings();
|
||||||
|
|
||||||
let requestUser = user;
|
let requestUser = user;
|
||||||
|
|
||||||
@@ -258,7 +259,11 @@ export class MediaRequest {
|
|||||||
>;
|
>;
|
||||||
const requestedSeasons =
|
const requestedSeasons =
|
||||||
requestBody.seasons === 'all'
|
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[]);
|
: (requestBody.seasons as number[]);
|
||||||
let existingSeasons: number[] = [];
|
let existingSeasons: number[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export interface PublicSettingsResponse {
|
|||||||
originalLanguage: string;
|
originalLanguage: string;
|
||||||
mediaServerType: number;
|
mediaServerType: number;
|
||||||
partialRequestsEnabled: boolean;
|
partialRequestsEnabled: boolean;
|
||||||
|
enableSpecialEpisodes: boolean;
|
||||||
cacheImages: boolean;
|
cacheImages: boolean;
|
||||||
vapidPublic: string;
|
vapidPublic: string;
|
||||||
enablePushRegistration: boolean;
|
enablePushRegistration: boolean;
|
||||||
|
|||||||
@@ -277,8 +277,13 @@ class PlexScanner
|
|||||||
|
|
||||||
const seasons = tvShow.seasons;
|
const seasons = tvShow.seasons;
|
||||||
const processableSeasons: ProcessableSeason[] = [];
|
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(
|
const matchedPlexSeason = metadata.Children?.Metadata.find(
|
||||||
(md) => Number(md.index) === season.season_number
|
(md) => Number(md.index) === season.season_number
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -102,9 +102,12 @@ class SonarrScanner
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tmdbId = tvShow.id;
|
const tmdbId = tvShow.id;
|
||||||
|
const settings = getSettings();
|
||||||
|
|
||||||
const filteredSeasons = sonarrSeries.seasons.filter((sn) =>
|
const filteredSeasons = sonarrSeries.seasons.filter(
|
||||||
tvShow.seasons.find((s) => s.season_number === sn.seasonNumber)
|
(sn) =>
|
||||||
|
tvShow.seasons.find((s) => s.season_number === sn.seasonNumber) &&
|
||||||
|
(!settings.main.partialRequestsEnabled ? sn.seasonNumber !== 0 : true)
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const season of filteredSeasons) {
|
for (const season of filteredSeasons) {
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ export interface MainSettings {
|
|||||||
trustProxy: boolean;
|
trustProxy: boolean;
|
||||||
mediaServerType: number;
|
mediaServerType: number;
|
||||||
partialRequestsEnabled: boolean;
|
partialRequestsEnabled: boolean;
|
||||||
|
enableSpecialEpisodes: boolean;
|
||||||
locale: string;
|
locale: string;
|
||||||
proxy: ProxySettings;
|
proxy: ProxySettings;
|
||||||
}
|
}
|
||||||
@@ -154,6 +155,7 @@ interface FullPublicSettings extends PublicSettings {
|
|||||||
jellyfinForgotPasswordUrl?: string;
|
jellyfinForgotPasswordUrl?: string;
|
||||||
jellyfinServerName?: string;
|
jellyfinServerName?: string;
|
||||||
partialRequestsEnabled: boolean;
|
partialRequestsEnabled: boolean;
|
||||||
|
enableSpecialEpisodes: boolean;
|
||||||
cacheImages: boolean;
|
cacheImages: boolean;
|
||||||
vapidPublic: string;
|
vapidPublic: string;
|
||||||
enablePushRegistration: boolean;
|
enablePushRegistration: boolean;
|
||||||
@@ -343,6 +345,7 @@ class Settings {
|
|||||||
trustProxy: false,
|
trustProxy: false,
|
||||||
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
||||||
partialRequestsEnabled: true,
|
partialRequestsEnabled: true,
|
||||||
|
enableSpecialEpisodes: false,
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
proxy: {
|
proxy: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@@ -587,6 +590,7 @@ class Settings {
|
|||||||
originalLanguage: this.data.main.originalLanguage,
|
originalLanguage: this.data.main.originalLanguage,
|
||||||
mediaServerType: this.main.mediaServerType,
|
mediaServerType: this.main.mediaServerType,
|
||||||
partialRequestsEnabled: this.data.main.partialRequestsEnabled,
|
partialRequestsEnabled: this.data.main.partialRequestsEnabled,
|
||||||
|
enableSpecialEpisodes: this.data.main.enableSpecialEpisodes,
|
||||||
cacheImages: this.data.main.cacheImages,
|
cacheImages: this.data.main.cacheImages,
|
||||||
vapidPublic: this.vapidPublic,
|
vapidPublic: this.vapidPublic,
|
||||||
enablePushRegistration: this.data.notifications.agents.webpush.enabled,
|
enablePushRegistration: this.data.notifications.agents.webpush.enabled,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Tooltip from '@app/components/Common/Tooltip';
|
|||||||
import RequestModal from '@app/components/RequestModal';
|
import RequestModal from '@app/components/RequestModal';
|
||||||
import StatusBadge from '@app/components/StatusBadge';
|
import StatusBadge from '@app/components/StatusBadge';
|
||||||
import useDeepLinks from '@app/hooks/useDeepLinks';
|
import useDeepLinks from '@app/hooks/useDeepLinks';
|
||||||
|
import useSettings from '@app/hooks/useSettings';
|
||||||
import { Permission, useUser } from '@app/hooks/useUser';
|
import { Permission, useUser } from '@app/hooks/useUser';
|
||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
@@ -219,6 +220,7 @@ interface RequestCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
||||||
|
const settings = useSettings();
|
||||||
const { ref, inView } = useInView({
|
const { ref, inView } = useInView({
|
||||||
triggerOnce: true,
|
triggerOnce: true,
|
||||||
});
|
});
|
||||||
@@ -411,7 +413,11 @@ const RequestCard = ({ request, onTitleData }: RequestCardProps) => {
|
|||||||
<span className="mr-2 font-bold ">
|
<span className="mr-2 font-bold ">
|
||||||
{intl.formatMessage(messages.seasons, {
|
{intl.formatMessage(messages.seasons, {
|
||||||
seasonCount:
|
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
|
? 0
|
||||||
: request.seasons.length,
|
: request.seasons.length,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import ConfirmButton from '@app/components/Common/ConfirmButton';
|
|||||||
import RequestModal from '@app/components/RequestModal';
|
import RequestModal from '@app/components/RequestModal';
|
||||||
import StatusBadge from '@app/components/StatusBadge';
|
import StatusBadge from '@app/components/StatusBadge';
|
||||||
import useDeepLinks from '@app/hooks/useDeepLinks';
|
import useDeepLinks from '@app/hooks/useDeepLinks';
|
||||||
|
import useSettings from '@app/hooks/useSettings';
|
||||||
import { Permission, useUser } from '@app/hooks/useUser';
|
import { Permission, useUser } from '@app/hooks/useUser';
|
||||||
import globalMessages from '@app/i18n/globalMessages';
|
import globalMessages from '@app/i18n/globalMessages';
|
||||||
import defineMessages from '@app/utils/defineMessages';
|
import defineMessages from '@app/utils/defineMessages';
|
||||||
@@ -294,6 +295,7 @@ interface RequestItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
||||||
|
const settings = useSettings();
|
||||||
const { ref, inView } = useInView({
|
const { ref, inView } = useInView({
|
||||||
triggerOnce: true,
|
triggerOnce: true,
|
||||||
});
|
});
|
||||||
@@ -481,7 +483,11 @@ const RequestItem = ({ request, revalidateList }: RequestItemProps) => {
|
|||||||
<span className="card-field-name">
|
<span className="card-field-name">
|
||||||
{intl.formatMessage(messages.seasons, {
|
{intl.formatMessage(messages.seasons, {
|
||||||
seasonCount:
|
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
|
? 0
|
||||||
: request.seasons.length,
|
: request.seasons.length,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -253,9 +253,13 @@ const TvRequestModal = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getAllSeasons = (): number[] => {
|
const getAllSeasons = (): number[] => {
|
||||||
return (data?.seasons ?? [])
|
let allSeasons = (data?.seasons ?? []).filter(
|
||||||
.filter((season) => season.episodeCount !== 0)
|
(season) => season.episodeCount !== 0
|
||||||
.map((season) => season.seasonNumber);
|
);
|
||||||
|
if (!settings.currentSettings.partialRequestsEnabled) {
|
||||||
|
allSeasons = allSeasons.filter((season) => season.seasonNumber !== 0);
|
||||||
|
}
|
||||||
|
return allSeasons.map((season) => season.seasonNumber);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAllRequestedSeasons = (): number[] => {
|
const getAllRequestedSeasons = (): number[] => {
|
||||||
@@ -577,7 +581,12 @@ const TvRequestModal = ({
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-700">
|
<tbody className="divide-y divide-gray-700">
|
||||||
{data?.seasons
|
{data?.seasons
|
||||||
.filter((season) => season.episodeCount !== 0)
|
.filter(
|
||||||
|
(season) =>
|
||||||
|
(!settings.currentSettings.enableSpecialEpisodes
|
||||||
|
? season.seasonNumber !== 0
|
||||||
|
: true) && season.episodeCount !== 0
|
||||||
|
)
|
||||||
.map((season) => {
|
.map((season) => {
|
||||||
const seasonRequest = getSeasonRequest(
|
const seasonRequest = getSeasonRequest(
|
||||||
season.seasonNumber
|
season.seasonNumber
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ const messages = defineMessages('components.Settings.SettingsMain', {
|
|||||||
validationApplicationUrl: 'You must provide a valid URL',
|
validationApplicationUrl: 'You must provide a valid URL',
|
||||||
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash',
|
||||||
partialRequestsEnabled: 'Allow Partial Series Requests',
|
partialRequestsEnabled: 'Allow Partial Series Requests',
|
||||||
|
enableSpecialEpisodes: 'Allow Special Episodes Requests',
|
||||||
locale: 'Display Language',
|
locale: 'Display Language',
|
||||||
proxyEnabled: 'HTTP(S) Proxy',
|
proxyEnabled: 'HTTP(S) Proxy',
|
||||||
proxyHostname: 'Proxy Hostname',
|
proxyHostname: 'Proxy Hostname',
|
||||||
@@ -158,6 +159,7 @@ const SettingsMain = () => {
|
|||||||
originalLanguage: data?.originalLanguage,
|
originalLanguage: data?.originalLanguage,
|
||||||
streamingRegion: data?.streamingRegion,
|
streamingRegion: data?.streamingRegion,
|
||||||
partialRequestsEnabled: data?.partialRequestsEnabled,
|
partialRequestsEnabled: data?.partialRequestsEnabled,
|
||||||
|
enableSpecialEpisodes: data?.enableSpecialEpisodes,
|
||||||
trustProxy: data?.trustProxy,
|
trustProxy: data?.trustProxy,
|
||||||
cacheImages: data?.cacheImages,
|
cacheImages: data?.cacheImages,
|
||||||
proxyEnabled: data?.proxy?.enabled,
|
proxyEnabled: data?.proxy?.enabled,
|
||||||
@@ -188,6 +190,7 @@ const SettingsMain = () => {
|
|||||||
streamingRegion: values.streamingRegion,
|
streamingRegion: values.streamingRegion,
|
||||||
originalLanguage: values.originalLanguage,
|
originalLanguage: values.originalLanguage,
|
||||||
partialRequestsEnabled: values.partialRequestsEnabled,
|
partialRequestsEnabled: values.partialRequestsEnabled,
|
||||||
|
enableSpecialEpisodes: values.enableSpecialEpisodes,
|
||||||
trustProxy: values.trustProxy,
|
trustProxy: values.trustProxy,
|
||||||
cacheImages: values.cacheImages,
|
cacheImages: values.cacheImages,
|
||||||
proxy: {
|
proxy: {
|
||||||
@@ -498,6 +501,47 @@ const SettingsMain = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label
|
||||||
|
htmlFor="enableSpecialEpisodes"
|
||||||
|
className="checkbox-label"
|
||||||
|
>
|
||||||
|
<span className="mr-2">
|
||||||
|
{intl.formatMessage(messages.enableSpecialEpisodes)}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div className="form-input-area">
|
||||||
|
<Field
|
||||||
|
type="checkbox"
|
||||||
|
id="enableSpecialEpisodes"
|
||||||
|
name="enableSpecialEpisodes"
|
||||||
|
onChange={() => {
|
||||||
|
setFieldValue(
|
||||||
|
'enableSpecialEpisodes',
|
||||||
|
!values.enableSpecialEpisodes
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="actions">
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
||||||
|
<Button
|
||||||
|
buttonType="primary"
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting || !isValid}
|
||||||
|
>
|
||||||
|
<ArrowDownOnSquareIcon />
|
||||||
|
<span>
|
||||||
|
{isSubmitting
|
||||||
|
? intl.formatMessage(globalMessages.saving)
|
||||||
|
: intl.formatMessage(globalMessages.save)}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label htmlFor="proxyEnabled" className="checkbox-label">
|
<label htmlFor="proxyEnabled" className="checkbox-label">
|
||||||
<span className="mr-2">
|
<span className="mr-2">
|
||||||
@@ -668,24 +712,6 @@ const SettingsMain = () => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="actions">
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<span className="ml-3 inline-flex rounded-md shadow-sm">
|
|
||||||
<Button
|
|
||||||
buttonType="primary"
|
|
||||||
type="submit"
|
|
||||||
disabled={isSubmitting || !isValid}
|
|
||||||
>
|
|
||||||
<ArrowDownOnSquareIcon />
|
|
||||||
<span>
|
|
||||||
{isSubmitting
|
|
||||||
? intl.formatMessage(globalMessages.saving)
|
|
||||||
: intl.formatMessage(globalMessages.save)}
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -301,7 +301,9 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showHasSpecials = data.seasons.some(
|
const showHasSpecials = data.seasons.some(
|
||||||
(season) => season.seasonNumber === 0
|
(season) =>
|
||||||
|
season.seasonNumber === 0 &&
|
||||||
|
settings.currentSettings.partialRequestsEnabled
|
||||||
);
|
);
|
||||||
|
|
||||||
const isComplete =
|
const isComplete =
|
||||||
@@ -799,6 +801,11 @@ const TvDetails = ({ tv }: TvDetailsProps) => {
|
|||||||
{data.seasons
|
{data.seasons
|
||||||
.slice()
|
.slice()
|
||||||
.reverse()
|
.reverse()
|
||||||
|
.filter(
|
||||||
|
(season) =>
|
||||||
|
settings.currentSettings.enableSpecialEpisodes ||
|
||||||
|
season.seasonNumber !== 0
|
||||||
|
)
|
||||||
.map((season) => {
|
.map((season) => {
|
||||||
const show4k =
|
const show4k =
|
||||||
settings.currentSettings.series4kEnabled &&
|
settings.currentSettings.series4kEnabled &&
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const defaultSettings = {
|
|||||||
originalLanguage: '',
|
originalLanguage: '',
|
||||||
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
||||||
partialRequestsEnabled: true,
|
partialRequestsEnabled: true,
|
||||||
|
enableSpecialEpisodes: false,
|
||||||
cacheImages: false,
|
cacheImages: false,
|
||||||
vapidPublic: '',
|
vapidPublic: '',
|
||||||
enablePushRegistration: false,
|
enablePushRegistration: false,
|
||||||
|
|||||||
@@ -919,6 +919,7 @@
|
|||||||
"components.Settings.SettingsMain.csrfProtectionTip": "Set external API access to read-only (requires HTTPS)",
|
"components.Settings.SettingsMain.csrfProtectionTip": "Set external API access to read-only (requires HTTPS)",
|
||||||
"components.Settings.SettingsMain.discoverRegion": "Discover Region",
|
"components.Settings.SettingsMain.discoverRegion": "Discover Region",
|
||||||
"components.Settings.SettingsMain.discoverRegionTip": "Filter content by regional availability",
|
"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.general": "General",
|
||||||
"components.Settings.SettingsMain.generalsettings": "General Settings",
|
"components.Settings.SettingsMain.generalsettings": "General Settings",
|
||||||
"components.Settings.SettingsMain.generalsettingsDescription": "Configure global and default settings for Jellyseerr.",
|
"components.Settings.SettingsMain.generalsettingsDescription": "Configure global and default settings for Jellyseerr.",
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ CoreApp.getInitialProps = async (initialProps) => {
|
|||||||
originalLanguage: '',
|
originalLanguage: '',
|
||||||
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
||||||
partialRequestsEnabled: true,
|
partialRequestsEnabled: true,
|
||||||
|
enableSpecialEpisodes: false,
|
||||||
cacheImages: false,
|
cacheImages: false,
|
||||||
vapidPublic: '',
|
vapidPublic: '',
|
||||||
enablePushRegistration: false,
|
enablePushRegistration: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user