New: Option to control whether new seasons get monitored automatically

(cherry picked from commit b95a84f6612333d96fcdca083f9c39d96956f3f4)
Closes #5083
This commit is contained in:
Qstick 2023-10-21 21:57:59 -05:00 committed by Mark McDowall
parent 5328fb4ab3
commit ade40b72bc
24 changed files with 236 additions and 8 deletions

View File

@ -0,0 +1,22 @@
import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import translate from 'Utilities/String/translate';
function SeriesMonitorNewItemsOptionsPopoverContent() {
return (
<DescriptionList>
<DescriptionListItem
title={translate('MonitorAllSeasons')}
data={translate('MonitorAllSeasonsDescription')}
/>
<DescriptionListItem
title={translate('MonitorNone')}
data={translate('MonitorNoNewSeasonsDescription')}
/>
</DescriptionList>
);
}
export default SeriesMonitorNewItemsOptionsPopoverContent;

View File

@ -14,6 +14,7 @@ import FormInputHelpText from './FormInputHelpText';
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
import KeyValueListInput from './KeyValueListInput';
import MonitorEpisodesSelectInput from './MonitorEpisodesSelectInput';
import MonitorNewItemsSelectInput from './MonitorNewItemsSelectInput';
import NumberInput from './NumberInput';
import OAuthInputConnector from './OAuthInputConnector';
import PasswordInput from './PasswordInput';
@ -49,6 +50,9 @@ function getComponent(type) {
case inputTypes.MONITOR_EPISODES_SELECT:
return MonitorEpisodesSelectInput;
case inputTypes.MONITOR_NEW_ITEMS_SELECT:
return MonitorNewItemsSelectInput;
case inputTypes.NUMBER:
return NumberInput;

View File

@ -0,0 +1,50 @@
import PropTypes from 'prop-types';
import React from 'react';
import monitorNewItemsOptions from 'Utilities/Series/monitorNewItemsOptions';
import SelectInput from './SelectInput';
function MonitorNewItemsSelectInput(props) {
const {
includeNoChange,
includeMixed,
...otherProps
} = props;
const values = [...monitorNewItemsOptions];
if (includeNoChange) {
values.unshift({
key: 'noChange',
value: 'No Change',
disabled: true
});
}
if (includeMixed) {
values.unshift({
key: 'mixed',
value: '(Mixed)',
disabled: true
});
}
return (
<SelectInput
values={values}
{...otherProps}
/>
);
}
MonitorNewItemsSelectInput.propTypes = {
includeNoChange: PropTypes.bool.isRequired,
includeMixed: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired
};
MonitorNewItemsSelectInput.defaultProps = {
includeNoChange: false,
includeMixed: false
};
export default MonitorNewItemsSelectInput;

View File

@ -4,6 +4,7 @@ export const CHECK = 'check';
export const DEVICE = 'device';
export const KEY_VALUE_LIST = 'keyValueList';
export const MONITOR_EPISODES_SELECT = 'monitorEpisodesSelect';
export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect';
export const FLOAT = 'float';
export const NUMBER = 'number';
export const OAUTH = 'oauth';
@ -31,6 +32,7 @@ export const all = [
DEVICE,
KEY_VALUE_LIST,
MONITOR_EPISODES_SELECT,
MONITOR_NEW_ITEMS_SELECT,
FLOAT,
NUMBER,
OAUTH,

View File

@ -3,3 +3,7 @@
margin-right: auto;
}
.labelIcon {
margin-left: 8px;
}

View File

@ -2,6 +2,7 @@
// Please do not change this file!
interface CssExports {
'deleteButton': string;
'labelIcon': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@ -1,16 +1,19 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import SeriesMonitorNewItemsOptionsPopoverContent from 'AddSeries/SeriesMonitorNewItemsOptionsPopoverContent';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import MoveSeriesModal from 'Series/MoveSeries/MoveSeriesModal';
import translate from 'Utilities/String/translate';
import styles from './EditSeriesModalContent.css';
@ -73,6 +76,7 @@ class EditSeriesModalContent extends Component {
const {
monitored,
monitorNewItems,
seasonFolder,
qualityProfileId,
seriesType,
@ -100,6 +104,31 @@ class EditSeriesModalContent extends Component {
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('MonitorNewSeasons')}
<Popover
anchor={
<Icon
className={styles.labelIcon}
name={icons.INFO}
/>
}
title={translate('MonitorNewSeasons')}
body={<SeriesMonitorNewItemsOptionsPopoverContent />}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup
type={inputTypes.MONITOR_NEW_ITEMS_SELECT}
name="monitorNewItems"
helpText={translate('MonitorNewSeasonsHelpText')}
{...monitorNewItems}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('UseSeasonFolder')}</FormLabel>

View File

@ -38,6 +38,7 @@ function createMapStateToProps() {
const seriesSettings = _.pick(series, [
'monitored',
'monitorNewItems',
'seasonFolder',
'qualityProfileId',
'seriesType',

View File

@ -14,6 +14,7 @@ import styles from './EditSeriesModalContent.css';
interface SavePayload {
monitored?: boolean;
monitorNewItems?: string;
qualityProfileId?: number;
seriesType?: string;
seasonFolder?: boolean;
@ -77,6 +78,7 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
const { seriesIds, onSavePress, onModalClose } = props;
const [monitored, setMonitored] = useState(NO_CHANGE);
const [monitorNewItems, setMonitorNewItems] = useState(NO_CHANGE);
const [qualityProfileId, setQualityProfileId] = useState<string | number>(
NO_CHANGE
);
@ -95,6 +97,11 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
payload.monitored = monitored === 'monitored';
}
if (monitorNewItems !== NO_CHANGE) {
hasChanges = true;
payload.monitorNewItems = monitorNewItems;
}
if (qualityProfileId !== NO_CHANGE) {
hasChanges = true;
payload.qualityProfileId = qualityProfileId as number;
@ -124,6 +131,7 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
},
[
monitored,
monitorNewItems,
qualityProfileId,
seriesType,
seasonFolder,
@ -139,6 +147,9 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
case 'monitored':
setMonitored(value);
break;
case 'monitorNewItems':
setMonitorNewItems(value);
break;
case 'qualityProfileId':
setQualityProfileId(value);
break;
@ -199,6 +210,19 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('MonitorNewItems')}</FormLabel>
<FormInputGroup
type={inputTypes.MONITOR_NEW_ITEMS_SELECT}
name="monitorNewItems"
value={monitorNewItems}
includeNoChange={true}
includeNoChangeDisabled={false}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel>

View File

@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
import SeriesMonitorNewItemsOptionsPopoverContent from 'AddSeries/SeriesMonitorNewItemsOptionsPopoverContent';
import SeriesTypePopoverContent from 'AddSeries/SeriesTypePopoverContent';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form';
@ -50,6 +51,7 @@ function EditImportListModalContent(props) {
minRefreshInterval,
shouldMonitor,
rootFolderPath,
monitorNewItems,
qualityProfileId,
seriesType,
seasonFolder,
@ -151,6 +153,31 @@ function EditImportListModalContent(props) {
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('MonitorNewSeasons')}
<Popover
anchor={
<Icon
className={styles.labelIcon}
name={icons.INFO}
/>
}
title={translate('MonitorNewSeasons')}
body={<SeriesMonitorNewItemsOptionsPopoverContent />}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup
type={inputTypes.MONITOR_NEW_ITEMS_SELECT}
name="monitorNewItems"
helpText={translate('MonitorNewSeasonsHelpText')}
{...monitorNewItems}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('RootFolder')}</FormLabel>

View File

@ -3,6 +3,7 @@ function getNewSeries(series, payload) {
const {
rootFolderPath,
monitor,
monitorNewItems,
qualityProfileId,
seriesType,
seasonFolder,
@ -19,6 +20,7 @@ function getNewSeries(series, payload) {
series.addOptions = addOptions;
series.monitored = true;
series.monitorNewItems = monitorNewItems;
series.qualityProfileId = qualityProfileId;
series.rootFolderPath = rootFolderPath;
series.seriesType = seriesType;

View File

@ -0,0 +1,18 @@
import translate from 'Utilities/String/translate';
const monitorNewItemsOptions = [
{
key: 'all',
get value() {
return translate('MonitorAllSeasons');
}
},
{
key: 'none',
get value() {
return translate('MonitorNone');
}
}
];
export default monitorNewItemsOptions;

View File

@ -58,9 +58,10 @@ namespace NzbDrone.Core.Test.TvTests
}
[Test]
public void should_monitor_new_seasons_automatically_if_series_is_monitored()
public void should_monitor_new_seasons_automatically_if_monitor_new_items_is_all()
{
_series.Monitored = true;
_series.MonitorNewItems = NewItemMonitorTypes.All;
var newSeriesInfo = _series.JsonClone();
newSeriesInfo.Seasons.Add(Builder<Season>.CreateNew()
.With(s => s.SeasonNumber = 2)
@ -75,9 +76,10 @@ namespace NzbDrone.Core.Test.TvTests
}
[Test]
public void should_not_monitor_new_seasons_automatically_if_series_is_not_monitored()
public void should_not_monitor_new_seasons_automatically_if_monitor_new_items_is_none()
{
_series.Monitored = false;
_series.MonitorNewItems = NewItemMonitorTypes.None;
var newSeriesInfo = _series.JsonClone();
newSeriesInfo.Seasons.Add(Builder<Season>.CreateNew()
.With(s => s.SeasonNumber = 2)

View File

@ -0,0 +1,15 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(200)]
public class AddNewItemMonitorType : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("Series").AddColumn("MonitorNewItems").AsInt32().WithDefaultValue(0);
Alter.Table("ImportLists").AddColumn("MonitorNewItems").AsInt32().WithDefaultValue(0);
}
}
}

View File

@ -9,6 +9,7 @@ namespace NzbDrone.Core.ImportLists
public bool EnableAutomaticAdd { get; set; }
public bool SearchForMissingEpisodes { get; set; }
public MonitorTypes ShouldMonitor { get; set; }
public NewItemMonitorTypes MonitorNewItems { get; set; }
public int QualityProfileId { get; set; }
public SeriesTypes SeriesType { get; set; }
public bool SeasonFolder { get; set; }

View File

@ -173,6 +173,7 @@ namespace NzbDrone.Core.ImportLists
Title = item.Title,
Year = item.Year,
Monitored = monitored,
MonitorNewItems = importList.MonitorNewItems,
RootFolderPath = importList.RootFolderPath,
QualityProfileId = importList.QualityProfileId,
SeriesType = importList.SeriesType,

View File

@ -976,6 +976,8 @@
"Monitor": "Monitor",
"MonitorAllEpisodes": "All Episodes",
"MonitorAllEpisodesDescription": "Monitor all episodes except specials",
"MonitorAllSeasons": "All Seasons",
"MonitorAllSeasonsDescription": "Monitor all new seasons automatically",
"MonitorExistingEpisodes": "Existing Episodes",
"MonitorExistingEpisodesDescription": "Monitor episodes that have files or have not aired yet",
"MonitorFirstSeason": "First Season",
@ -986,6 +988,9 @@
"MonitorLatestSeasonDescription": "Monitor all episodes of the latest season that aired within the last 90 days and all future seasons",
"MonitorMissingEpisodes": "Missing Episodes",
"MonitorMissingEpisodesDescription": "Monitor episodes that do not have files or have not aired yet",
"MonitorNewSeasons": "Monitor New Seasons",
"MonitorNewSeasonsHelpText": "Which new seasons should be monitored automatically",
"MonitorNoNewSeasonsDescription": "Do not monitor any new seasons automatically",
"MonitorNone": "None",
"MonitorNoneDescription": "No episodes will be monitored",
"MonitorPilotEpisode": "Pilot Episode",

View File

@ -23,4 +23,10 @@ namespace NzbDrone.Core.Tv
UnmonitorSpecials,
None
}
public enum NewItemMonitorTypes
{
All,
None
}
}

View File

@ -63,7 +63,7 @@ namespace NzbDrone.Core.Tv
else
{
episodeToUpdate = new Episode();
episodeToUpdate.Monitored = GetMonitoredStatus(episode, seasons);
episodeToUpdate.Monitored = GetMonitoredStatus(episode, seasons, series);
newList.Add(episodeToUpdate);
}
@ -135,9 +135,9 @@ namespace NzbDrone.Core.Tv
}
}
private bool GetMonitoredStatus(Episode episode, IEnumerable<Season> seasons)
private bool GetMonitoredStatus(Episode episode, IEnumerable<Season> seasons, Series series)
{
if (episode.EpisodeNumber == 0 && episode.SeasonNumber != 1)
if ((episode.EpisodeNumber == 0 && episode.SeasonNumber != 1) || series.MonitorNewItems == NewItemMonitorTypes.None)
{
return false;
}

View File

@ -30,6 +30,7 @@ namespace NzbDrone.Core.Tv
public string Overview { get; set; }
public string AirTime { get; set; }
public bool Monitored { get; set; }
public NewItemMonitorTypes MonitorNewItems { get; set; }
public int QualityProfileId { get; set; }
public bool SeasonFolder { get; set; }
public DateTime? LastInfoSync { get; set; }
@ -71,6 +72,7 @@ namespace NzbDrone.Core.Tv
SeasonFolder = otherSeries.SeasonFolder;
Monitored = otherSeries.Monitored;
MonitorNewItems = otherSeries.MonitorNewItems;
SeriesType = otherSeries.SeriesType;
RootFolderPath = otherSeries.RootFolderPath;

View File

@ -9,6 +9,7 @@ namespace Sonarr.Api.V3.ImportLists
public bool EnableAutomaticAdd { get; set; }
public bool SearchForMissingEpisodes { get; set; }
public MonitorTypes ShouldMonitor { get; set; }
public NewItemMonitorTypes MonitorNewItems { get; set; }
public string RootFolderPath { get; set; }
public int QualityProfileId { get; set; }
public SeriesTypes SeriesType { get; set; }
@ -32,6 +33,7 @@ namespace Sonarr.Api.V3.ImportLists
resource.EnableAutomaticAdd = definition.EnableAutomaticAdd;
resource.SearchForMissingEpisodes = definition.SearchForMissingEpisodes;
resource.ShouldMonitor = definition.ShouldMonitor;
resource.MonitorNewItems = definition.MonitorNewItems;
resource.RootFolderPath = definition.RootFolderPath;
resource.QualityProfileId = definition.QualityProfileId;
resource.SeriesType = definition.SeriesType;
@ -55,6 +57,7 @@ namespace Sonarr.Api.V3.ImportLists
definition.EnableAutomaticAdd = resource.EnableAutomaticAdd;
definition.SearchForMissingEpisodes = resource.SearchForMissingEpisodes;
definition.ShouldMonitor = resource.ShouldMonitor;
definition.MonitorNewItems = resource.MonitorNewItems;
definition.RootFolderPath = resource.RootFolderPath;
definition.QualityProfileId = resource.QualityProfileId;
definition.SeriesType = resource.SeriesType;

View File

@ -34,6 +34,11 @@ namespace Sonarr.Api.V3.Series
series.Monitored = resource.Monitored.Value;
}
if (resource.MonitorNewItems.HasValue)
{
series.MonitorNewItems = resource.MonitorNewItems.Value;
}
if (resource.QualityProfileId.HasValue)
{
series.QualityProfileId = resource.QualityProfileId.Value;

View File

@ -7,6 +7,7 @@ namespace Sonarr.Api.V3.Series
{
public List<int> SeriesIds { get; set; }
public bool? Monitored { get; set; }
public NewItemMonitorTypes? MonitorNewItems { get; set; }
public int? QualityProfileId { get; set; }
public SeriesTypes? SeriesType { get; set; }
public bool? SeasonFolder { get; set; }

View File

@ -44,6 +44,7 @@ namespace Sonarr.Api.V3.Series
// Editing Only
public bool SeasonFolder { get; set; }
public bool Monitored { get; set; }
public NewItemMonitorTypes MonitorNewItems { get; set; }
public bool UseSceneNumbering { get; set; }
public int Runtime { get; set; }
@ -115,6 +116,7 @@ namespace Sonarr.Api.V3.Series
SeasonFolder = model.SeasonFolder,
Monitored = model.Monitored,
MonitorNewItems = model.MonitorNewItems,
UseSceneNumbering = model.UseSceneNumbering,
Runtime = model.Runtime,
@ -178,6 +180,7 @@ namespace Sonarr.Api.V3.Series
SeasonFolder = resource.SeasonFolder,
Monitored = resource.Monitored,
MonitorNewItems = resource.MonitorNewItems,
UseSceneNumbering = resource.UseSceneNumbering,
Runtime = resource.Runtime,