New: Option to control which new author books get monitored

pull/1362/head
ta264 3 years ago
parent 1d694af98e
commit c51ae664aa

@ -0,0 +1,27 @@
import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import translate from 'Utilities/String/translate';
function AuthorMonitorNewItemsOptionsPopoverContent() {
return (
<DescriptionList>
<DescriptionListItem
title={translate('AllBooks')}
data="Monitor all new books"
/>
<DescriptionListItem
title={translate('NewBooks')}
data="Monitor new books released after the newest existing book"
/>
<DescriptionListItem
title={translate('None')}
data="Don't monitor any new books"
/>
</DescriptionList>
);
}
export default AuthorMonitorNewItemsOptionsPopoverContent;

@ -1,46 +1,52 @@
import React from 'react'; import React from 'react';
import Alert from 'Components/Alert';
import DescriptionList from 'Components/DescriptionList/DescriptionList'; import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
function AuthorMonitoringOptionsPopoverContent() { function AuthorMonitoringOptionsPopoverContent() {
return ( return (
<DescriptionList> <>
<DescriptionListItem <Alert>
title={translate('AllBooks')} This is a one time adjustment to set which books are monitored
data="Monitor all books" </Alert>
/> <DescriptionList>
<DescriptionListItem
<DescriptionListItem title={translate('AllBooks')}
title={translate('FutureBooks')} data="Monitor all books"
data="Monitor books that have not released yet" />
/>
<DescriptionListItem
<DescriptionListItem title={translate('FutureBooks')}
title={translate('MissingBooks')} data="Monitor books that have not released yet"
data="Monitor books that do not have files or have not released yet" />
/>
<DescriptionListItem
<DescriptionListItem title={translate('MissingBooks')}
title={translate('ExistingBooks')} data="Monitor books that do not have files or have not released yet"
data="Monitor books that have files or have not released yet" />
/>
<DescriptionListItem
<DescriptionListItem title={translate('ExistingBooks')}
title={translate('FirstBook')} data="Monitor books that have files or have not released yet"
data="Monitor the first book. All other books will be ignored" />
/>
<DescriptionListItem
<DescriptionListItem title={translate('FirstBook')}
title={translate('LatestBook')} data="Monitor the first book. All other books will be ignored"
data="Monitor the latest book and future books" />
/>
<DescriptionListItem
<DescriptionListItem title={translate('LatestBook')}
title={translate('None')} data="Monitor the latest book and future books"
data="No books will be monitored" />
/>
</DescriptionList> <DescriptionListItem
title={translate('None')}
data="No books will be monitored"
/>
</DescriptionList>
</>
); );
} }

@ -1,6 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import AuthorMetadataProfilePopoverContent from 'AddAuthor/AuthorMetadataProfilePopoverContent'; import AuthorMetadataProfilePopoverContent from 'AddAuthor/AuthorMetadataProfilePopoverContent';
import AuthorMonitorNewItemsOptionsPopoverContent from 'AddAuthor/AuthorMonitorNewItemsOptionsPopoverContent';
import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal'; import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal';
import Form from 'Components/Form/Form'; import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup'; import FormGroup from 'Components/Form/FormGroup';
@ -73,6 +74,7 @@ class EditAuthorModalContent extends Component {
const { const {
monitored, monitored,
monitorNewItems,
qualityProfileId, qualityProfileId,
metadataProfileId, metadataProfileId,
path, path,
@ -101,6 +103,31 @@ class EditAuthorModalContent extends Component {
/> />
</FormGroup> </FormGroup>
<FormGroup>
<FormLabel>
{translate('MonitorNewItems')}
<Popover
anchor={
<Icon
className={styles.labelIcon}
name={icons.INFO}
/>
}
title={translate('MonitorNewItems')}
body={<AuthorMonitorNewItemsOptionsPopoverContent />}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup
type={inputTypes.MONITOR_NEW_ITEMS_SELECT}
name="monitorNewItems"
helpText={translate('MonitorNewItemsHelpText')}
{...monitorNewItems}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup> <FormGroup>
<FormLabel> <FormLabel>
{translate('QualityProfile')} {translate('QualityProfile')}

@ -39,6 +39,7 @@ function createMapStateToProps() {
const authorSettings = _.pick(author, [ const authorSettings = _.pick(author, [
'monitored', 'monitored',
'monitorNewItems',
'qualityProfileId', 'qualityProfileId',
'metadataProfileId', 'metadataProfileId',
'path', 'path',

@ -1,11 +1,23 @@
.footer {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.dropdownContainer {
display: flex;
flex-wrap: wrap;
margin-bottom: 10px;
}
.inputContainer { .inputContainer {
flex: 1;
margin-right: 20px; margin-right: 20px;
min-width: 150px; min-width: 150px;
} }
.buttonContainer { .buttonContainer {
display: flex; display: flex;
justify-content: flex-end;
flex-grow: 1; flex-grow: 1;
} }
@ -24,12 +36,14 @@
composes: button from '~Components/Link/SpinnerButton.css'; composes: button from '~Components/Link/SpinnerButton.css';
margin-right: 10px; margin-right: 10px;
margin-bottom: 10px;
height: 35px; height: 35px;
} }
.deleteSelectedButton { .deleteSelectedButton {
composes: button from '~Components/Link/SpinnerButton.css'; composes: button from '~Components/Link/SpinnerButton.css';
margin-bottom: 10px;
margin-left: 50px; margin-left: 50px;
height: 35px; height: 35px;
} }
@ -48,6 +62,10 @@
} }
@media only screen and (max-width: $breakpointSmall) { @media only screen and (max-width: $breakpointSmall) {
.dropdownContainer {
display: block;
}
.inputContainer { .inputContainer {
margin-right: 0; margin-right: 0;
} }
@ -61,6 +79,7 @@
} }
.buttons { .buttons {
display: block;
justify-content: space-between; justify-content: space-between;
} }

@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal'; import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal';
import MetadataProfileSelectInputConnector from 'Components/Form/MetadataProfileSelectInputConnector'; import MetadataProfileSelectInputConnector from 'Components/Form/MetadataProfileSelectInputConnector';
import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput';
import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSelectInputConnector'; import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSelectInputConnector';
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector'; import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
import SelectInput from 'Components/Form/SelectInput'; import SelectInput from 'Components/Form/SelectInput';
@ -26,6 +27,7 @@ class AuthorEditorFooter extends Component {
this.state = { this.state = {
monitored: NO_CHANGE, monitored: NO_CHANGE,
monitorNewItems: NO_CHANGE,
qualityProfileId: NO_CHANGE, qualityProfileId: NO_CHANGE,
metadataProfileId: NO_CHANGE, metadataProfileId: NO_CHANGE,
rootFolderPath: NO_CHANGE, rootFolderPath: NO_CHANGE,
@ -46,6 +48,7 @@ class AuthorEditorFooter extends Component {
if (prevProps.isSaving && !isSaving && !saveError) { if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({ this.setState({
monitored: NO_CHANGE, monitored: NO_CHANGE,
monitorNewItems: NO_CHANGE,
qualityProfileId: NO_CHANGE, qualityProfileId: NO_CHANGE,
metadataProfileId: NO_CHANGE, metadataProfileId: NO_CHANGE,
rootFolderPath: NO_CHANGE, rootFolderPath: NO_CHANGE,
@ -145,6 +148,7 @@ class AuthorEditorFooter extends Component {
const { const {
monitored, monitored,
monitorNewItems,
qualityProfileId, qualityProfileId,
metadataProfileId, metadataProfileId,
rootFolderPath, rootFolderPath,
@ -163,83 +167,99 @@ class AuthorEditorFooter extends Component {
return ( return (
<PageContentFooter> <PageContentFooter>
<div className={styles.inputContainer}> <div className={styles.footer}>
<AuthorEditorFooterLabel <div className={styles.dropdownContainer}>
label={translate('MonitorAuthor')} <div className={styles.inputContainer}>
isSaving={isSaving && monitored !== NO_CHANGE} <AuthorEditorFooterLabel
/> label={translate('MonitorAuthor')}
isSaving={isSaving && monitored !== NO_CHANGE}
<SelectInput />
name="monitored"
value={monitored} <SelectInput
values={monitoredOptions} name="monitored"
isDisabled={!selectedCount} value={monitored}
onChange={this.onInputChange} values={monitoredOptions}
/> isDisabled={!selectedCount}
</div> onChange={this.onInputChange}
/>
</div>
<div <div className={styles.inputContainer}>
className={styles.inputContainer} <AuthorEditorFooterLabel
> label={translate('MonitorNewItems')}
<AuthorEditorFooterLabel isSaving={isSaving && monitored !== NO_CHANGE}
label={translate('QualityProfile')} />
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
/> <MonitorNewItemsSelectInput
name="monitorNewItems"
<QualityProfileSelectInputConnector value={monitorNewItems}
name="qualityProfileId" includeNoChange={true}
value={qualityProfileId} isDisabled={!selectedCount}
includeNoChange={true} onChange={this.onInputChange}
isDisabled={!selectedCount} />
onChange={this.onInputChange} </div>
/>
</div>
<div <div className={styles.inputContainer}>
className={styles.inputContainer} <AuthorEditorFooterLabel
> label={translate('QualityProfile')}
<AuthorEditorFooterLabel isSaving={isSaving && qualityProfileId !== NO_CHANGE}
label={translate('MetadataProfile')} />
isSaving={isSaving && metadataProfileId !== NO_CHANGE}
/> <QualityProfileSelectInputConnector
name="qualityProfileId"
<MetadataProfileSelectInputConnector value={qualityProfileId}
name="metadataProfileId" includeNoChange={true}
value={metadataProfileId} isDisabled={!selectedCount}
includeNoChange={true} onChange={this.onInputChange}
includeNone={true} />
isDisabled={!selectedCount} </div>
onChange={this.onInputChange}
/>
</div>
<div <div
className={styles.inputContainer} className={styles.inputContainer}
> >
<AuthorEditorFooterLabel <AuthorEditorFooterLabel
label={translate('RootFolder')} label={translate('MetadataProfile')}
isSaving={isSaving && rootFolderPath !== NO_CHANGE} isSaving={isSaving && metadataProfileId !== NO_CHANGE}
/> />
<RootFolderSelectInputConnector <MetadataProfileSelectInputConnector
name="rootFolderPath" name="metadataProfileId"
value={rootFolderPath} value={metadataProfileId}
includeNoChange={true} includeNoChange={true}
isDisabled={!selectedCount} includeNone={true}
selectedValueOptions={{ includeFreeSpace: false }} isDisabled={!selectedCount}
onChange={this.onInputChange} onChange={this.onInputChange}
/> />
</div> </div>
<div
className={styles.inputContainer}
>
<AuthorEditorFooterLabel
label={translate('RootFolder')}
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
/>
<RootFolderSelectInputConnector
name="rootFolderPath"
value={rootFolderPath}
includeNoChange={true}
isDisabled={!selectedCount}
selectedValueOptions={{ includeFreeSpace: false }}
onChange={this.onInputChange}
/>
</div>
</div>
<div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}>
<AuthorEditorFooterLabel
label={translate('SelectedCountAuthorsSelectedInterp', [selectedCount])}
isSaving={false}
/>
<div className={styles.buttonContainer}> <div className={styles.buttons}>
<div className={styles.buttonContainerContent}>
<AuthorEditorFooterLabel
label={translate('SelectedCountAuthorsSelectedInterp', [selectedCount])}
isSaving={false}
/>
<div className={styles.buttons}>
<div>
<SpinnerButton <SpinnerButton
className={styles.organizeSelectedButton} className={styles.organizeSelectedButton}
kind={kinds.WARNING} kind={kinds.WARNING}
@ -268,17 +288,18 @@ class AuthorEditorFooter extends Component {
> >
Set Readarr Tags Set Readarr Tags
</SpinnerButton> </SpinnerButton>
</div>
<SpinnerButton <SpinnerButton
className={styles.deleteSelectedButton} className={styles.deleteSelectedButton}
kind={kinds.DANGER} kind={kinds.DANGER}
isSpinning={isDeleting} isSpinning={isDeleting}
isDisabled={!selectedCount || isDeleting} isDisabled={!selectedCount || isDeleting}
onPress={this.onDeleteSelectedPress} onPress={this.onDeleteSelectedPress}
> >
Delete Delete
</SpinnerButton> </SpinnerButton>
</div>
</div> </div>
</div> </div>
</div> </div>

@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form'; import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup'; import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup'; import FormInputGroup from 'Components/Form/FormInputGroup';
@ -10,7 +11,7 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes } from 'Helpers/Props'; import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
const NO_CHANGE = 'noChange'; const NO_CHANGE = 'noChange';
@ -92,6 +93,12 @@ class MonitoringOptionsModalContent extends Component {
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<Alert kind={kinds.INFO}>
<div>
{translate('MonitorBookExistingOnlyWarning')}
</div>
</Alert>
<Form {...otherProps}> <Form {...otherProps}>
<FormGroup> <FormGroup>
<FormLabel>{translate('Monitoring')}</FormLabel> <FormLabel>{translate('Monitoring')}</FormLabel>

@ -1,6 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import MonitorBooksSelectInput from 'Components/Form/MonitorBooksSelectInput'; import MonitorBooksSelectInput from 'Components/Form/MonitorBooksSelectInput';
import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput';
import SelectInput from 'Components/Form/SelectInput'; import SelectInput from 'Components/Form/SelectInput';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter'; import PageContentFooter from 'Components/Page/PageContentFooter';
@ -19,7 +20,8 @@ class BookshelfFooter extends Component {
this.state = { this.state = {
monitored: NO_CHANGE, monitored: NO_CHANGE,
monitor: NO_CHANGE monitor: NO_CHANGE,
monitorNewItems: NO_CHANGE
}; };
} }
@ -32,7 +34,8 @@ class BookshelfFooter extends Component {
if (prevProps.isSaving && !isSaving && !saveError) { if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({ this.setState({
monitored: NO_CHANGE, monitored: NO_CHANGE,
monitor: NO_CHANGE monitor: NO_CHANGE,
monitorNewItems: NO_CHANGE
}); });
} }
} }
@ -47,7 +50,8 @@ class BookshelfFooter extends Component {
onUpdateSelectedPress = () => { onUpdateSelectedPress = () => {
const { const {
monitor, monitor,
monitored monitored,
monitorNewItems
} = this.state; } = this.state;
const changes = {}; const changes = {};
@ -60,6 +64,10 @@ class BookshelfFooter extends Component {
changes.monitor = monitor; changes.monitor = monitor;
} }
if (monitorNewItems !== NO_CHANGE) {
changes.monitorNewItems = monitorNewItems;
}
this.props.onUpdateSelectedPress(changes); this.props.onUpdateSelectedPress(changes);
} }
@ -74,7 +82,8 @@ class BookshelfFooter extends Component {
const { const {
monitored, monitored,
monitor monitor,
monitorNewItems
} = this.state; } = this.state;
const monitoredOptions = [ const monitoredOptions = [
@ -83,7 +92,9 @@ class BookshelfFooter extends Component {
{ key: 'unmonitored', value: 'Unmonitored' } { key: 'unmonitored', value: 'Unmonitored' }
]; ];
const noChanges = monitored === NO_CHANGE && monitor === NO_CHANGE; const noChanges = monitored === NO_CHANGE &&
monitor === NO_CHANGE &&
monitorNewItems === NO_CHANGE;
return ( return (
<PageContentFooter> <PageContentFooter>
@ -103,7 +114,7 @@ class BookshelfFooter extends Component {
<div className={styles.inputContainer}> <div className={styles.inputContainer}>
<div className={styles.label}> <div className={styles.label}>
Monitor Books Monitor Existing Books
</div> </div>
<MonitorBooksSelectInput <MonitorBooksSelectInput
@ -115,6 +126,20 @@ class BookshelfFooter extends Component {
/> />
</div> </div>
<div className={styles.inputContainer}>
<div className={styles.label}>
Monitor New Books
</div>
<MonitorNewItemsSelectInput
name="monitorNewItems"
value={monitorNewItems}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div> <div>
<div className={styles.label}> <div className={styles.label}>
{selectedCount} Author(s) Selected {selectedCount} Author(s) Selected

@ -16,6 +16,7 @@ import IndexerSelectInputConnector from './IndexerSelectInputConnector';
import KeyValueListInput from './KeyValueListInput'; import KeyValueListInput from './KeyValueListInput';
import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector'; import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector';
import MonitorBooksSelectInput from './MonitorBooksSelectInput'; import MonitorBooksSelectInput from './MonitorBooksSelectInput';
import MonitorNewItemsSelectInput from './MonitorNewItemsSelectInput';
import NumberInput from './NumberInput'; import NumberInput from './NumberInput';
import OAuthInputConnector from './OAuthInputConnector'; import OAuthInputConnector from './OAuthInputConnector';
import PasswordInput from './PasswordInput'; import PasswordInput from './PasswordInput';
@ -51,6 +52,9 @@ function getComponent(type) {
case inputTypes.MONITOR_BOOKS_SELECT: case inputTypes.MONITOR_BOOKS_SELECT:
return MonitorBooksSelectInput; return MonitorBooksSelectInput;
case inputTypes.MONITOR_NEW_ITEMS_SELECT:
return MonitorNewItemsSelectInput;
case inputTypes.NUMBER: case inputTypes.NUMBER:
return NumberInput; return NumberInput;

@ -0,0 +1,50 @@
import PropTypes from 'prop-types';
import React from 'react';
import monitorNewItemsOptions from 'Utilities/Author/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;

@ -5,6 +5,7 @@ export const DEVICE = 'device';
export const BOOKSHELF = 'bookshelf'; export const BOOKSHELF = 'bookshelf';
export const KEY_VALUE_LIST = 'keyValueList'; export const KEY_VALUE_LIST = 'keyValueList';
export const MONITOR_BOOKS_SELECT = 'monitorBooksSelect'; export const MONITOR_BOOKS_SELECT = 'monitorBooksSelect';
export const MONITOR_NEW_ITEMS_SELECT = 'monitorNewItemsSelect';
export const NUMBER = 'number'; export const NUMBER = 'number';
export const OAUTH = 'oauth'; export const OAUTH = 'oauth';
export const PASSWORD = 'password'; export const PASSWORD = 'password';
@ -29,6 +30,7 @@ export const all = [
BOOKSHELF, BOOKSHELF,
KEY_VALUE_LIST, KEY_VALUE_LIST,
MONITOR_BOOKS_SELECT, MONITOR_BOOKS_SELECT,
MONITOR_NEW_ITEMS_SELECT,
NUMBER, NUMBER,
OAUTH, OAUTH,
PASSWORD, PASSWORD,

@ -57,6 +57,7 @@ class AddNewAuthorModalContentConnector extends Component {
foreignAuthorId, foreignAuthorId,
rootFolderPath, rootFolderPath,
monitor, monitor,
monitorNewItems,
qualityProfileId, qualityProfileId,
metadataProfileId, metadataProfileId,
tags tags
@ -66,6 +67,7 @@ class AddNewAuthorModalContentConnector extends Component {
foreignAuthorId, foreignAuthorId,
rootFolderPath: rootFolderPath.value, rootFolderPath: rootFolderPath.value,
monitor: monitor.value, monitor: monitor.value,
monitorNewItems: monitorNewItems.value,
qualityProfileId: qualityProfileId.value, qualityProfileId: qualityProfileId.value,
metadataProfileId: metadataProfileId.value, metadataProfileId: metadataProfileId.value,
tags: tags.value, tags: tags.value,
@ -91,6 +93,7 @@ AddNewAuthorModalContentConnector.propTypes = {
foreignAuthorId: PropTypes.string.isRequired, foreignAuthorId: PropTypes.string.isRequired,
rootFolderPath: PropTypes.object, rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired, monitor: PropTypes.object.isRequired,
monitorNewItems: PropTypes.object.isRequired,
qualityProfileId: PropTypes.object, qualityProfileId: PropTypes.object,
metadataProfileId: PropTypes.object, metadataProfileId: PropTypes.object,
tags: PropTypes.object.isRequired, tags: PropTypes.object.isRequired,

@ -58,6 +58,7 @@ class AddNewBookModalContentConnector extends Component {
foreignBookId, foreignBookId,
rootFolderPath, rootFolderPath,
monitor, monitor,
monitorNewItems,
qualityProfileId, qualityProfileId,
metadataProfileId, metadataProfileId,
tags tags
@ -67,6 +68,7 @@ class AddNewBookModalContentConnector extends Component {
foreignBookId, foreignBookId,
rootFolderPath: rootFolderPath.value, rootFolderPath: rootFolderPath.value,
monitor: monitor.value, monitor: monitor.value,
monitorNewItems: monitorNewItems.value,
qualityProfileId: qualityProfileId.value, qualityProfileId: qualityProfileId.value,
metadataProfileId: metadataProfileId.value, metadataProfileId: metadataProfileId.value,
tags: tags.value, tags: tags.value,
@ -93,6 +95,7 @@ AddNewBookModalContentConnector.propTypes = {
foreignBookId: PropTypes.string.isRequired, foreignBookId: PropTypes.string.isRequired,
rootFolderPath: PropTypes.object, rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired, monitor: PropTypes.object.isRequired,
monitorNewItems: PropTypes.object.isRequired,
qualityProfileId: PropTypes.object, qualityProfileId: PropTypes.object,
metadataProfileId: PropTypes.object, metadataProfileId: PropTypes.object,
tags: PropTypes.object.isRequired, tags: PropTypes.object.isRequired,

@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import AuthorMetadataProfilePopoverContent from 'AddAuthor/AuthorMetadataProfilePopoverContent'; import AuthorMetadataProfilePopoverContent from 'AddAuthor/AuthorMetadataProfilePopoverContent';
import AuthorMonitoringOptionsPopoverContent from 'AddAuthor/AuthorMonitoringOptionsPopoverContent'; import AuthorMonitoringOptionsPopoverContent from 'AddAuthor/AuthorMonitoringOptionsPopoverContent';
import AuthorMonitorNewItemsOptionsPopoverContent from 'AddAuthor/AuthorMonitorNewItemsOptionsPopoverContent';
import Form from 'Components/Form/Form'; import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup'; import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup'; import FormInputGroup from 'Components/Form/FormInputGroup';
@ -32,6 +33,7 @@ class AddAuthorOptionsForm extends Component {
const { const {
rootFolderPath, rootFolderPath,
monitor, monitor,
monitorNewItems,
qualityProfileId, qualityProfileId,
metadataProfileId, metadataProfileId,
includeNoneMetadataProfile, includeNoneMetadataProfile,
@ -77,12 +79,38 @@ class AddAuthorOptionsForm extends Component {
<FormInputGroup <FormInputGroup
type={inputTypes.MONITOR_BOOKS_SELECT} type={inputTypes.MONITOR_BOOKS_SELECT}
name="monitor" name="monitor"
helpText={translate('MonitoringOptionsHelpText')}
onChange={onInputChange} onChange={onInputChange}
includeSpecificBook={includeSpecificBookMonitor} includeSpecificBook={includeSpecificBookMonitor}
{...monitor} {...monitor}
/> />
</FormGroup> </FormGroup>
<FormGroup>
<FormLabel>
{translate('MonitorNewItems')}
<Popover
anchor={
<Icon
className={styles.labelIcon}
name={icons.INFO}
/>
}
title={translate('MonitorNewItems')}
body={<AuthorMonitorNewItemsOptionsPopoverContent />}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup
type={inputTypes.MONITOR_NEW_ITEMS_SELECT}
name="monitorNewItems"
helpText={translate('MonitorNewItemsHelpText')}
{...monitorNewItems}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup> <FormGroup>
<FormLabel> <FormLabel>
{translate('QualityProfile')} {translate('QualityProfile')}
@ -145,6 +173,7 @@ class AddAuthorOptionsForm extends Component {
AddAuthorOptionsForm.propTypes = { AddAuthorOptionsForm.propTypes = {
rootFolderPath: PropTypes.object, rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired, monitor: PropTypes.object.isRequired,
monitorNewItems: PropTypes.string.isRequired,
qualityProfileId: PropTypes.object, qualityProfileId: PropTypes.object,
metadataProfileId: PropTypes.object, metadataProfileId: PropTypes.object,
showMetadataProfile: PropTypes.bool.isRequired, showMetadataProfile: PropTypes.bool.isRequired,

@ -1,8 +1,10 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import AuthorMonitorNewItemsOptionsPopoverContent from 'AddAuthor/AuthorMonitorNewItemsOptionsPopoverContent';
import Alert from 'Components/Alert'; import Alert from 'Components/Alert';
import DescriptionList from 'Components/DescriptionList/DescriptionList'; import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form'; import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup'; import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup'; import FormInputGroup from 'Components/Form/FormInputGroup';
@ -76,6 +78,7 @@ function EditImportListModalContent(props) {
shouldMonitorExisting, shouldMonitorExisting,
shouldSearch, shouldSearch,
rootFolderPath, rootFolderPath,
monitorNewItems,
qualityProfileId, qualityProfileId,
metadataProfileId, metadataProfileId,
tags, tags,
@ -114,148 +117,178 @@ function EditImportListModalContent(props) {
{message.value.message} {message.value.message}
</Alert> </Alert>
} }
<FormGroup>
<FormLabel> <FieldSet legend={translate('ImportListSettings')} >
{translate('Name')} <FormGroup>
</FormLabel> <FormLabel>
{translate('Name')}
<FormInputGroup </FormLabel>
type={inputTypes.TEXT}
name="name" <FormInputGroup
{...name} type={inputTypes.TEXT}
onChange={onInputChange} name="name"
/> {...name}
</FormGroup> onChange={onInputChange}
/>
<FormGroup> </FormGroup>
<FormLabel>
{translate('EnableAutomaticAdd')} <FormGroup>
</FormLabel> <FormLabel>
{translate('EnableAutomaticAdd')}
<FormInputGroup </FormLabel>
type={inputTypes.CHECK}
name="enableAutomaticAdd" <FormInputGroup
helpText={translate('EnableAutomaticAddHelpText')} type={inputTypes.CHECK}
{...enableAutomaticAdd} name="enableAutomaticAdd"
onChange={onInputChange} helpText={translate('EnableAutomaticAddHelpText')}
/> {...enableAutomaticAdd}
</FormGroup> onChange={onInputChange}
/>
<FormGroup> </FormGroup>
<FormLabel>
Monitor <FormGroup>
<FormLabel>
<Popover Monitor
anchor={
<Icon <Popover
className={styles.labelIcon} anchor={
name={icons.INFO} <Icon
/> className={styles.labelIcon}
} name={icons.INFO}
title={translate('MonitoringOptions')} />
body={<ImportListMonitoringOptionsPopoverContent />} }
position={tooltipPositions.RIGHT} title={translate('MonitoringOptions')}
body={<ImportListMonitoringOptionsPopoverContent />}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="shouldMonitor"
values={monitorOptions}
helpText={translate('ShouldMonitorHelpText')}
{...shouldMonitor}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('ShouldMonitorExisting')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="shouldMonitorExisting"
helpText={translate('ShouldMonitorExistingHelpText')}
{...shouldMonitorExisting}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('SearchForNewItems')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="shouldSearch"
helpText={translate('ShouldSearchHelpText')}
{...shouldSearch}
onChange={onInputChange}
/>
</FormGroup>
</FieldSet>
<FieldSet legend={translate('AddedAuthorSettings')} >
<FormGroup>
<FormLabel>
{translate('RootFolder')}
</FormLabel>
<FormInputGroup
type={inputTypes.ROOT_FOLDER_SELECT}
name="rootFolderPath"
helpText={translate('RootFolderPathHelpText')}
{...rootFolderPath}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('MonitorNewItems')}
<Popover
anchor={
<Icon
className={styles.labelIcon}
name={icons.INFO}
/>
}
title={translate('MonitorNewItems')}
body={<AuthorMonitorNewItemsOptionsPopoverContent />}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup
type={inputTypes.MONITOR_NEW_ITEMS_SELECT}
name="monitorNewItems"
helpText={translate('MonitorNewItemsHelpText')}
{...monitorNewItems}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('QualityProfile')}
</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
helpText={translate('QualityProfileIdHelpText')}
{...qualityProfileId}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup className={showMetadataProfile ? undefined : styles.hideMetadataProfile}>
<FormLabel>
{translate('MetadataProfile')}
</FormLabel>
<FormInputGroup
type={inputTypes.METADATA_PROFILE_SELECT}
name="metadataProfileId"
helpText={translate('MetadataProfileIdHelpText')}
{...metadataProfileId}
includeNone={true}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('ReadarrTags')}
</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('TagsHelpText')}
{...tags}
onChange={onInputChange}
/> />
</FormLabel> </FormGroup>
</FieldSet>
<FormInputGroup
type={inputTypes.SELECT}
name="shouldMonitor"
values={monitorOptions}
helpText={translate('ShouldMonitorHelpText')}
{...shouldMonitor}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('ShouldMonitorExisting')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="shouldMonitorExisting"
helpText={translate('ShouldMonitorExistingHelpText')}
{...shouldMonitorExisting}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('SearchForNewItems')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="shouldSearch"
helpText={translate('ShouldSearchHelpText')}
{...shouldSearch}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('RootFolder')}
</FormLabel>
<FormInputGroup
type={inputTypes.ROOT_FOLDER_SELECT}
name="rootFolderPath"
helpText={translate('RootFolderPathHelpText')}
{...rootFolderPath}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('QualityProfile')}
</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
helpText={translate('QualityProfileIdHelpText')}
{...qualityProfileId}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup className={showMetadataProfile ? undefined : styles.hideMetadataProfile}>
<FormLabel>
{translate('MetadataProfile')}
</FormLabel>
<FormInputGroup
type={inputTypes.METADATA_PROFILE_SELECT}
name="metadataProfileId"
helpText={translate('MetadataProfileIdHelpText')}
{...metadataProfileId}
includeNone={true}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('ReadarrTags')}
</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('TagsHelpText')}
{...tags}
onChange={onInputChange}
/>
</FormGroup>
{ {
!!fields && !!fields.length && !!fields && !!fields.length &&
<div> <FieldSet legend={translate('ImportListSpecificSettings')} >
{ {
fields.map((field) => { fields.map((field) => {
return ( return (
@ -271,7 +304,7 @@ function EditImportListModalContent(props) {
); );
}) })
} }
</div> </FieldSet>
} }
</Form> </Form>

@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import AuthorMetadataProfilePopoverContent from 'AddAuthor/AuthorMetadataProfilePopoverContent'; import AuthorMetadataProfilePopoverContent from 'AddAuthor/AuthorMetadataProfilePopoverContent';
import AuthorMonitoringOptionsPopoverContent from 'AddAuthor/AuthorMonitoringOptionsPopoverContent'; import AuthorMonitoringOptionsPopoverContent from 'AddAuthor/AuthorMonitoringOptionsPopoverContent';
import AuthorMonitorNewItemsOptionsPopoverContent from 'AddAuthor/AuthorMonitorNewItemsOptionsPopoverContent';
import Form from 'Components/Form/Form'; import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup'; import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup'; import FormInputGroup from 'Components/Form/FormInputGroup';
@ -43,6 +44,7 @@ function EditRootFolderModalContent(props) {
defaultQualityProfileId, defaultQualityProfileId,
defaultMetadataProfileId, defaultMetadataProfileId,
defaultMonitorOption, defaultMonitorOption,
defaultNewItemMonitorOption,
defaultTags, defaultTags,
isCalibreLibrary, isCalibreLibrary,
host, host,
@ -295,7 +297,7 @@ function EditRootFolderModalContent(props) {
<FormGroup> <FormGroup>
<FormLabel> <FormLabel>
Monitor {translate('Monitor')}
<Popover <Popover
anchor={ anchor={
@ -320,6 +322,31 @@ function EditRootFolderModalContent(props) {
</FormGroup> </FormGroup>
<FormGroup>
<FormLabel>
{translate('MonitorNewItems')}
<Popover
anchor={
<Icon
className={styles.labelIcon}
name={icons.INFO}
/>
}
title={translate('MonitorNewItems')}
body={<AuthorMonitorNewItemsOptionsPopoverContent />}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup
type={inputTypes.MONITOR_NEW_ITEMS_SELECT}
name="defaultNewItemMonitorOption"
{...defaultNewItemMonitorOption}
onChange={onInputChange}
helpText={translate('MonitorNewItemsHelpText')}
/>
</FormGroup>
<FormGroup> <FormGroup>
<FormLabel> <FormLabel>
{translate('QualityProfile')} {translate('QualityProfile')}

@ -4,7 +4,6 @@ import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest'; import createAjaxRequest from 'Utilities/createAjaxRequest';
import { filterPredicates, filters } from './authorActions'; import { filterPredicates, filters } from './authorActions';
import { set } from './baseActions'; import { set } from './baseActions';
import { fetchBooks } from './bookActions';
import createHandleActions from './Creators/createHandleActions'; import createHandleActions from './Creators/createHandleActions';
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer'; import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
@ -97,7 +96,8 @@ export const actionHandlers = handleThunks({
const { const {
authorIds, authorIds,
monitored, monitored,
monitor monitor,
monitorNewItems
} = payload; } = payload;
const authors = []; const authors = [];
@ -122,14 +122,13 @@ export const actionHandlers = handleThunks({
method: 'POST', method: 'POST',
data: JSON.stringify({ data: JSON.stringify({
authors, authors,
monitoringOptions: { monitor } monitoringOptions: { monitor },
monitorNewItems
}), }),
dataType: 'json' dataType: 'json'
}).request; }).request;
promise.done((data) => { promise.done((data) => {
dispatch(fetchBooks());
dispatch(set({ dispatch(set({
section, section,
isSaving: false, isSaving: false,

@ -3,6 +3,7 @@ function getNewAuthor(author, payload) {
const { const {
rootFolderPath, rootFolderPath,
monitor, monitor,
monitorNewItems,
qualityProfileId, qualityProfileId,
metadataProfileId, metadataProfileId,
tags, tags,
@ -16,6 +17,7 @@ function getNewAuthor(author, payload) {
author.addOptions = addOptions; author.addOptions = addOptions;
author.monitored = true; author.monitored = true;
author.monitorNewItems = monitorNewItems;
author.qualityProfileId = qualityProfileId; author.qualityProfileId = qualityProfileId;
author.metadataProfileId = metadataProfileId; author.metadataProfileId = metadataProfileId;
author.rootFolderPath = rootFolderPath; author.rootFolderPath = rootFolderPath;

@ -0,0 +1,7 @@
const monitorNewItemsOptions = [
{ key: 'all', value: 'All Books' },
{ key: 'none', value: 'None' },
{ key: 'new', value: 'New' }
];
export default monitorNewItemsOptions;

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FizzWare.NBuilder;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Core.Books;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.BookTests
{
[TestFixture]
public class MonitorNewBookServiceFixture : CoreTest<MonitorNewBookService>
{
private List<Book> _books;
[SetUp]
public void Setup()
{
_books = Builder<Book>.CreateListOfSize(4)
.All()
.With(e => e.Monitored = true)
.With(e => e.ReleaseDate = DateTime.UtcNow.AddDays(-7))
//Future
.TheFirst(1)
.With(e => e.ReleaseDate = DateTime.UtcNow.AddDays(7))
//Future/TBA
.TheNext(1)
.With(e => e.ReleaseDate = null)
.Build()
.ToList();
}
[Test]
public void should_monitor_with_all()
{
foreach (var book in _books)
{
Subject.ShouldMonitorNewBook(book, _books, NewItemMonitorTypes.All).Should().BeTrue();
}
}
[Test]
public void should_not_monitor_with_none()
{
foreach (var book in _books)
{
Subject.ShouldMonitorNewBook(book, _books, NewItemMonitorTypes.None).Should().BeFalse();
}
}
[Test]
public void should_only_monitor_new_with_new()
{
Subject.ShouldMonitorNewBook(_books[0], _books, NewItemMonitorTypes.New).Should().BeTrue();
foreach (var book in _books.Skip(1))
{
Subject.ShouldMonitorNewBook(book, _books, NewItemMonitorTypes.New).Should().BeFalse();
}
}
}
}

@ -62,7 +62,7 @@ namespace NzbDrone.Core.Test.MusicTests
Mocker.GetMock<IMetadataProfileService>() Mocker.GetMock<IMetadataProfileService>()
.Setup(s => s.FilterBooks(It.IsAny<Author>(), It.IsAny<int>())) .Setup(s => s.FilterBooks(It.IsAny<Author>(), It.IsAny<int>()))
.Returns(_books); .Returns(_remoteBooks);
Mocker.GetMock<IProvideAuthorInfo>() Mocker.GetMock<IProvideAuthorInfo>()
.Setup(s => s.GetAuthorAndBooks(It.IsAny<string>(), It.IsAny<double>())) .Setup(s => s.GetAuthorAndBooks(It.IsAny<string>(), It.IsAny<double>()))
@ -83,6 +83,10 @@ namespace NzbDrone.Core.Test.MusicTests
Mocker.GetMock<IRootFolderService>() Mocker.GetMock<IRootFolderService>()
.Setup(x => x.All()) .Setup(x => x.All())
.Returns(new List<RootFolder>()); .Returns(new List<RootFolder>());
Mocker.GetMock<IMonitorNewBookService>()
.Setup(x => x.ShouldMonitorNewBook(It.IsAny<Book>(), It.IsAny<List<Book>>(), It.IsAny<NewItemMonitorTypes>()))
.Returns(true);
} }
private void GivenNewAuthorInfo(Author author) private void GivenNewAuthorInfo(Author author)
@ -151,6 +155,29 @@ namespace NzbDrone.Core.Test.MusicTests
VerifyEventPublished<AuthorRefreshCompleteEvent>(); VerifyEventPublished<AuthorRefreshCompleteEvent>();
} }
[Test]
public void should_call_new_book_monitor_service_when_adding_book()
{
var newBook = Builder<Book>.CreateNew()
.With(x => x.Id = 0)
.With(x => x.ForeignBookId = "3")
.Build();
_remoteBooks.Add(newBook);
var newAuthorInfo = _author.JsonClone();
newAuthorInfo.Metadata = _author.Metadata.Value.JsonClone();
newAuthorInfo.Books = _remoteBooks;
GivenNewAuthorInfo(newAuthorInfo);
GivenBooksForRefresh(_books);
AllowAuthorUpdate();
Subject.Execute(new RefreshAuthorCommand(_author.Id));
Mocker.GetMock<IMonitorNewBookService>()
.Verify(x => x.ShouldMonitorNewBook(newBook, _books, _author.MonitorNewItems), Times.Once());
}
[Test] [Test]
public void should_log_error_and_delete_if_musicbrainz_id_not_found_and_author_has_no_files() public void should_log_error_and_delete_if_musicbrainz_id_not_found_and_author_has_no_files()
{ {

@ -20,6 +20,7 @@ namespace NzbDrone.Core.Books
public int AuthorMetadataId { get; set; } public int AuthorMetadataId { get; set; }
public string CleanName { get; set; } public string CleanName { get; set; }
public bool Monitored { get; set; } public bool Monitored { get; set; }
public NewItemMonitorTypes MonitorNewItems { get; set; }
public DateTime? LastInfoSync { get; set; } public DateTime? LastInfoSync { get; set; }
public string Path { get; set; } public string Path { get; set; }
public string RootFolderPath { get; set; } public string RootFolderPath { get; set; }
@ -70,6 +71,7 @@ namespace NzbDrone.Core.Books
Id = other.Id; Id = other.Id;
AuthorMetadataId = other.AuthorMetadataId; AuthorMetadataId = other.AuthorMetadataId;
Monitored = other.Monitored; Monitored = other.Monitored;
MonitorNewItems = other.MonitorNewItems;
LastInfoSync = other.LastInfoSync; LastInfoSync = other.LastInfoSync;
Path = other.Path; Path = other.Path;
RootFolderPath = other.RootFolderPath; RootFolderPath = other.RootFolderPath;
@ -93,6 +95,7 @@ namespace NzbDrone.Core.Books
AddOptions = other.AddOptions; AddOptions = other.AddOptions;
RootFolderPath = other.RootFolderPath; RootFolderPath = other.RootFolderPath;
Monitored = other.Monitored; Monitored = other.Monitored;
MonitorNewItems = other.MonitorNewItems;
} }
} }
} }

@ -0,0 +1,14 @@
namespace NzbDrone.Core.Books
{
public enum MonitorTypes
{
All,
Future,
Missing,
Existing,
Latest,
First,
None,
Unknown
}
}

@ -14,16 +14,4 @@ namespace NzbDrone.Core.Books
public List<string> BooksToMonitor { get; set; } public List<string> BooksToMonitor { get; set; }
public bool Monitored { get; set; } public bool Monitored { get; set; }
} }
public enum MonitorTypes
{
All,
Future,
Missing,
Existing,
Latest,
First,
None,
Unknown
}
} }

@ -0,0 +1,9 @@
namespace NzbDrone.Core.Books
{
public enum NewItemMonitorTypes
{
All,
None,
New
}
}

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NLog;
namespace NzbDrone.Core.Books
{
public interface IMonitorNewBookService
{
bool ShouldMonitorNewBook(Book addedBook, List<Book> existingBooks, NewItemMonitorTypes author);
}
public class MonitorNewBookService : IMonitorNewBookService
{
private readonly Logger _logger;
public MonitorNewBookService(Logger logger)
{
_logger = logger;
}
public bool ShouldMonitorNewBook(Book addedBook, List<Book> existingBooks, NewItemMonitorTypes monitorNewItems)
{
if (monitorNewItems == NewItemMonitorTypes.None)
{
return false;
}
if (monitorNewItems == NewItemMonitorTypes.All)
{
return true;
}
if (monitorNewItems == NewItemMonitorTypes.New)
{
var newest = existingBooks.OrderByDescending(x => x.ReleaseDate ?? DateTime.MinValue).FirstOrDefault()?.ReleaseDate ?? DateTime.MinValue;
return (addedBook.ReleaseDate ?? DateTime.MinValue) >= newest;
}
throw new NotImplementedException($"Unknown new item monitor type {monitorNewItems}");
}
}
}

@ -42,6 +42,7 @@ namespace NzbDrone.Core.Books
private readonly IHistoryService _historyService; private readonly IHistoryService _historyService;
private readonly IRootFolderService _rootFolderService; private readonly IRootFolderService _rootFolderService;
private readonly ICheckIfAuthorShouldBeRefreshed _checkIfAuthorShouldBeRefreshed; private readonly ICheckIfAuthorShouldBeRefreshed _checkIfAuthorShouldBeRefreshed;
private readonly IMonitorNewBookService _monitorNewBookService;
private readonly IConfigService _configService; private readonly IConfigService _configService;
private readonly IImportListExclusionService _importListExclusionService; private readonly IImportListExclusionService _importListExclusionService;
private readonly Logger _logger; private readonly Logger _logger;
@ -59,6 +60,7 @@ namespace NzbDrone.Core.Books
IHistoryService historyService, IHistoryService historyService,
IRootFolderService rootFolderService, IRootFolderService rootFolderService,
ICheckIfAuthorShouldBeRefreshed checkIfAuthorShouldBeRefreshed, ICheckIfAuthorShouldBeRefreshed checkIfAuthorShouldBeRefreshed,
IMonitorNewBookService monitorNewBookService,
IConfigService configService, IConfigService configService,
IImportListExclusionService importListExclusionService, IImportListExclusionService importListExclusionService,
Logger logger) Logger logger)
@ -76,6 +78,7 @@ namespace NzbDrone.Core.Books
_historyService = historyService; _historyService = historyService;
_rootFolderService = rootFolderService; _rootFolderService = rootFolderService;
_checkIfAuthorShouldBeRefreshed = checkIfAuthorShouldBeRefreshed; _checkIfAuthorShouldBeRefreshed = checkIfAuthorShouldBeRefreshed;
_monitorNewBookService = monitorNewBookService;
_configService = configService; _configService = configService;
_importListExclusionService = importListExclusionService; _importListExclusionService = importListExclusionService;
_logger = logger; _logger = logger;
@ -264,6 +267,14 @@ namespace NzbDrone.Core.Books
remote.UseDbFieldsFrom(local); remote.UseDbFieldsFrom(local);
} }
protected override void ProcessChildren(Author entity, SortedChildren children)
{
foreach (var book in children.Added)
{
book.Monitored = _monitorNewBookService.ShouldMonitorNewBook(book, children.UpToDate, entity.MonitorNewItems);
}
}
protected override void AddChildren(List<Book> children) protected override void AddChildren(List<Book> children)
{ {
_bookService.InsertMany(children); _bookService.InsertMany(children);

@ -93,6 +93,11 @@ namespace NzbDrone.Core.Books
protected abstract void PrepareNewChild(TChild child, TEntity entity); protected abstract void PrepareNewChild(TChild child, TEntity entity);
protected abstract void PrepareExistingChild(TChild local, TChild remote, TEntity entity); protected abstract void PrepareExistingChild(TChild local, TChild remote, TEntity entity);
protected virtual void ProcessChildren(TEntity entity, SortedChildren children)
{
}
protected abstract void AddChildren(List<TChild> children); protected abstract void AddChildren(List<TChild> children);
protected abstract bool RefreshChildren(SortedChildren localChildren, List<TChild> remoteChildren, Author remoteData, bool forceChildRefresh, bool forceUpdateFileTags, DateTime? lastUpdate); protected abstract bool RefreshChildren(SortedChildren localChildren, List<TChild> remoteChildren, Author remoteData, bool forceChildRefresh, bool forceUpdateFileTags, DateTime? lastUpdate);
@ -277,6 +282,8 @@ namespace NzbDrone.Core.Books
sortedChildren.Deleted.Count); sortedChildren.Deleted.Count);
} }
ProcessChildren(entity, sortedChildren);
// Add in the new children (we have checked that foreign IDs don't clash) // Add in the new children (we have checked that foreign IDs don't clash)
AddChildren(sortedChildren.Added); AddChildren(sortedChildren.Added);

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

@ -1,3 +1,4 @@
using NzbDrone.Core.Books;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
namespace NzbDrone.Core.ImportLists namespace NzbDrone.Core.ImportLists
@ -8,6 +9,7 @@ namespace NzbDrone.Core.ImportLists
public ImportListMonitorType ShouldMonitor { get; set; } public ImportListMonitorType ShouldMonitor { get; set; }
public bool ShouldMonitorExisting { get; set; } public bool ShouldMonitorExisting { get; set; }
public bool ShouldSearch { get; set; } public bool ShouldSearch { get; set; }
public NewItemMonitorTypes MonitorNewItems { get; set; }
public int ProfileId { get; set; } public int ProfileId { get; set; }
public int MetadataProfileId { get; set; } public int MetadataProfileId { get; set; }
public string RootFolderPath { get; set; } public string RootFolderPath { get; set; }

@ -227,6 +227,7 @@ namespace NzbDrone.Core.ImportLists
var toAddAuthor = new Author var toAddAuthor = new Author
{ {
Monitored = monitored, Monitored = monitored,
MonitorNewItems = importList.MonitorNewItems,
RootFolderPath = importList.RootFolderPath, RootFolderPath = importList.RootFolderPath,
QualityProfileId = importList.ProfileId, QualityProfileId = importList.ProfileId,
MetadataProfileId = importList.MetadataProfileId, MetadataProfileId = importList.MetadataProfileId,

@ -5,6 +5,7 @@
"About": "About", "About": "About",
"Absolute": "Absolute", "Absolute": "Absolute",
"Actions": "Actions", "Actions": "Actions",
"AddedAuthorSettings": "Added Author Settings",
"AddImportListExclusionHelpText": "Prevent book from being added to Readarr by Import Lists or Author Refresh", "AddImportListExclusionHelpText": "Prevent book from being added to Readarr by Import Lists or Author Refresh",
"AddingTag": "Adding tag", "AddingTag": "Adding tag",
"AddListExclusion": "Add List Exclusion", "AddListExclusion": "Add List Exclusion",
@ -140,7 +141,7 @@
"Dates": "Dates", "Dates": "Dates",
"DBMigration": "DB Migration", "DBMigration": "DB Migration",
"DefaultMetadataProfileIdHelpText": "Default Metadata Profile for authors detected in this folder", "DefaultMetadataProfileIdHelpText": "Default Metadata Profile for authors detected in this folder",
"DefaultMonitorOptionHelpText": "Default Monitoring Options for books by authors detected in this folder", "DefaultMonitorOptionHelpText": "Which books should be monitored on initial add for authors detected in this folder",
"DefaultQualityProfileIdHelpText": "Default Quality Profile for authors detected in this folder", "DefaultQualityProfileIdHelpText": "Default Quality Profile for authors detected in this folder",
"DefaultReadarrTags": "Default Readarr Tags", "DefaultReadarrTags": "Default Readarr Tags",
"DefaultTagsHelpText": "Default Readarr Tags for authors detected in this folder", "DefaultTagsHelpText": "Default Readarr Tags for authors detected in this folder",
@ -291,7 +292,8 @@
"Importing": "Importing", "Importing": "Importing",
"ImportListExclusions": "Import List Exclusions", "ImportListExclusions": "Import List Exclusions",
"ImportLists": "Import Lists", "ImportLists": "Import Lists",
"ImportListSettings": "Import List Settings", "ImportListSettings": "General Import List Settings",
"ImportListSpecificSettings": "Import List Specific Settings",
"IncludeHealthWarningsHelpText": "Include Health Warnings", "IncludeHealthWarningsHelpText": "Include Health Warnings",
"IncludePreferredWhenRenaming": "Include Preferred when Renaming", "IncludePreferredWhenRenaming": "Include Preferred when Renaming",
"IncludeUnknownAuthorItemsHelpText": "Show items without a author in the queue, this could include removed authors, movies or anything else in Readarr's category", "IncludeUnknownAuthorItemsHelpText": "Show items without a author in the queue, this could include removed authors, movies or anything else in Readarr's category",
@ -381,12 +383,16 @@
"Mode": "Mode", "Mode": "Mode",
"MonitorAuthor": "Monitor Author", "MonitorAuthor": "Monitor Author",
"MonitorBook": "Monitor Book", "MonitorBook": "Monitor Book",
"MonitorBookExistingOnlyWarning": "This is a one off adjustment of the monitored setting for each book. Use the option under Author/Edit to control what happens for newly added books",
"Monitored": "Monitored", "Monitored": "Monitored",
"MonitoredAuthorIsMonitored": "Author is monitored", "MonitoredAuthorIsMonitored": "Author is monitored",
"MonitoredAuthorIsUnmonitored": "Author is unmonitored", "MonitoredAuthorIsUnmonitored": "Author is unmonitored",
"MonitoredHelpText": "Readarr will search for and download book", "MonitoredHelpText": "Readarr will search for and download book",
"Monitoring": "Monitoring", "Monitoring": "Monitoring",
"MonitoringOptions": "Monitoring Options", "MonitoringOptions": "Monitoring Options",
"MonitoringOptionsHelpText": "Which books should be monitored after the author is added (one-time adjustment)",
"MonitorNewItems": "Monitor New Books",
"MonitorNewItemsHelpText": "Which new books should be monitored",
"MonoVersion": "Mono Version", "MonoVersion": "Mono Version",
"MoreInfo": "More Info", "MoreInfo": "More Info",
"MusicBrainzAuthorID": "MusicBrainz Author ID", "MusicBrainzAuthorID": "MusicBrainz Author ID",
@ -404,6 +410,7 @@
"NamingSettings": "Naming Settings", "NamingSettings": "Naming Settings",
"NETCore": ".NET Core", "NETCore": ".NET Core",
"New": "New", "New": "New",
"NewBooks": "New Books",
"NoBackupsAreAvailable": "No backups are available", "NoBackupsAreAvailable": "No backups are available",
"NoHistory": "No history.", "NoHistory": "No history.",
"NoHistoryBlocklist": "No history blocklist", "NoHistoryBlocklist": "No history blocklist",

@ -328,6 +328,7 @@ namespace NzbDrone.Core.MediaFiles.BookImport
author.MetadataProfileId = rootFolder.DefaultMetadataProfileId; author.MetadataProfileId = rootFolder.DefaultMetadataProfileId;
author.QualityProfileId = rootFolder.DefaultQualityProfileId; author.QualityProfileId = rootFolder.DefaultQualityProfileId;
author.Monitored = rootFolder.DefaultMonitorOption != MonitorTypes.None; author.Monitored = rootFolder.DefaultMonitorOption != MonitorTypes.None;
author.MonitorNewItems = rootFolder.DefaultNewItemMonitorOption;
author.Tags = rootFolder.DefaultTags; author.Tags = rootFolder.DefaultTags;
author.AddOptions = new AddAuthorOptions author.AddOptions = new AddAuthorOptions
{ {

@ -12,6 +12,7 @@ namespace NzbDrone.Core.RootFolders
public int DefaultMetadataProfileId { get; set; } public int DefaultMetadataProfileId { get; set; }
public int DefaultQualityProfileId { get; set; } public int DefaultQualityProfileId { get; set; }
public MonitorTypes DefaultMonitorOption { get; set; } public MonitorTypes DefaultMonitorOption { get; set; }
public NewItemMonitorTypes DefaultNewItemMonitorOption { get; set; }
public HashSet<int> DefaultTags { get; set; } public HashSet<int> DefaultTags { get; set; }
public bool IsCalibreLibrary { get; set; } public bool IsCalibreLibrary { get; set; }
public CalibreSettings CalibreSettings { get; set; } public CalibreSettings CalibreSettings { get; set; }

@ -18,7 +18,6 @@ using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths; using NzbDrone.Core.Validation.Paths;
using NzbDrone.Http.REST.Attributes; using NzbDrone.Http.REST.Attributes;
using NzbDrone.SignalR; using NzbDrone.SignalR;
using Readarr.Api.V1.Books;
using Readarr.Http; using Readarr.Http;
using Readarr.Http.Extensions; using Readarr.Http.Extensions;
using Readarr.Http.REST; using Readarr.Http.REST;

@ -34,6 +34,11 @@ namespace Readarr.Api.V1.Author
author.Monitored = resource.Monitored.Value; author.Monitored = resource.Monitored.Value;
} }
if (resource.MonitorNewItems.HasValue)
{
author.MonitorNewItems = resource.MonitorNewItems.Value;
}
if (resource.QualityProfileId.HasValue) if (resource.QualityProfileId.HasValue)
{ {
author.QualityProfileId = resource.QualityProfileId.Value; author.QualityProfileId = resource.QualityProfileId.Value;

@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using NzbDrone.Core.Books;
namespace Readarr.Api.V1.Author namespace Readarr.Api.V1.Author
{ {
@ -6,6 +7,7 @@ namespace Readarr.Api.V1.Author
{ {
public List<int> AuthorIds { get; set; } public List<int> AuthorIds { get; set; }
public bool? Monitored { get; set; } public bool? Monitored { get; set; }
public NewItemMonitorTypes? MonitorNewItems { get; set; }
public int? QualityProfileId { get; set; } public int? QualityProfileId { get; set; }
public int? MetadataProfileId { get; set; } public int? MetadataProfileId { get; set; }
public string RootFolderPath { get; set; } public string RootFolderPath { get; set; }

@ -5,7 +5,6 @@ using Newtonsoft.Json;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.Books; using NzbDrone.Core.Books;
using NzbDrone.Core.MediaCover; using NzbDrone.Core.MediaCover;
using Readarr.Api.V1.Books;
using Readarr.Http.REST; using Readarr.Http.REST;
namespace Readarr.Api.V1.Author namespace Readarr.Api.V1.Author
@ -43,6 +42,7 @@ namespace Readarr.Api.V1.Author
//Editing Only //Editing Only
public bool Monitored { get; set; } public bool Monitored { get; set; }
public NewItemMonitorTypes MonitorNewItems { get; set; }
public string RootFolderPath { get; set; } public string RootFolderPath { get; set; }
public List<string> Genres { get; set; } public List<string> Genres { get; set; }
@ -91,6 +91,7 @@ namespace Readarr.Api.V1.Author
Links = model.Metadata.Value.Links, Links = model.Metadata.Value.Links,
Monitored = model.Monitored, Monitored = model.Monitored,
MonitorNewItems = model.MonitorNewItems,
CleanName = model.CleanName, CleanName = model.CleanName,
ForeignAuthorId = model.Metadata.Value.ForeignAuthorId, ForeignAuthorId = model.Metadata.Value.ForeignAuthorId,
@ -141,6 +142,7 @@ namespace Readarr.Api.V1.Author
MetadataProfileId = resource.MetadataProfileId, MetadataProfileId = resource.MetadataProfileId,
Monitored = resource.Monitored, Monitored = resource.Monitored,
MonitorNewItems = resource.MonitorNewItems,
CleanName = resource.CleanName, CleanName = resource.CleanName,
RootFolderPath = resource.RootFolderPath, RootFolderPath = resource.RootFolderPath,

@ -18,7 +18,7 @@ namespace Readarr.Api.V1.Bookshelf
} }
[HttpPost] [HttpPost]
public ActionResult<object> UpdateAll([FromBody] BookshelfResource request) public IActionResult UpdateAll([FromBody] BookshelfResource request)
{ {
//Read from request //Read from request
var authorToUpdate = _authorService.GetAuthors(request.Authors.Select(s => s.Id)); var authorToUpdate = _authorService.GetAuthors(request.Authors.Select(s => s.Id));
@ -37,10 +37,15 @@ namespace Readarr.Api.V1.Bookshelf
author.Monitored = false; author.Monitored = false;
} }
if (request.MonitorNewItems.HasValue)
{
author.MonitorNewItems = request.MonitorNewItems.Value;
}
_bookMonitoredService.SetBookMonitoredStatus(author, request.MonitoringOptions); _bookMonitoredService.SetBookMonitoredStatus(author, request.MonitoringOptions);
} }
return Accepted(new object()); return Accepted(request);
} }
} }
} }

@ -7,5 +7,6 @@ namespace Readarr.Api.V1.Bookshelf
{ {
public List<BookshelfAuthorResource> Authors { get; set; } public List<BookshelfAuthorResource> Authors { get; set; }
public MonitoringOptions MonitoringOptions { get; set; } public MonitoringOptions MonitoringOptions { get; set; }
public NewItemMonitorTypes? MonitorNewItems { get; set; }
} }
} }

@ -1,3 +1,4 @@
using NzbDrone.Core.Books;
using NzbDrone.Core.ImportLists; using NzbDrone.Core.ImportLists;
namespace Readarr.Api.V1.ImportLists namespace Readarr.Api.V1.ImportLists
@ -9,6 +10,7 @@ namespace Readarr.Api.V1.ImportLists
public bool ShouldMonitorExisting { get; set; } public bool ShouldMonitorExisting { get; set; }
public bool ShouldSearch { get; set; } public bool ShouldSearch { get; set; }
public string RootFolderPath { get; set; } public string RootFolderPath { get; set; }
public NewItemMonitorTypes MonitorNewItems { get; set; }
public int QualityProfileId { get; set; } public int QualityProfileId { get; set; }
public int MetadataProfileId { get; set; } public int MetadataProfileId { get; set; }
public ImportListType ListType { get; set; } public ImportListType ListType { get; set; }
@ -31,6 +33,7 @@ namespace Readarr.Api.V1.ImportLists
resource.ShouldMonitorExisting = definition.ShouldMonitorExisting; resource.ShouldMonitorExisting = definition.ShouldMonitorExisting;
resource.ShouldSearch = definition.ShouldSearch; resource.ShouldSearch = definition.ShouldSearch;
resource.RootFolderPath = definition.RootFolderPath; resource.RootFolderPath = definition.RootFolderPath;
resource.MonitorNewItems = definition.MonitorNewItems;
resource.QualityProfileId = definition.ProfileId; resource.QualityProfileId = definition.ProfileId;
resource.MetadataProfileId = definition.MetadataProfileId; resource.MetadataProfileId = definition.MetadataProfileId;
resource.ListType = definition.ListType; resource.ListType = definition.ListType;
@ -53,6 +56,7 @@ namespace Readarr.Api.V1.ImportLists
definition.ShouldMonitorExisting = resource.ShouldMonitorExisting; definition.ShouldMonitorExisting = resource.ShouldMonitorExisting;
definition.ShouldSearch = resource.ShouldSearch; definition.ShouldSearch = resource.ShouldSearch;
definition.RootFolderPath = resource.RootFolderPath; definition.RootFolderPath = resource.RootFolderPath;
definition.MonitorNewItems = resource.MonitorNewItems;
definition.ProfileId = resource.QualityProfileId; definition.ProfileId = resource.QualityProfileId;
definition.MetadataProfileId = resource.MetadataProfileId; definition.MetadataProfileId = resource.MetadataProfileId;
definition.ListType = resource.ListType; definition.ListType = resource.ListType;

@ -15,6 +15,7 @@ namespace Readarr.Api.V1.RootFolders
public int DefaultMetadataProfileId { get; set; } public int DefaultMetadataProfileId { get; set; }
public int DefaultQualityProfileId { get; set; } public int DefaultQualityProfileId { get; set; }
public MonitorTypes DefaultMonitorOption { get; set; } public MonitorTypes DefaultMonitorOption { get; set; }
public NewItemMonitorTypes DefaultNewItemMonitorOption { get; set; }
public HashSet<int> DefaultTags { get; set; } public HashSet<int> DefaultTags { get; set; }
public bool IsCalibreLibrary { get; set; } public bool IsCalibreLibrary { get; set; }
public string Host { get; set; } public string Host { get; set; }
@ -50,6 +51,7 @@ namespace Readarr.Api.V1.RootFolders
DefaultMetadataProfileId = model.DefaultMetadataProfileId, DefaultMetadataProfileId = model.DefaultMetadataProfileId,
DefaultQualityProfileId = model.DefaultQualityProfileId, DefaultQualityProfileId = model.DefaultQualityProfileId,
DefaultMonitorOption = model.DefaultMonitorOption, DefaultMonitorOption = model.DefaultMonitorOption,
DefaultNewItemMonitorOption = model.DefaultNewItemMonitorOption,
DefaultTags = model.DefaultTags, DefaultTags = model.DefaultTags,
IsCalibreLibrary = model.IsCalibreLibrary, IsCalibreLibrary = model.IsCalibreLibrary,
Host = model.CalibreSettings?.Host, Host = model.CalibreSettings?.Host,
@ -105,6 +107,7 @@ namespace Readarr.Api.V1.RootFolders
DefaultMetadataProfileId = resource.DefaultMetadataProfileId, DefaultMetadataProfileId = resource.DefaultMetadataProfileId,
DefaultQualityProfileId = resource.DefaultQualityProfileId, DefaultQualityProfileId = resource.DefaultQualityProfileId,
DefaultMonitorOption = resource.DefaultMonitorOption, DefaultMonitorOption = resource.DefaultMonitorOption,
DefaultNewItemMonitorOption = resource.DefaultNewItemMonitorOption,
DefaultTags = resource.DefaultTags, DefaultTags = resource.DefaultTags,
IsCalibreLibrary = resource.IsCalibreLibrary, IsCalibreLibrary = resource.IsCalibreLibrary,
CalibreSettings = cs CalibreSettings = cs

Loading…
Cancel
Save