mirror of
https://github.com/seerr-team/seerr.git
synced 2025-12-23 23:58:07 -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,
|
||||
"mediaServerType": 1,
|
||||
"partialRequestsEnabled": true,
|
||||
"enableSpecialEpisodes": false,
|
||||
"locale": "en"
|
||||
},
|
||||
"plex": {
|
||||
|
||||
@@ -188,6 +188,9 @@ components:
|
||||
defaultPermissions:
|
||||
type: number
|
||||
example: 32
|
||||
enableSpecialEpisodes:
|
||||
type: boolean
|
||||
example: false
|
||||
PlexLibrary:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface PublicSettingsResponse {
|
||||
originalLanguage: string;
|
||||
mediaServerType: number;
|
||||
partialRequestsEnabled: boolean;
|
||||
enableSpecialEpisodes: boolean;
|
||||
cacheImages: boolean;
|
||||
vapidPublic: string;
|
||||
enablePushRegistration: boolean;
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
<span className="mr-2 font-bold ">
|
||||
{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,
|
||||
})}
|
||||
|
||||
@@ -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) => {
|
||||
<span className="card-field-name">
|
||||
{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,
|
||||
})}
|
||||
|
||||
@@ -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 = ({
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-700">
|
||||
{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
|
||||
|
||||
@@ -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 = () => {
|
||||
/>
|
||||
</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">
|
||||
<label htmlFor="proxyEnabled" className="checkbox-label">
|
||||
<span className="mr-2">
|
||||
@@ -668,24 +712,6 @@ const SettingsMain = () => {
|
||||
</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>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -21,6 +21,7 @@ const defaultSettings = {
|
||||
originalLanguage: '',
|
||||
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
||||
partialRequestsEnabled: true,
|
||||
enableSpecialEpisodes: false,
|
||||
cacheImages: false,
|
||||
vapidPublic: '',
|
||||
enablePushRegistration: false,
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -199,6 +199,7 @@ CoreApp.getInitialProps = async (initialProps) => {
|
||||
originalLanguage: '',
|
||||
mediaServerType: MediaServerType.NOT_CONFIGURED,
|
||||
partialRequestsEnabled: true,
|
||||
enableSpecialEpisodes: false,
|
||||
cacheImages: false,
|
||||
vapidPublic: '',
|
||||
enablePushRegistration: false,
|
||||
|
||||
Reference in New Issue
Block a user