parent
49eb3ab2cf
commit
62f6c855bc
|
@ -21,6 +21,7 @@ import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementCo
|
||||||
import Profiles from 'Settings/Profiles/Profiles';
|
import Profiles from 'Settings/Profiles/Profiles';
|
||||||
import Quality from 'Settings/Quality/Quality';
|
import Quality from 'Settings/Quality/Quality';
|
||||||
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
|
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
|
||||||
|
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
||||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||||
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
||||||
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
|
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
|
||||||
|
@ -170,6 +171,11 @@ function AppRoutes(props) {
|
||||||
component={DownloadClientSettingsConnector}
|
component={DownloadClientSettingsConnector}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/settings/importlists"
|
||||||
|
component={ImportListSettingsConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/settings/connect"
|
path="/settings/connect"
|
||||||
component={NotificationSettings}
|
component={NotificationSettings}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
||||||
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
||||||
import { fetchSeries } from 'Store/Actions/seriesActions';
|
import { fetchSeries } from 'Store/Actions/seriesActions';
|
||||||
import { fetchTags } from 'Store/Actions/tagActions';
|
import { fetchTags } from 'Store/Actions/tagActions';
|
||||||
import { fetchQualityProfiles, fetchLanguageProfiles, fetchUISettings } from 'Store/Actions/settingsActions';
|
import { fetchQualityProfiles, fetchLanguageProfiles, fetchImportLists, fetchUISettings } from 'Store/Actions/settingsActions';
|
||||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||||
import ErrorPage from './ErrorPage';
|
import ErrorPage from './ErrorPage';
|
||||||
import LoadingPage from './LoadingPage';
|
import LoadingPage from './LoadingPage';
|
||||||
|
@ -49,6 +49,7 @@ const selectIsPopulated = createSelector(
|
||||||
(state) => state.settings.ui.isPopulated,
|
(state) => state.settings.ui.isPopulated,
|
||||||
(state) => state.settings.qualityProfiles.isPopulated,
|
(state) => state.settings.qualityProfiles.isPopulated,
|
||||||
(state) => state.settings.languageProfiles.isPopulated,
|
(state) => state.settings.languageProfiles.isPopulated,
|
||||||
|
(state) => state.settings.importLists.isPopulated,
|
||||||
(state) => state.system.status.isPopulated,
|
(state) => state.system.status.isPopulated,
|
||||||
(
|
(
|
||||||
seriesIsPopulated,
|
seriesIsPopulated,
|
||||||
|
@ -57,6 +58,7 @@ const selectIsPopulated = createSelector(
|
||||||
uiSettingsIsPopulated,
|
uiSettingsIsPopulated,
|
||||||
qualityProfilesIsPopulated,
|
qualityProfilesIsPopulated,
|
||||||
languageProfilesIsPopulated,
|
languageProfilesIsPopulated,
|
||||||
|
importListsIsPopulated,
|
||||||
systemStatusIsPopulated
|
systemStatusIsPopulated
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
|
@ -66,6 +68,7 @@ const selectIsPopulated = createSelector(
|
||||||
uiSettingsIsPopulated &&
|
uiSettingsIsPopulated &&
|
||||||
qualityProfilesIsPopulated &&
|
qualityProfilesIsPopulated &&
|
||||||
languageProfilesIsPopulated &&
|
languageProfilesIsPopulated &&
|
||||||
|
importListsIsPopulated &&
|
||||||
systemStatusIsPopulated
|
systemStatusIsPopulated
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -78,6 +81,7 @@ const selectErrors = createSelector(
|
||||||
(state) => state.settings.ui.error,
|
(state) => state.settings.ui.error,
|
||||||
(state) => state.settings.qualityProfiles.error,
|
(state) => state.settings.qualityProfiles.error,
|
||||||
(state) => state.settings.languageProfiles.error,
|
(state) => state.settings.languageProfiles.error,
|
||||||
|
(state) => state.settings.importLists.error,
|
||||||
(state) => state.system.status.error,
|
(state) => state.system.status.error,
|
||||||
(
|
(
|
||||||
seriesError,
|
seriesError,
|
||||||
|
@ -86,6 +90,7 @@ const selectErrors = createSelector(
|
||||||
uiSettingsError,
|
uiSettingsError,
|
||||||
qualityProfilesError,
|
qualityProfilesError,
|
||||||
languageProfilesError,
|
languageProfilesError,
|
||||||
|
importListsError,
|
||||||
systemStatusError
|
systemStatusError
|
||||||
) => {
|
) => {
|
||||||
const hasError = !!(
|
const hasError = !!(
|
||||||
|
@ -95,6 +100,7 @@ const selectErrors = createSelector(
|
||||||
uiSettingsError ||
|
uiSettingsError ||
|
||||||
qualityProfilesError ||
|
qualityProfilesError ||
|
||||||
languageProfilesError ||
|
languageProfilesError ||
|
||||||
|
importListsError ||
|
||||||
systemStatusError
|
systemStatusError
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -106,6 +112,7 @@ const selectErrors = createSelector(
|
||||||
uiSettingsError,
|
uiSettingsError,
|
||||||
qualityProfilesError,
|
qualityProfilesError,
|
||||||
languageProfilesError,
|
languageProfilesError,
|
||||||
|
importListsError,
|
||||||
systemStatusError
|
systemStatusError
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -153,6 +160,9 @@ function createMapDispatchToProps(dispatch, props) {
|
||||||
dispatchFetchLanguageProfiles() {
|
dispatchFetchLanguageProfiles() {
|
||||||
dispatch(fetchLanguageProfiles());
|
dispatch(fetchLanguageProfiles());
|
||||||
},
|
},
|
||||||
|
dispatchFetchImportLists() {
|
||||||
|
dispatch(fetchImportLists());
|
||||||
|
},
|
||||||
dispatchFetchUISettings() {
|
dispatchFetchUISettings() {
|
||||||
dispatch(fetchUISettings());
|
dispatch(fetchUISettings());
|
||||||
},
|
},
|
||||||
|
@ -188,6 +198,7 @@ class PageConnector extends Component {
|
||||||
this.props.dispatchFetchTags();
|
this.props.dispatchFetchTags();
|
||||||
this.props.dispatchFetchQualityProfiles();
|
this.props.dispatchFetchQualityProfiles();
|
||||||
this.props.dispatchFetchLanguageProfiles();
|
this.props.dispatchFetchLanguageProfiles();
|
||||||
|
this.props.dispatchFetchImportLists();
|
||||||
this.props.dispatchFetchUISettings();
|
this.props.dispatchFetchUISettings();
|
||||||
this.props.dispatchFetchStatus();
|
this.props.dispatchFetchStatus();
|
||||||
}
|
}
|
||||||
|
@ -211,6 +222,7 @@ class PageConnector extends Component {
|
||||||
dispatchFetchTags,
|
dispatchFetchTags,
|
||||||
dispatchFetchQualityProfiles,
|
dispatchFetchQualityProfiles,
|
||||||
dispatchFetchLanguageProfiles,
|
dispatchFetchLanguageProfiles,
|
||||||
|
dispatchFetchImportLists,
|
||||||
dispatchFetchUISettings,
|
dispatchFetchUISettings,
|
||||||
dispatchFetchStatus,
|
dispatchFetchStatus,
|
||||||
...otherProps
|
...otherProps
|
||||||
|
@ -249,6 +261,7 @@ PageConnector.propTypes = {
|
||||||
dispatchFetchTags: PropTypes.func.isRequired,
|
dispatchFetchTags: PropTypes.func.isRequired,
|
||||||
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
||||||
dispatchFetchLanguageProfiles: PropTypes.func.isRequired,
|
dispatchFetchLanguageProfiles: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchImportLists: PropTypes.func.isRequired,
|
||||||
dispatchFetchUISettings: PropTypes.func.isRequired,
|
dispatchFetchUISettings: PropTypes.func.isRequired,
|
||||||
dispatchFetchStatus: PropTypes.func.isRequired,
|
dispatchFetchStatus: PropTypes.func.isRequired,
|
||||||
onSidebarVisibleChange: PropTypes.func.isRequired
|
onSidebarVisibleChange: PropTypes.func.isRequired
|
||||||
|
|
|
@ -111,6 +111,10 @@ const links = [
|
||||||
title: 'Download Clients',
|
title: 'Download Clients',
|
||||||
to: '/settings/downloadclients'
|
to: '/settings/downloadclients'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Import Lists',
|
||||||
|
to: '/settings/importlists'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Connect',
|
title: 'Connect',
|
||||||
to: '/settings/connect'
|
to: '/settings/connect'
|
||||||
|
|
|
@ -22,7 +22,8 @@ class DeleteSeriesModalContent extends Component {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
deleteFiles: false
|
deleteFiles: false,
|
||||||
|
addImportListExclusion: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,11 +34,16 @@ class DeleteSeriesModalContent extends Component {
|
||||||
this.setState({ deleteFiles: value });
|
this.setState({ deleteFiles: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onAddImportListExclusionChange = ({ value }) => {
|
||||||
|
this.setState({ addImportListExclusion: value });
|
||||||
|
}
|
||||||
|
|
||||||
onDeleteSeriesConfirmed = () => {
|
onDeleteSeriesConfirmed = () => {
|
||||||
const deleteFiles = this.state.deleteFiles;
|
const deleteFiles = this.state.deleteFiles;
|
||||||
|
const addImportListExclusion = this.state.addImportListExclusion;
|
||||||
|
|
||||||
this.setState({ deleteFiles: false });
|
this.setState({ deleteFiles: false, addImportListExclusion: false });
|
||||||
this.props.onDeletePress(deleteFiles);
|
this.props.onDeletePress(deleteFiles, addImportListExclusion);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -57,6 +63,7 @@ class DeleteSeriesModalContent extends Component {
|
||||||
} = statistics;
|
} = statistics;
|
||||||
|
|
||||||
const deleteFiles = this.state.deleteFiles;
|
const deleteFiles = this.state.deleteFiles;
|
||||||
|
const addImportListExclusion = this.state.addImportListExclusion;
|
||||||
let deleteFilesLabel = `Delete ${episodeFileCount} Episode Files`;
|
let deleteFilesLabel = `Delete ${episodeFileCount} Episode Files`;
|
||||||
let deleteFilesHelpText = 'Delete the episode files and series folder';
|
let deleteFilesHelpText = 'Delete the episode files and series folder';
|
||||||
|
|
||||||
|
@ -83,6 +90,19 @@ class DeleteSeriesModalContent extends Component {
|
||||||
{path}
|
{path}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Add List Exclusion</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="addImportListExclusion"
|
||||||
|
value={addImportListExclusion}
|
||||||
|
helpText="Prevent series from being added to Sonarr by lists"
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onChange={this.onAddImportListExclusionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{deleteFilesLabel}</FormLabel>
|
<FormLabel>{deleteFilesLabel}</FormLabel>
|
||||||
|
|
||||||
|
|
|
@ -24,10 +24,11 @@ class DeleteSeriesModalContentConnector extends Component {
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onDeletePress = (deleteFiles) => {
|
onDeletePress = (deleteFiles, addImportListExclusion) => {
|
||||||
this.props.deleteSeries({
|
this.props.deleteSeries({
|
||||||
id: this.props.seriesId,
|
id: this.props.seriesId,
|
||||||
deleteFiles
|
deleteFiles,
|
||||||
|
addImportListExclusion
|
||||||
});
|
});
|
||||||
|
|
||||||
this.props.onModalClose(true);
|
this.props.onModalClose(true);
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import EditImportListExclusionModalContentConnector from './EditImportListExclusionModalContentConnector';
|
||||||
|
|
||||||
|
function EditImportListExclusionModal({ isOpen, onModalClose, ...otherProps }) {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
size={sizes.MEDIUM}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<EditImportListExclusionModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditImportListExclusionModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditImportListExclusionModal;
|
|
@ -0,0 +1,43 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
|
import EditImportListExclusionModal from './EditImportListExclusionModal';
|
||||||
|
|
||||||
|
function mapStateToProps() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
clearPendingChanges
|
||||||
|
};
|
||||||
|
|
||||||
|
class EditImportListExclusionModalConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onModalClose = () => {
|
||||||
|
this.props.clearPendingChanges({ section: 'settings.importListExclusions' });
|
||||||
|
this.props.onModalClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<EditImportListExclusionModal
|
||||||
|
{...this.props}
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditImportListExclusionModalConnector.propTypes = {
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
clearPendingChanges: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(EditImportListExclusionModalConnector);
|
|
@ -0,0 +1,11 @@
|
||||||
|
.body {
|
||||||
|
composes: modalBody from '~Components/Modal/ModalBody.css';
|
||||||
|
|
||||||
|
flex: 1 1 430px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteButton {
|
||||||
|
composes: button from '~Components/Link/Button.css';
|
||||||
|
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
|
import { stringSettingShape, numberSettingShape } from 'Helpers/Props/Shapes/settingShape';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import styles from './EditImportListExclusionModalContent.css';
|
||||||
|
|
||||||
|
function EditImportListExclusionModalContent(props) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
isFetching,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
item,
|
||||||
|
onInputChange,
|
||||||
|
onSavePress,
|
||||||
|
onModalClose,
|
||||||
|
onDeleteImportListExclusionPress,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
tvdbId
|
||||||
|
} = item;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{id ? 'Edit Import List Exclusion' : 'Add Import List Exclusion'}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody className={styles.body}>
|
||||||
|
{
|
||||||
|
isFetching &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !!error &&
|
||||||
|
<div>Unable to add a new import list exclusion, please try again.</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !error &&
|
||||||
|
<Form
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Title</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="title"
|
||||||
|
helpText="The name of the series to exclude"
|
||||||
|
{...title}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>TVDB ID</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="tvdbId"
|
||||||
|
helpText="The TVDB ID of the series to exclude"
|
||||||
|
{...tvdbId}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
{
|
||||||
|
id &&
|
||||||
|
<Button
|
||||||
|
className={styles.deleteButton}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onPress={onDeleteImportListExclusionPress}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onPress={onModalClose}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<SpinnerErrorButton
|
||||||
|
isSpinning={isSaving}
|
||||||
|
error={saveError}
|
||||||
|
onPress={onSavePress}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImportListExclusionShape = {
|
||||||
|
title: PropTypes.shape(stringSettingShape).isRequired,
|
||||||
|
tvdbId: PropTypes.shape(numberSettingShape).isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
EditImportListExclusionModalContent.propTypes = {
|
||||||
|
id: PropTypes.number,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
saveError: PropTypes.object,
|
||||||
|
item: PropTypes.shape(ImportListExclusionShape).isRequired,
|
||||||
|
onInputChange: PropTypes.func.isRequired,
|
||||||
|
onSavePress: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
onDeleteImportListExclusionPress: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditImportListExclusionModalContent;
|
|
@ -0,0 +1,118 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
|
import { setImportListExclusionValue, saveImportListExclusion } from 'Store/Actions/settingsActions';
|
||||||
|
import EditImportListExclusionModalContent from './EditImportListExclusionModalContent';
|
||||||
|
|
||||||
|
const newImportListExclusion = {
|
||||||
|
title: '',
|
||||||
|
tvdbId: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
function createImportListExclusionSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { id }) => id,
|
||||||
|
(state) => state.settings.importListExclusions,
|
||||||
|
(id, importListExclusions) => {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
pendingChanges,
|
||||||
|
items
|
||||||
|
} = importListExclusions;
|
||||||
|
|
||||||
|
const mapping = id ? _.find(items, { id }) : newImportListExclusion;
|
||||||
|
const settings = selectSettings(mapping, pendingChanges, saveError);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
isFetching,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
item: settings.settings,
|
||||||
|
...settings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createImportListExclusionSelector(),
|
||||||
|
(importListExclusion) => {
|
||||||
|
return {
|
||||||
|
...importListExclusion
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
setImportListExclusionValue,
|
||||||
|
saveImportListExclusion
|
||||||
|
};
|
||||||
|
|
||||||
|
class EditImportListExclusionModalContentConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (!this.props.id) {
|
||||||
|
Object.keys(newImportListExclusion).forEach((name) => {
|
||||||
|
this.props.setImportListExclusionValue({
|
||||||
|
name,
|
||||||
|
value: newImportListExclusion[name]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||||
|
this.props.onModalClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = ({ name, value }) => {
|
||||||
|
this.props.setImportListExclusionValue({ name, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
onSavePress = () => {
|
||||||
|
this.props.saveImportListExclusion({ id: this.props.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<EditImportListExclusionModalContent
|
||||||
|
{...this.props}
|
||||||
|
onSavePress={this.onSavePress}
|
||||||
|
onInputChange={this.onInputChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditImportListExclusionModalContentConnector.propTypes = {
|
||||||
|
id: PropTypes.number,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
saveError: PropTypes.object,
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
setImportListExclusionValue: PropTypes.func.isRequired,
|
||||||
|
saveImportListExclusion: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(EditImportListExclusionModalContentConnector);
|
|
@ -0,0 +1,23 @@
|
||||||
|
.importListExclusion {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
height: 30px;
|
||||||
|
border-bottom: 1px solid $borderColor;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
flex: 0 0 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tvdbId {
|
||||||
|
flex: 0 0 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import EditImportListExclusionModalConnector from './EditImportListExclusionModalConnector';
|
||||||
|
import styles from './ImportListExclusion.css';
|
||||||
|
|
||||||
|
class ImportListExclusion extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isEditImportListExclusionModalOpen: false,
|
||||||
|
isDeleteImportListExclusionModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onEditImportListExclusionPress = () => {
|
||||||
|
this.setState({ isEditImportListExclusionModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditImportListExclusionModalClose = () => {
|
||||||
|
this.setState({ isEditImportListExclusionModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteImportListExclusionPress = () => {
|
||||||
|
this.setState({
|
||||||
|
isEditImportListExclusionModalOpen: false,
|
||||||
|
isDeleteImportListExclusionModalOpen: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteImportListExclusionModalClose = () => {
|
||||||
|
this.setState({ isDeleteImportListExclusionModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onConfirmDeleteImportListExclusion = () => {
|
||||||
|
this.props.onConfirmDeleteImportListExclusion(this.props.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
tvdbId
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.importListExclusion
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={styles.title}>{title}</div>
|
||||||
|
<div className={styles.tvdbId}>{tvdbId}</div>
|
||||||
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
<Link
|
||||||
|
onPress={this.onEditImportListExclusionPress}
|
||||||
|
>
|
||||||
|
<Icon name={icons.EDIT} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditImportListExclusionModalConnector
|
||||||
|
id={id}
|
||||||
|
isOpen={this.state.isEditImportListExclusionModalOpen}
|
||||||
|
onModalClose={this.onEditImportListExclusionModalClose}
|
||||||
|
onDeleteImportListExclusionPress={this.onDeleteImportListExclusionPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={this.state.isDeleteImportListExclusionModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title="Delete Import List Exclusion"
|
||||||
|
message="Are you sure you want to delete this import list exclusion?"
|
||||||
|
confirmLabel="Delete"
|
||||||
|
onConfirm={this.onConfirmDeleteImportListExclusion}
|
||||||
|
onCancel={this.onDeleteImportListExclusionModalClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportListExclusion.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
tvdbId: PropTypes.number.isRequired,
|
||||||
|
onConfirmDeleteImportListExclusion: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
ImportListExclusion.defaultProps = {
|
||||||
|
// The drag preview will not connect the drag handle.
|
||||||
|
connectDragSource: (node) => node
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImportListExclusion;
|
|
@ -0,0 +1,23 @@
|
||||||
|
.importListExclusionsHeader {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.host {
|
||||||
|
flex: 0 0 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path {
|
||||||
|
flex: 0 0 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addImportListExclusion {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addButton {
|
||||||
|
text-align: center;
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||||
|
import ImportListExclusion from './ImportListExclusion';
|
||||||
|
import EditImportListExclusionModalConnector from './EditImportListExclusionModalConnector';
|
||||||
|
import styles from './ImportListExclusions.css';
|
||||||
|
|
||||||
|
class ImportListExclusions extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isAddImportListExclusionModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onAddImportListExclusionPress = () => {
|
||||||
|
this.setState({ isAddImportListExclusionModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onModalClose = () => {
|
||||||
|
this.setState({ isAddImportListExclusionModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
onConfirmDeleteImportListExclusion,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldSet legend="Import List Exclusions">
|
||||||
|
<PageSectionContent
|
||||||
|
errorMessage="Unable to load Import List Exclusions"
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<div className={styles.importListExclusionsHeader}>
|
||||||
|
<div className={styles.host}>Title</div>
|
||||||
|
<div className={styles.path}>TVDB ID</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
items.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<ImportListExclusion
|
||||||
|
key={item.id}
|
||||||
|
{...item}
|
||||||
|
{...otherProps}
|
||||||
|
index={index}
|
||||||
|
onConfirmDeleteImportListExclusion={onConfirmDeleteImportListExclusion}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.addImportListExclusion}>
|
||||||
|
<Link
|
||||||
|
className={styles.addButton}
|
||||||
|
onPress={this.onAddImportListExclusionPress}
|
||||||
|
>
|
||||||
|
<Icon name={icons.ADD} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditImportListExclusionModalConnector
|
||||||
|
isOpen={this.state.isAddImportListExclusionModalOpen}
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</PageSectionContent>
|
||||||
|
</FieldSet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportListExclusions.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onConfirmDeleteImportListExclusion: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImportListExclusions;
|
|
@ -0,0 +1,59 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { fetchImportListExclusions, deleteImportListExclusion } from 'Store/Actions/settingsActions';
|
||||||
|
import ImportListExclusions from './ImportListExclusions';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.importListExclusions,
|
||||||
|
(importListExclusions) => {
|
||||||
|
return {
|
||||||
|
...importListExclusions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
fetchImportListExclusions,
|
||||||
|
deleteImportListExclusion
|
||||||
|
};
|
||||||
|
|
||||||
|
class ImportListExclusionsConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.fetchImportListExclusions();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onConfirmDeleteImportListExclusion = (id) => {
|
||||||
|
this.props.deleteImportListExclusion({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<ImportListExclusions
|
||||||
|
{...this.state}
|
||||||
|
{...this.props}
|
||||||
|
onConfirmDeleteImportListExclusion={this.onConfirmDeleteImportListExclusion}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportListExclusionsConnector.propTypes = {
|
||||||
|
fetchImportListExclusions: PropTypes.func.isRequired,
|
||||||
|
deleteImportListExclusion: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(ImportListExclusionsConnector);
|
|
@ -0,0 +1,90 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
|
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||||
|
import ImportListsConnector from './ImportLists/ImportListsConnector';
|
||||||
|
import ImportListsExclusionsConnector from './ImportListExclusions/ImportListExclusionsConnector';
|
||||||
|
|
||||||
|
class ImportListSettings extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
hasPendingChanges: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
setListOptionsRef = (ref) => {
|
||||||
|
this._listOptions = ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
onHasPendingChange = (hasPendingChanges) => {
|
||||||
|
this.setState({
|
||||||
|
hasPendingChanges
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onSavePress = () => {
|
||||||
|
this._listOptions.getWrappedInstance().save();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isTestingAll,
|
||||||
|
dispatchTestAllImportLists
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isSaving,
|
||||||
|
hasPendingChanges
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title="Import List Settings">
|
||||||
|
<SettingsToolbarConnector
|
||||||
|
isSaving={isSaving}
|
||||||
|
hasPendingChanges={hasPendingChanges}
|
||||||
|
additionalButtons={
|
||||||
|
<Fragment>
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Test All Lists"
|
||||||
|
iconName={icons.TEST}
|
||||||
|
isSpinning={isTestingAll}
|
||||||
|
onPress={dispatchTestAllImportLists}
|
||||||
|
/>
|
||||||
|
</Fragment>
|
||||||
|
}
|
||||||
|
onSavePress={this.onSavePress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageContentBody>
|
||||||
|
<ImportListsConnector />
|
||||||
|
<ImportListsExclusionsConnector />
|
||||||
|
</PageContentBody>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportListSettings.propTypes = {
|
||||||
|
isTestingAll: PropTypes.bool.isRequired,
|
||||||
|
dispatchTestAllImportLists: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImportListSettings;
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { testAllImportLists } from 'Store/Actions/settingsActions';
|
||||||
|
import ImportListSettings from './ImportListSettings';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.importLists.isTestingAll,
|
||||||
|
(isTestingAll) => {
|
||||||
|
return {
|
||||||
|
isTestingAll
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchTestAllImportLists: testAllImportLists
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(ImportListSettings);
|
|
@ -0,0 +1,44 @@
|
||||||
|
.list {
|
||||||
|
composes: card from '~Components/Card.css';
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
width: 300px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.underlay {
|
||||||
|
@add-mixin cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
@add-mixin linkOverlay;
|
||||||
|
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
text-align: center;
|
||||||
|
font-weight: lighter;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presetsMenu {
|
||||||
|
composes: menu from '~Components/Menu/Menu.css';
|
||||||
|
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.presetsMenuButton {
|
||||||
|
composes: button from '~Components/Link/Button.css';
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
margin-left: 5px;
|
||||||
|
content: '\25BE';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import Menu from 'Components/Menu/Menu';
|
||||||
|
import MenuContent from 'Components/Menu/MenuContent';
|
||||||
|
import AddImportListPresetMenuItem from './AddImportListPresetMenuItem';
|
||||||
|
import styles from './AddImportListItem.css';
|
||||||
|
|
||||||
|
class AddImportListItem extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onImportListSelect = () => {
|
||||||
|
const {
|
||||||
|
implementation
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
this.props.onImportListSelect({ implementation });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
implementation,
|
||||||
|
implementationName,
|
||||||
|
infoLink,
|
||||||
|
presets,
|
||||||
|
onImportListSelect
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const hasPresets = !!(presets && presets.length);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.list}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
className={styles.underlay}
|
||||||
|
onPress={this.onImportListSelect}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.overlay}>
|
||||||
|
<div className={styles.name}>
|
||||||
|
{implementationName}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.actions}>
|
||||||
|
{
|
||||||
|
hasPresets &&
|
||||||
|
<span>
|
||||||
|
<Button
|
||||||
|
size={sizes.SMALL}
|
||||||
|
onPress={this.onListSelect}
|
||||||
|
>
|
||||||
|
Custom
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Menu className={styles.presetsMenu}>
|
||||||
|
<Button
|
||||||
|
className={styles.presetsMenuButton}
|
||||||
|
size={sizes.SMALL}
|
||||||
|
>
|
||||||
|
Presets
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<MenuContent>
|
||||||
|
{
|
||||||
|
presets.map((preset) => {
|
||||||
|
return (
|
||||||
|
<AddImportListPresetMenuItem
|
||||||
|
key={preset.name}
|
||||||
|
name={preset.name}
|
||||||
|
implementation={implementation}
|
||||||
|
onPress={onImportListSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</MenuContent>
|
||||||
|
</Menu>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
to={infoLink}
|
||||||
|
size={sizes.SMALL}
|
||||||
|
>
|
||||||
|
More info
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddImportListItem.propTypes = {
|
||||||
|
implementation: PropTypes.string.isRequired,
|
||||||
|
implementationName: PropTypes.string.isRequired,
|
||||||
|
infoLink: PropTypes.string.isRequired,
|
||||||
|
presets: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
onImportListSelect: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddImportListItem;
|
|
@ -0,0 +1,25 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import AddImportListModalContentConnector from './AddImportListModalContentConnector';
|
||||||
|
|
||||||
|
function AddImportListModal({ isOpen, onModalClose, ...otherProps }) {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<AddImportListModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AddImportListModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddImportListModal;
|
|
@ -0,0 +1,5 @@
|
||||||
|
.lists {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import AddImportListItem from './AddImportListItem';
|
||||||
|
import styles from './AddImportListModalContent.css';
|
||||||
|
import titleCase from 'Utilities/String/titleCase';
|
||||||
|
|
||||||
|
class AddImportListModalContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isSchemaFetching,
|
||||||
|
isSchemaPopulated,
|
||||||
|
schemaError,
|
||||||
|
listGroups,
|
||||||
|
onImportListSelect,
|
||||||
|
onModalClose
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
Add List
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
{
|
||||||
|
isSchemaFetching ?
|
||||||
|
<LoadingIndicator /> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isSchemaFetching && !!schemaError ?
|
||||||
|
<div>Unable to add a new list, please try again.</div> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isSchemaPopulated && !schemaError ?
|
||||||
|
<div>
|
||||||
|
|
||||||
|
<Alert kind={kinds.INFO}>
|
||||||
|
<div>Sonarr supports multiple lists for importing Series into the database.</div>
|
||||||
|
<div>For more information on the individual lists, click on the info buttons.</div>
|
||||||
|
</Alert>
|
||||||
|
{
|
||||||
|
Object.keys(listGroups).map((key) => {
|
||||||
|
return (
|
||||||
|
<FieldSet legend={`${titleCase(key)} List`} key={key}>
|
||||||
|
<div className={styles.lists}>
|
||||||
|
{
|
||||||
|
listGroups[key].map((list) => {
|
||||||
|
return (
|
||||||
|
<AddImportListItem
|
||||||
|
key={list.implementation}
|
||||||
|
implementation={list.implementation}
|
||||||
|
{...list}
|
||||||
|
onImportListSelect={onImportListSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</FieldSet>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
onPress={onModalClose}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddImportListModalContent.propTypes = {
|
||||||
|
isSchemaFetching: PropTypes.bool.isRequired,
|
||||||
|
isSchemaPopulated: PropTypes.bool.isRequired,
|
||||||
|
schemaError: PropTypes.object,
|
||||||
|
listGroups: PropTypes.object.isRequired,
|
||||||
|
onImportListSelect: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddImportListModalContent;
|
|
@ -0,0 +1,76 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { fetchImportListSchema, selectImportListSchema } from 'Store/Actions/settingsActions';
|
||||||
|
import AddImportListModalContent from './AddImportListModalContent';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.importLists,
|
||||||
|
(importLists) => {
|
||||||
|
const {
|
||||||
|
isSchemaFetching,
|
||||||
|
isSchemaPopulated,
|
||||||
|
schemaError,
|
||||||
|
schema
|
||||||
|
} = importLists;
|
||||||
|
|
||||||
|
const listGroups = _.chain(schema)
|
||||||
|
.sortBy((o) => o.listOrder)
|
||||||
|
.groupBy('listType')
|
||||||
|
.value();
|
||||||
|
|
||||||
|
return {
|
||||||
|
isSchemaFetching,
|
||||||
|
isSchemaPopulated,
|
||||||
|
schemaError,
|
||||||
|
listGroups
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
fetchImportListSchema,
|
||||||
|
selectImportListSchema
|
||||||
|
};
|
||||||
|
|
||||||
|
class AddImportListModalContentConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.fetchImportListSchema();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onImportListSelect = ({ implementation, name }) => {
|
||||||
|
this.props.selectImportListSchema({ implementation, presetName: name });
|
||||||
|
this.props.onModalClose({ listSelected: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<AddImportListModalContent
|
||||||
|
{...this.props}
|
||||||
|
onImportListSelect={this.onImportListSelect}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddImportListModalContentConnector.propTypes = {
|
||||||
|
fetchImportListSchema: PropTypes.func.isRequired,
|
||||||
|
selectImportListSchema: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(AddImportListModalContentConnector);
|
|
@ -0,0 +1,49 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import MenuItem from 'Components/Menu/MenuItem';
|
||||||
|
|
||||||
|
class AddImportListPresetMenuItem extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onPress = () => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
implementation
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
this.props.onPress({
|
||||||
|
name,
|
||||||
|
implementation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
implementation,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
{...otherProps}
|
||||||
|
onPress={this.onPress}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AddImportListPresetMenuItem.propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
implementation: PropTypes.string.isRequired,
|
||||||
|
onPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddImportListPresetMenuItem;
|
|
@ -0,0 +1,25 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import EditImportListModalContentConnector from './EditImportListModalContentConnector';
|
||||||
|
|
||||||
|
function EditImportListModal({ isOpen, onModalClose, ...otherProps }) {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<EditImportListModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditImportListModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditImportListModal;
|
|
@ -0,0 +1,65 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
|
import { cancelTestImportList, cancelSaveImportList } from 'Store/Actions/settingsActions';
|
||||||
|
import EditImportListModal from './EditImportListModal';
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
const section = 'settings.importLists';
|
||||||
|
|
||||||
|
return {
|
||||||
|
dispatchClearPendingChanges() {
|
||||||
|
dispatch(clearPendingChanges({ section }));
|
||||||
|
},
|
||||||
|
|
||||||
|
dispatchCancelTestImportList() {
|
||||||
|
dispatch(cancelTestImportList({ section }));
|
||||||
|
},
|
||||||
|
|
||||||
|
dispatchCancelSaveImportList() {
|
||||||
|
dispatch(cancelSaveImportList({ section }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class EditImportListModalConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onModalClose = () => {
|
||||||
|
this.props.dispatchClearPendingChanges();
|
||||||
|
this.props.dispatchCancelTestImportList();
|
||||||
|
this.props.dispatchCancelSaveImportList();
|
||||||
|
this.props.onModalClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
dispatchClearPendingChanges,
|
||||||
|
dispatchCancelTestImportList,
|
||||||
|
dispatchCancelSaveImportList,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EditImportListModal
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditImportListModalConnector.propTypes = {
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
dispatchClearPendingChanges: PropTypes.func.isRequired,
|
||||||
|
dispatchCancelTestImportList: PropTypes.func.isRequired,
|
||||||
|
dispatchCancelSaveImportList: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(null, createMapDispatchToProps)(EditImportListModalConnector);
|
|
@ -0,0 +1,15 @@
|
||||||
|
.deleteButton {
|
||||||
|
composes: button from '~Components/Link/Button.css';
|
||||||
|
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hideLanguageProfile {
|
||||||
|
composes: group from '~Components/Form/FormGroup.css';
|
||||||
|
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.labelIcon {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
|
@ -0,0 +1,251 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
|
||||||
|
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||||
|
import styles from './EditImportListModalContent.css';
|
||||||
|
|
||||||
|
function EditImportListModalContent(props) {
|
||||||
|
|
||||||
|
const {
|
||||||
|
advancedSettings,
|
||||||
|
isFetching,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
isTesting,
|
||||||
|
saveError,
|
||||||
|
item,
|
||||||
|
onInputChange,
|
||||||
|
onFieldChange,
|
||||||
|
onModalClose,
|
||||||
|
onSavePress,
|
||||||
|
onTestPress,
|
||||||
|
onDeleteImportListPress,
|
||||||
|
showLanguageProfile,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
enableAutomaticAdd,
|
||||||
|
shouldMonitor,
|
||||||
|
rootFolderPath,
|
||||||
|
qualityProfileId,
|
||||||
|
languageProfileId,
|
||||||
|
tags,
|
||||||
|
fields
|
||||||
|
} = item;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{id ? 'Edit List' : 'Add List'}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
{
|
||||||
|
isFetching ?
|
||||||
|
<LoadingIndicator /> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !!error ?
|
||||||
|
<div>Unable to add a new list, please try again.</div> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !error ?
|
||||||
|
<Form {...otherProps}>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Name</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="name"
|
||||||
|
{...name}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Enable Automatic Add</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="enableAutomaticAdd"
|
||||||
|
helpText={'Add series to Sonarr when syncs are performed via the UI or by Sonarr'}
|
||||||
|
{...enableAutomaticAdd}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
Monitor
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.labelIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title="Monitoring Options"
|
||||||
|
body={<SeriesMonitoringOptionsPopoverContent />}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.MONITOR_EPISODES_SELECT}
|
||||||
|
name="shouldMonitor"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...shouldMonitor}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Root Folder</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||||
|
name="rootFolderPath"
|
||||||
|
helpText={'Root Folder list items will be added to'}
|
||||||
|
{...rootFolderPath}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Quality Profile</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||||
|
name="qualityProfileId"
|
||||||
|
helpText={'Quality Profile list items should be added with'}
|
||||||
|
{...qualityProfileId}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup className={showLanguageProfile ? undefined : styles.hideLanguageProfile}>
|
||||||
|
<FormLabel>Language Profile</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.LANGUAGE_PROFILE_SELECT}
|
||||||
|
name="languageProfileId"
|
||||||
|
helpText={'Language Profile list items should be added with'}
|
||||||
|
{...languageProfileId}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Sonarr Tags</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TAG}
|
||||||
|
name="tags"
|
||||||
|
helpText="Add series from this list with these tags"
|
||||||
|
{...tags}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
{
|
||||||
|
!!fields && !!fields.length &&
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
fields.map((field) => {
|
||||||
|
return (
|
||||||
|
<ProviderFieldFormGroup
|
||||||
|
key={field.name}
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
provider="importList"
|
||||||
|
providerData={item}
|
||||||
|
section="settings.importLists"
|
||||||
|
{...field}
|
||||||
|
onChange={onFieldChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</Form> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
{
|
||||||
|
id &&
|
||||||
|
<Button
|
||||||
|
className={styles.deleteButton}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onPress={onDeleteImportListPress}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
|
<SpinnerErrorButton
|
||||||
|
isSpinning={isTesting}
|
||||||
|
error={saveError}
|
||||||
|
onPress={onTestPress}
|
||||||
|
>
|
||||||
|
Test
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onPress={onModalClose}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<SpinnerErrorButton
|
||||||
|
isSpinning={isSaving}
|
||||||
|
error={saveError}
|
||||||
|
onPress={onSavePress}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditImportListModalContent.propTypes = {
|
||||||
|
advancedSettings: PropTypes.bool.isRequired,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
isTesting: PropTypes.bool.isRequired,
|
||||||
|
saveError: PropTypes.object,
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
showLanguageProfile: PropTypes.bool.isRequired,
|
||||||
|
onInputChange: PropTypes.func.isRequired,
|
||||||
|
onFieldChange: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
onSavePress: PropTypes.func.isRequired,
|
||||||
|
onTestPress: PropTypes.func.isRequired,
|
||||||
|
onDeleteImportListPress: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditImportListModalContent;
|
|
@ -0,0 +1,90 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||||
|
import { setImportListValue, setImportListFieldValue, saveImportList, testImportList } from 'Store/Actions/settingsActions';
|
||||||
|
import EditImportListModalContent from './EditImportListModalContent';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.advancedSettings,
|
||||||
|
(state) => state.settings.languageProfiles,
|
||||||
|
createProviderSettingsSelector('importLists'),
|
||||||
|
(advancedSettings, languageProfiles, importList) => {
|
||||||
|
return {
|
||||||
|
advancedSettings,
|
||||||
|
showLanguageProfile: languageProfiles.items.length > 1,
|
||||||
|
...importList
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
setImportListValue,
|
||||||
|
setImportListFieldValue,
|
||||||
|
saveImportList,
|
||||||
|
testImportList
|
||||||
|
};
|
||||||
|
|
||||||
|
class EditImportListModalContentConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||||
|
this.props.onModalClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = ({ name, value }) => {
|
||||||
|
this.props.setImportListValue({ name, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
onFieldChange = ({ name, value }) => {
|
||||||
|
this.props.setImportListFieldValue({ name, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
onSavePress = () => {
|
||||||
|
this.props.saveImportList({ id: this.props.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
onTestPress = () => {
|
||||||
|
this.props.testImportList({ id: this.props.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<EditImportListModalContent
|
||||||
|
{...this.props}
|
||||||
|
onSavePress={this.onSavePress}
|
||||||
|
onTestPress={this.onTestPress}
|
||||||
|
onInputChange={this.onInputChange}
|
||||||
|
onFieldChange={this.onFieldChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditImportListModalContentConnector.propTypes = {
|
||||||
|
id: PropTypes.number,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
saveError: PropTypes.object,
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
setImportListValue: PropTypes.func.isRequired,
|
||||||
|
setImportListFieldValue: PropTypes.func.isRequired,
|
||||||
|
saveImportList: PropTypes.func.isRequired,
|
||||||
|
testImportList: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(EditImportListModalContentConnector);
|
|
@ -0,0 +1,19 @@
|
||||||
|
.list {
|
||||||
|
composes: card from '~Components/Card.css';
|
||||||
|
|
||||||
|
width: 290px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
@add-mixin truncate;
|
||||||
|
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.enabled {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import Card from 'Components/Card';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import EditImportListModalConnector from './EditImportListModalConnector';
|
||||||
|
import styles from './ImportList.css';
|
||||||
|
|
||||||
|
class ImportList extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isEditImportListModalOpen: false,
|
||||||
|
isDeleteImportListModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onEditImportListPress = () => {
|
||||||
|
this.setState({ isEditImportListModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditImportListModalClose = () => {
|
||||||
|
this.setState({ isEditImportListModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteImportListPress = () => {
|
||||||
|
this.setState({
|
||||||
|
isEditImportListModalOpen: false,
|
||||||
|
isDeleteImportListModalOpen: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteImportListModalClose= () => {
|
||||||
|
this.setState({ isDeleteImportListModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onConfirmDeleteImportList = () => {
|
||||||
|
this.props.onConfirmDeleteImportList(this.props.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
enableAutomaticAdd
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={styles.list}
|
||||||
|
overlayContent={true}
|
||||||
|
onPress={this.onEditImportListPress}
|
||||||
|
>
|
||||||
|
<div className={styles.name}>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.enabled}>
|
||||||
|
{
|
||||||
|
enableAutomaticAdd &&
|
||||||
|
<Label kind={kinds.SUCCESS}>
|
||||||
|
Automatic Add
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditImportListModalConnector
|
||||||
|
id={id}
|
||||||
|
isOpen={this.state.isEditImportListModalOpen}
|
||||||
|
onModalClose={this.onEditImportListModalClose}
|
||||||
|
onDeleteImportListPress={this.onDeleteImportListPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={this.state.isDeleteImportListModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title="Delete Import List"
|
||||||
|
message={`Are you sure you want to delete the list '${name}'?`}
|
||||||
|
confirmLabel="Delete"
|
||||||
|
onConfirm={this.onConfirmDeleteImportList}
|
||||||
|
onCancel={this.onDeleteImportListModalClose}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportList.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
enableAutomaticAdd: PropTypes.bool.isRequired,
|
||||||
|
onConfirmDeleteImportList: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImportList;
|
|
@ -0,0 +1,20 @@
|
||||||
|
.lists {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addList {
|
||||||
|
composes: list from '~./ImportList.css';
|
||||||
|
|
||||||
|
background-color: $cardAlternateBackgroundColor;
|
||||||
|
color: $gray;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 5px 20px 0;
|
||||||
|
border: 1px solid $borderColor;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: $white;
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
|
import Card from 'Components/Card';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||||
|
import ImportList from './ImportList';
|
||||||
|
import AddImportListModal from './AddImportListModal';
|
||||||
|
import EditImportListModalConnector from './EditImportListModalConnector';
|
||||||
|
import styles from './ImportLists.css';
|
||||||
|
|
||||||
|
class ImportLists extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isAddImportListModalOpen: false,
|
||||||
|
isEditImportListModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onAddImportListPress = () => {
|
||||||
|
this.setState({ isAddImportListModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onAddImportListModalClose = ({ listSelected = false } = {}) => {
|
||||||
|
this.setState({
|
||||||
|
isAddImportListModalOpen: false,
|
||||||
|
isEditImportListModalOpen: listSelected
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditImportListModalClose = () => {
|
||||||
|
this.setState({ isEditImportListModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
onConfirmDeleteImportList,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isAddImportListModalOpen,
|
||||||
|
isEditImportListModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldSet
|
||||||
|
legend="Import Lists"
|
||||||
|
>
|
||||||
|
<PageSectionContent
|
||||||
|
errorMessage="Unable to load Lists"
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<div className={styles.lists}>
|
||||||
|
{
|
||||||
|
items.sort(sortByName).map((item) => {
|
||||||
|
return (
|
||||||
|
<ImportList
|
||||||
|
key={item.id}
|
||||||
|
{...item}
|
||||||
|
onConfirmDeleteImportList={onConfirmDeleteImportList}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<Card
|
||||||
|
className={styles.addList}
|
||||||
|
onPress={this.onAddImportListPress}
|
||||||
|
>
|
||||||
|
<div className={styles.center}>
|
||||||
|
<Icon
|
||||||
|
name={icons.ADD}
|
||||||
|
size={45}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AddImportListModal
|
||||||
|
isOpen={isAddImportListModalOpen}
|
||||||
|
onModalClose={this.onAddImportListModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditImportListModalConnector
|
||||||
|
isOpen={isEditImportListModalOpen}
|
||||||
|
onModalClose={this.onEditImportListModalClose}
|
||||||
|
/>
|
||||||
|
</PageSectionContent>
|
||||||
|
</FieldSet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportLists.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onConfirmDeleteImportList: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImportLists;
|
|
@ -0,0 +1,62 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { fetchImportLists, deleteImportList } from 'Store/Actions/settingsActions';
|
||||||
|
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||||
|
import ImportLists from './ImportLists';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.importLists,
|
||||||
|
(importLists) => {
|
||||||
|
return {
|
||||||
|
...importLists
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
fetchImportLists,
|
||||||
|
deleteImportList,
|
||||||
|
fetchRootFolders
|
||||||
|
};
|
||||||
|
|
||||||
|
class ListsConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.fetchImportLists();
|
||||||
|
this.props.fetchRootFolders();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onConfirmDeleteImportList = (id) => {
|
||||||
|
this.props.deleteImportList({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<ImportLists
|
||||||
|
{...this.props}
|
||||||
|
onConfirmDeleteImportList={this.onConfirmDeleteImportList}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ListsConnector.propTypes = {
|
||||||
|
fetchImportLists: PropTypes.func.isRequired,
|
||||||
|
deleteImportList: PropTypes.func.isRequired,
|
||||||
|
fetchRootFolders: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(ListsConnector);
|
|
@ -18,6 +18,7 @@ function TagDetailsModalContent(props) {
|
||||||
isTagUsed,
|
isTagUsed,
|
||||||
series,
|
series,
|
||||||
delayProfiles,
|
delayProfiles,
|
||||||
|
importLists,
|
||||||
notifications,
|
notifications,
|
||||||
releaseProfiles,
|
releaseProfiles,
|
||||||
onModalClose,
|
onModalClose,
|
||||||
|
@ -95,6 +96,21 @@ function TagDetailsModalContent(props) {
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!!importLists.length &&
|
||||||
|
<FieldSet legend="Import Lists">
|
||||||
|
{
|
||||||
|
importLists.map((item) => {
|
||||||
|
return (
|
||||||
|
<div key={item.id}>
|
||||||
|
{item.name}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</FieldSet>
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!!releaseProfiles.length &&
|
!!releaseProfiles.length &&
|
||||||
<FieldSet legend="Release Profiles">
|
<FieldSet legend="Release Profiles">
|
||||||
|
@ -170,6 +186,7 @@ TagDetailsModalContent.propTypes = {
|
||||||
isTagUsed: PropTypes.bool.isRequired,
|
isTagUsed: PropTypes.bool.isRequired,
|
||||||
series: PropTypes.arrayOf(PropTypes.object).isRequired,
|
series: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
delayProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
delayProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
importLists: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
notifications: PropTypes.arrayOf(PropTypes.object).isRequired,
|
notifications: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
releaseProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
releaseProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired,
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -45,6 +45,14 @@ function createMatchingDelayProfilesSelector() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createMatchingImportListsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { importListIds }) => importListIds,
|
||||||
|
(state) => state.settings.importLists.items,
|
||||||
|
findMatchingItems
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function createMatchingNotificationsSelector() {
|
function createMatchingNotificationsSelector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state, { notificationIds }) => notificationIds,
|
(state, { notificationIds }) => notificationIds,
|
||||||
|
@ -65,12 +73,14 @@ function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createMatchingSeriesSelector(),
|
createMatchingSeriesSelector(),
|
||||||
createMatchingDelayProfilesSelector(),
|
createMatchingDelayProfilesSelector(),
|
||||||
|
createMatchingImportListsSelector(),
|
||||||
createMatchingNotificationsSelector(),
|
createMatchingNotificationsSelector(),
|
||||||
createMatchingReleaseProfilesSelector(),
|
createMatchingReleaseProfilesSelector(),
|
||||||
(series, delayProfiles, notifications, releaseProfiles) => {
|
(series, delayProfiles, importLists, notifications, releaseProfiles) => {
|
||||||
return {
|
return {
|
||||||
series,
|
series,
|
||||||
delayProfiles,
|
delayProfiles,
|
||||||
|
importLists,
|
||||||
notifications,
|
notifications,
|
||||||
releaseProfiles
|
releaseProfiles
|
||||||
};
|
};
|
||||||
|
|
|
@ -53,6 +53,7 @@ class Tag extends Component {
|
||||||
const {
|
const {
|
||||||
label,
|
label,
|
||||||
delayProfileIds,
|
delayProfileIds,
|
||||||
|
importListIds,
|
||||||
notificationIds,
|
notificationIds,
|
||||||
restrictionIds,
|
restrictionIds,
|
||||||
seriesIds
|
seriesIds
|
||||||
|
@ -65,6 +66,7 @@ class Tag extends Component {
|
||||||
|
|
||||||
const isTagUsed = !!(
|
const isTagUsed = !!(
|
||||||
delayProfileIds.length ||
|
delayProfileIds.length ||
|
||||||
|
importListIds.length ||
|
||||||
notificationIds.length ||
|
notificationIds.length ||
|
||||||
restrictionIds.length ||
|
restrictionIds.length ||
|
||||||
seriesIds.length
|
seriesIds.length
|
||||||
|
@ -84,31 +86,43 @@ class Tag extends Component {
|
||||||
isTagUsed &&
|
isTagUsed &&
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
!!seriesIds.length &&
|
seriesIds.length ?
|
||||||
<div>
|
<div>
|
||||||
{seriesIds.length} series
|
{seriesIds.length} series
|
||||||
</div>
|
</div> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!!delayProfileIds.length &&
|
delayProfileIds.length ?
|
||||||
<div>
|
<div>
|
||||||
{delayProfileIds.length} delay profile{delayProfileIds.length > 1 && 's'}
|
{delayProfileIds.length} delay profile{delayProfileIds.length > 1 && 's'}
|
||||||
</div>
|
</div> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!!notificationIds.length &&
|
importListIds.length ?
|
||||||
|
<div>
|
||||||
|
{importListIds.length} import list{importListIds.length > 1 && 's'}
|
||||||
|
</div> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
notificationIds.length ?
|
||||||
<div>
|
<div>
|
||||||
{notificationIds.length} connection{notificationIds.length > 1 && 's'}
|
{notificationIds.length} connection{notificationIds.length > 1 && 's'}
|
||||||
</div>
|
</div> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!!restrictionIds.length &&
|
restrictionIds.length ?
|
||||||
<div>
|
<div>
|
||||||
{restrictionIds.length} restriction{restrictionIds.length > 1 && 's'}
|
{restrictionIds.length} restriction{restrictionIds.length > 1 && 's'}
|
||||||
</div>
|
</div> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -125,6 +139,7 @@ class Tag extends Component {
|
||||||
isTagUsed={isTagUsed}
|
isTagUsed={isTagUsed}
|
||||||
seriesIds={seriesIds}
|
seriesIds={seriesIds}
|
||||||
delayProfileIds={delayProfileIds}
|
delayProfileIds={delayProfileIds}
|
||||||
|
importListIds={importListIds}
|
||||||
notificationIds={notificationIds}
|
notificationIds={notificationIds}
|
||||||
restrictionIds={restrictionIds}
|
restrictionIds={restrictionIds}
|
||||||
isOpen={isDetailsModalOpen}
|
isOpen={isDetailsModalOpen}
|
||||||
|
@ -150,6 +165,7 @@ Tag.propTypes = {
|
||||||
id: PropTypes.number.isRequired,
|
id: PropTypes.number.isRequired,
|
||||||
label: PropTypes.string.isRequired,
|
label: PropTypes.string.isRequired,
|
||||||
delayProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
delayProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
|
importListIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
restrictionIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
restrictionIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
seriesIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
seriesIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
|
@ -158,6 +174,7 @@ Tag.propTypes = {
|
||||||
|
|
||||||
Tag.defaultProps = {
|
Tag.defaultProps = {
|
||||||
delayProfileIds: [],
|
delayProfileIds: [],
|
||||||
|
importListIds: [],
|
||||||
notificationIds: [],
|
notificationIds: [],
|
||||||
restrictionIds: [],
|
restrictionIds: [],
|
||||||
seriesIds: []
|
seriesIds: []
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { fetchTagDetails } from 'Store/Actions/tagActions';
|
import { fetchTagDetails } from 'Store/Actions/tagActions';
|
||||||
import { fetchDelayProfiles, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
|
import { fetchDelayProfiles, fetchNotifications, fetchReleaseProfiles, fetchImportLists } from 'Store/Actions/settingsActions';
|
||||||
import Tags from './Tags';
|
import Tags from './Tags';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
|
@ -27,6 +27,7 @@ function createMapStateToProps() {
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
dispatchFetchTagDetails: fetchTagDetails,
|
dispatchFetchTagDetails: fetchTagDetails,
|
||||||
dispatchFetchDelayProfiles: fetchDelayProfiles,
|
dispatchFetchDelayProfiles: fetchDelayProfiles,
|
||||||
|
dispatchFetchImportLists: fetchImportLists,
|
||||||
dispatchFetchNotifications: fetchNotifications,
|
dispatchFetchNotifications: fetchNotifications,
|
||||||
dispatchFetchReleaseProfiles: fetchReleaseProfiles
|
dispatchFetchReleaseProfiles: fetchReleaseProfiles
|
||||||
};
|
};
|
||||||
|
@ -40,12 +41,14 @@ class MetadatasConnector extends Component {
|
||||||
const {
|
const {
|
||||||
dispatchFetchTagDetails,
|
dispatchFetchTagDetails,
|
||||||
dispatchFetchDelayProfiles,
|
dispatchFetchDelayProfiles,
|
||||||
|
dispatchFetchImportLists,
|
||||||
dispatchFetchNotifications,
|
dispatchFetchNotifications,
|
||||||
dispatchFetchReleaseProfiles
|
dispatchFetchReleaseProfiles
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
dispatchFetchTagDetails();
|
dispatchFetchTagDetails();
|
||||||
dispatchFetchDelayProfiles();
|
dispatchFetchDelayProfiles();
|
||||||
|
dispatchFetchImportLists();
|
||||||
dispatchFetchNotifications();
|
dispatchFetchNotifications();
|
||||||
dispatchFetchReleaseProfiles();
|
dispatchFetchReleaseProfiles();
|
||||||
}
|
}
|
||||||
|
@ -65,6 +68,7 @@ class MetadatasConnector extends Component {
|
||||||
MetadatasConnector.propTypes = {
|
MetadatasConnector.propTypes = {
|
||||||
dispatchFetchTagDetails: PropTypes.func.isRequired,
|
dispatchFetchTagDetails: PropTypes.func.isRequired,
|
||||||
dispatchFetchDelayProfiles: PropTypes.func.isRequired,
|
dispatchFetchDelayProfiles: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchImportLists: PropTypes.func.isRequired,
|
||||||
dispatchFetchNotifications: PropTypes.func.isRequired,
|
dispatchFetchNotifications: PropTypes.func.isRequired,
|
||||||
dispatchFetchReleaseProfiles: PropTypes.func.isRequired
|
dispatchFetchReleaseProfiles: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import { createThunk } from 'Store/thunks';
|
||||||
|
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||||
|
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||||
|
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||||
|
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
const section = 'settings.importListExclusions';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const FETCH_IMPORT_LIST_EXCLUSIONS = 'settings/importListExclusions/fetchImportListExclusions';
|
||||||
|
export const SAVE_IMPORT_LIST_EXCLUSION = 'settings/importListExclusions/saveImportListExclusion';
|
||||||
|
export const DELETE_IMPORT_LIST_EXCLUSION = 'settings/importListExclusions/deleteImportListExclusion';
|
||||||
|
export const SET_IMPORT_LIST_EXCLUSION_VALUE = 'settings/importListExclusions/setImportListExclusionValue';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetchImportListExclusions = createThunk(FETCH_IMPORT_LIST_EXCLUSIONS);
|
||||||
|
export const saveImportListExclusion = createThunk(SAVE_IMPORT_LIST_EXCLUSION);
|
||||||
|
export const deleteImportListExclusion = createThunk(DELETE_IMPORT_LIST_EXCLUSION);
|
||||||
|
|
||||||
|
export const setImportListExclusionValue = createAction(SET_IMPORT_LIST_EXCLUSION_VALUE, (payload) => {
|
||||||
|
return {
|
||||||
|
section,
|
||||||
|
...payload
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// Details
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
defaultState: {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
items: [],
|
||||||
|
isSaving: false,
|
||||||
|
saveError: null,
|
||||||
|
pendingChanges: {}
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
actionHandlers: {
|
||||||
|
[FETCH_IMPORT_LIST_EXCLUSIONS]: createFetchHandler(section, '/importlistexclusion'),
|
||||||
|
[SAVE_IMPORT_LIST_EXCLUSION]: createSaveProviderHandler(section, '/importlistexclusion'),
|
||||||
|
[DELETE_IMPORT_LIST_EXCLUSION]: createRemoveItemHandler(section, '/importlistexclusion')
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
reducers: {
|
||||||
|
[SET_IMPORT_LIST_EXCLUSION_VALUE]: createSetSettingValueReducer(section)
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
|
@ -0,0 +1,118 @@
|
||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import { createThunk } from 'Store/thunks';
|
||||||
|
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
|
||||||
|
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||||
|
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
||||||
|
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||||
|
import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler';
|
||||||
|
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||||
|
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
|
||||||
|
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
|
||||||
|
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
const section = 'settings.importLists';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const FETCH_IMPORT_LISTS = 'settings/importlists/fetchImportLists';
|
||||||
|
export const FETCH_IMPORT_LIST_SCHEMA = 'settings/importlists/fetchImportListSchema';
|
||||||
|
export const SELECT_IMPORT_LIST_SCHEMA = 'settings/importlists/selectImportListSchema';
|
||||||
|
export const SET_IMPORT_LIST_VALUE = 'settings/importlists/setImportListValue';
|
||||||
|
export const SET_IMPORT_LIST_FIELD_VALUE = 'settings/importlists/setImportListFieldValue';
|
||||||
|
export const SAVE_IMPORT_LIST = 'settings/importlists/saveImportList';
|
||||||
|
export const CANCEL_SAVE_IMPORT_LIST = 'settings/importlists/cancelSaveImportList';
|
||||||
|
export const DELETE_IMPORT_LIST = 'settings/importlists/deleteImportList';
|
||||||
|
export const TEST_IMPORT_LIST = 'settings/importlists/testImportList';
|
||||||
|
export const CANCEL_TEST_IMPORT_LIST = 'settings/importlists/cancelTestImportList';
|
||||||
|
export const TEST_ALL_IMPORT_LISTS = 'settings/importlists/testAllImportLists';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetchImportLists = createThunk(FETCH_IMPORT_LISTS);
|
||||||
|
export const fetchImportListSchema = createThunk(FETCH_IMPORT_LIST_SCHEMA);
|
||||||
|
export const selectImportListSchema = createAction(SELECT_IMPORT_LIST_SCHEMA);
|
||||||
|
|
||||||
|
export const saveImportList = createThunk(SAVE_IMPORT_LIST);
|
||||||
|
export const cancelSaveImportList = createThunk(CANCEL_SAVE_IMPORT_LIST);
|
||||||
|
export const deleteImportList = createThunk(DELETE_IMPORT_LIST);
|
||||||
|
export const testImportList = createThunk(TEST_IMPORT_LIST);
|
||||||
|
export const cancelTestImportList = createThunk(CANCEL_TEST_IMPORT_LIST);
|
||||||
|
export const testAllImportLists = createThunk(TEST_ALL_IMPORT_LISTS);
|
||||||
|
|
||||||
|
export const setImportListValue = createAction(SET_IMPORT_LIST_VALUE, (payload) => {
|
||||||
|
return {
|
||||||
|
section,
|
||||||
|
...payload
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setImportListFieldValue = createAction(SET_IMPORT_LIST_FIELD_VALUE, (payload) => {
|
||||||
|
return {
|
||||||
|
section,
|
||||||
|
...payload
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// Details
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
defaultState: {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
isSchemaFetching: false,
|
||||||
|
isSchemaPopulated: false,
|
||||||
|
schemaError: null,
|
||||||
|
schema: [],
|
||||||
|
selectedSchema: {},
|
||||||
|
isSaving: false,
|
||||||
|
saveError: null,
|
||||||
|
isTesting: false,
|
||||||
|
isTestingAll: false,
|
||||||
|
items: [],
|
||||||
|
pendingChanges: {}
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
actionHandlers: {
|
||||||
|
[FETCH_IMPORT_LISTS]: createFetchHandler(section, '/importlist'),
|
||||||
|
[FETCH_IMPORT_LIST_SCHEMA]: createFetchSchemaHandler(section, '/importlist/schema'),
|
||||||
|
|
||||||
|
[SAVE_IMPORT_LIST]: createSaveProviderHandler(section, '/importlist'),
|
||||||
|
[CANCEL_SAVE_IMPORT_LIST]: createCancelSaveProviderHandler(section),
|
||||||
|
[DELETE_IMPORT_LIST]: createRemoveItemHandler(section, '/importlist'),
|
||||||
|
[TEST_IMPORT_LIST]: createTestProviderHandler(section, '/importlist'),
|
||||||
|
[CANCEL_TEST_IMPORT_LIST]: createCancelTestProviderHandler(section),
|
||||||
|
[TEST_ALL_IMPORT_LISTS]: createTestAllProvidersHandler(section, '/importlist')
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
reducers: {
|
||||||
|
[SET_IMPORT_LIST_VALUE]: createSetSettingValueReducer(section),
|
||||||
|
[SET_IMPORT_LIST_FIELD_VALUE]: createSetProviderFieldValueReducer(section),
|
||||||
|
|
||||||
|
[SELECT_IMPORT_LIST_SCHEMA]: (state, { payload }) => {
|
||||||
|
return selectProviderSchema(state, section, payload, (selectedSchema) => {
|
||||||
|
selectedSchema.enableAutomaticAdd = true;
|
||||||
|
selectedSchema.shouldMonitor = 'all';
|
||||||
|
|
||||||
|
return selectedSchema;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
|
@ -199,7 +199,8 @@ export const deleteSeries = createThunk(DELETE_SERIES, (payload) => {
|
||||||
return {
|
return {
|
||||||
...payload,
|
...payload,
|
||||||
queryParams: {
|
queryParams: {
|
||||||
deleteFiles: payload.deleteFiles
|
deleteFiles: payload.deleteFiles,
|
||||||
|
addImportListExclusion: payload.addImportListExclusion
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,6 +5,8 @@ import delayProfiles from './Settings/delayProfiles';
|
||||||
import downloadClients from './Settings/downloadClients';
|
import downloadClients from './Settings/downloadClients';
|
||||||
import downloadClientOptions from './Settings/downloadClientOptions';
|
import downloadClientOptions from './Settings/downloadClientOptions';
|
||||||
import general from './Settings/general';
|
import general from './Settings/general';
|
||||||
|
import importLists from './Settings/importLists';
|
||||||
|
import importListExclusions from './Settings/importListExclusions';
|
||||||
import indexerOptions from './Settings/indexerOptions';
|
import indexerOptions from './Settings/indexerOptions';
|
||||||
import indexers from './Settings/indexers';
|
import indexers from './Settings/indexers';
|
||||||
import languageProfiles from './Settings/languageProfiles';
|
import languageProfiles from './Settings/languageProfiles';
|
||||||
|
@ -23,6 +25,8 @@ export * from './Settings/delayProfiles';
|
||||||
export * from './Settings/downloadClients';
|
export * from './Settings/downloadClients';
|
||||||
export * from './Settings/downloadClientOptions';
|
export * from './Settings/downloadClientOptions';
|
||||||
export * from './Settings/general';
|
export * from './Settings/general';
|
||||||
|
export * from './Settings/importLists';
|
||||||
|
export * from './Settings/importListExclusions';
|
||||||
export * from './Settings/indexerOptions';
|
export * from './Settings/indexerOptions';
|
||||||
export * from './Settings/indexers';
|
export * from './Settings/indexers';
|
||||||
export * from './Settings/languageProfiles';
|
export * from './Settings/languageProfiles';
|
||||||
|
@ -52,6 +56,8 @@ export const defaultState = {
|
||||||
downloadClients: downloadClients.defaultState,
|
downloadClients: downloadClients.defaultState,
|
||||||
downloadClientOptions: downloadClientOptions.defaultState,
|
downloadClientOptions: downloadClientOptions.defaultState,
|
||||||
general: general.defaultState,
|
general: general.defaultState,
|
||||||
|
importLists: importLists.defaultState,
|
||||||
|
importListExclusions: importListExclusions.defaultState,
|
||||||
indexerOptions: indexerOptions.defaultState,
|
indexerOptions: indexerOptions.defaultState,
|
||||||
indexers: indexers.defaultState,
|
indexers: indexers.defaultState,
|
||||||
languageProfiles: languageProfiles.defaultState,
|
languageProfiles: languageProfiles.defaultState,
|
||||||
|
@ -89,6 +95,8 @@ export const actionHandlers = handleThunks({
|
||||||
...downloadClients.actionHandlers,
|
...downloadClients.actionHandlers,
|
||||||
...downloadClientOptions.actionHandlers,
|
...downloadClientOptions.actionHandlers,
|
||||||
...general.actionHandlers,
|
...general.actionHandlers,
|
||||||
|
...importLists.actionHandlers,
|
||||||
|
...importListExclusions.actionHandlers,
|
||||||
...indexerOptions.actionHandlers,
|
...indexerOptions.actionHandlers,
|
||||||
...indexers.actionHandlers,
|
...indexers.actionHandlers,
|
||||||
...languageProfiles.actionHandlers,
|
...languageProfiles.actionHandlers,
|
||||||
|
@ -117,6 +125,8 @@ export const reducers = createHandleActions({
|
||||||
...downloadClients.reducers,
|
...downloadClients.reducers,
|
||||||
...downloadClientOptions.reducers,
|
...downloadClientOptions.reducers,
|
||||||
...general.reducers,
|
...general.reducers,
|
||||||
|
...importLists.reducers,
|
||||||
|
...importListExclusions.reducers,
|
||||||
...indexerOptions.reducers,
|
...indexerOptions.reducers,
|
||||||
...indexers.reducers,
|
...indexers.reducers,
|
||||||
...languageProfiles.reducers,
|
...languageProfiles.reducers,
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createAllSeriesSelector from './createAllSeriesSelector';
|
import createAllSeriesSelector from './createAllSeriesSelector';
|
||||||
|
|
||||||
|
@ -6,12 +5,13 @@ function createProfileInUseSelector(profileProp) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state, { id }) => id,
|
(state, { id }) => id,
|
||||||
createAllSeriesSelector(),
|
createAllSeriesSelector(),
|
||||||
(id, series) => {
|
(state) => state.settings.importLists.items,
|
||||||
|
(id, series, lists) => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _.some(series, { [profileProp]: id });
|
return series.some((s) => s[profileProp] === id) || lists.some((list) => list[profileProp] === id);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.HealthCheck.Checks;
|
||||||
|
using NzbDrone.Core.ImportLists;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class ImportListStatusCheckFixture : CoreTest<ImportListStatusCheck>
|
||||||
|
{
|
||||||
|
private List<IImportList> _importLists = new List<IImportList>();
|
||||||
|
private List<ImportListStatus> _blockedImportLists = new List<ImportListStatus>();
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IImportListFactory>()
|
||||||
|
.Setup(v => v.GetAvailableProviders())
|
||||||
|
.Returns(_importLists);
|
||||||
|
|
||||||
|
Mocker.GetMock<IImportListStatusService>()
|
||||||
|
.Setup(v => v.GetBlockedProviders())
|
||||||
|
.Returns(_blockedImportLists);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mock<IImportList> GivenImportList(int id, double backoffHours, double failureHours)
|
||||||
|
{
|
||||||
|
var mockImportList = new Mock<IImportList>();
|
||||||
|
mockImportList.SetupGet(s => s.Definition).Returns(new ImportListDefinition { Id = id });
|
||||||
|
|
||||||
|
_importLists.Add(mockImportList.Object);
|
||||||
|
|
||||||
|
if (backoffHours != 0.0)
|
||||||
|
{
|
||||||
|
_blockedImportLists.Add(new ImportListStatus
|
||||||
|
{
|
||||||
|
ProviderId = id,
|
||||||
|
InitialFailure = DateTime.UtcNow.AddHours(-failureHours),
|
||||||
|
MostRecentFailure = DateTime.UtcNow.AddHours(-0.1),
|
||||||
|
EscalationLevel = 5,
|
||||||
|
DisabledTill = DateTime.UtcNow.AddHours(backoffHours)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return mockImportList;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_return_error_when_no_import_lists()
|
||||||
|
{
|
||||||
|
Subject.Check().ShouldBeOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_warning_if_import_list_unavailable()
|
||||||
|
{
|
||||||
|
GivenImportList(1, 10.0, 24.0);
|
||||||
|
GivenImportList(2, 0.0, 0.0);
|
||||||
|
|
||||||
|
Subject.Check().ShouldBeWarning();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_error_if_all_import_lists_unavailable()
|
||||||
|
{
|
||||||
|
GivenImportList(1, 10.0, 24.0);
|
||||||
|
|
||||||
|
Subject.Check().ShouldBeError();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_return_warning_if_few_import_lists_unavailable()
|
||||||
|
{
|
||||||
|
GivenImportList(1, 10.0, 24.0);
|
||||||
|
GivenImportList(2, 10.0, 24.0);
|
||||||
|
GivenImportList(3, 0.0, 0.0);
|
||||||
|
|
||||||
|
Subject.Check().ShouldBeWarning();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,10 +26,8 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
|
||||||
.Returns(_blockedIndexers);
|
.Returns(_blockedIndexers);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mock<IIndexer> GivenIndexer(int i, double backoffHours, double failureHours)
|
private Mock<IIndexer> GivenIndexer(int id, double backoffHours, double failureHours)
|
||||||
{
|
{
|
||||||
var id = i;
|
|
||||||
|
|
||||||
var mockIndexer = new Mock<IIndexer>();
|
var mockIndexer = new Mock<IIndexer>();
|
||||||
mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition { Id = id });
|
mockIndexer.SetupGet(s => s.Definition).Returns(new IndexerDefinition { Id = id });
|
||||||
mockIndexer.SetupGet(s => s.SupportsSearch).Returns(true);
|
mockIndexer.SetupGet(s => s.SupportsSearch).Returns(true);
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
using FizzWare.NBuilder;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.Housekeeping.Housekeepers;
|
||||||
|
using NzbDrone.Core.ImportLists;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class CleanupOrphanedImportListFixture : DbTest<CleanupOrphanedImportListStatus, ImportListStatus>
|
||||||
|
{
|
||||||
|
private ImportListDefinition _importList;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_importList = Builder<ImportListDefinition>.CreateNew()
|
||||||
|
.BuildNew();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GivenIndexer()
|
||||||
|
{
|
||||||
|
Db.Insert(_importList);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_delete_orphaned_indexerstatus()
|
||||||
|
{
|
||||||
|
var status = Builder<ImportListStatus>.CreateNew()
|
||||||
|
.With(h => h.ProviderId = _importList.Id)
|
||||||
|
.BuildNew();
|
||||||
|
Db.Insert(status);
|
||||||
|
|
||||||
|
Subject.Clean();
|
||||||
|
AllStoredModels.Should().BeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_delete_unorphaned_indexerstatus()
|
||||||
|
{
|
||||||
|
GivenIndexer();
|
||||||
|
|
||||||
|
var status = Builder<ImportListStatus>.CreateNew()
|
||||||
|
.With(h => h.ProviderId = _importList.Id)
|
||||||
|
.BuildNew();
|
||||||
|
Db.Insert(status);
|
||||||
|
|
||||||
|
Subject.Clean();
|
||||||
|
AllStoredModels.Should().HaveCount(1);
|
||||||
|
AllStoredModels.Should().Contain(h => h.ProviderId == _importList.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
using NzbDrone.Core.ImportLists;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.ImportListTests
|
||||||
|
{
|
||||||
|
public class ImportListStatusServiceFixture : CoreTest<ImportListStatusService>
|
||||||
|
{
|
||||||
|
private DateTime _epoch;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
_epoch = DateTime.UtcNow;
|
||||||
|
|
||||||
|
Mocker.GetMock<IRuntimeInfo>()
|
||||||
|
.SetupGet(v => v.StartTime)
|
||||||
|
.Returns(_epoch - TimeSpan.FromHours(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WithStatus(ImportListStatus status)
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IImportListStatusRepository>()
|
||||||
|
.Setup(v => v.FindByProviderId(1))
|
||||||
|
.Returns(status);
|
||||||
|
|
||||||
|
Mocker.GetMock<IImportListStatusRepository>()
|
||||||
|
.Setup(v => v.All())
|
||||||
|
.Returns(new[] { status });
|
||||||
|
}
|
||||||
|
|
||||||
|
private void VerifyUpdate()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IImportListStatusRepository>()
|
||||||
|
.Verify(v => v.Upsert(It.IsAny<ImportListStatus>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void VerifyNoUpdate()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IImportListStatusRepository>()
|
||||||
|
.Verify(v => v.Upsert(It.IsAny<ImportListStatus>()), Times.Never());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_cancel_backoff_on_success()
|
||||||
|
{
|
||||||
|
WithStatus(new ImportListStatus { EscalationLevel = 2 });
|
||||||
|
|
||||||
|
Subject.RecordSuccess(1);
|
||||||
|
|
||||||
|
VerifyUpdate();
|
||||||
|
|
||||||
|
var status = Subject.GetBlockedProviders().FirstOrDefault();
|
||||||
|
status.Should().BeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_store_update_if_already_okay()
|
||||||
|
{
|
||||||
|
WithStatus(new ImportListStatus { EscalationLevel = 0 });
|
||||||
|
|
||||||
|
Subject.RecordSuccess(1);
|
||||||
|
|
||||||
|
VerifyNoUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,136 @@
|
||||||
|
using System.Linq;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Moq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.ImportLists;
|
||||||
|
using NzbDrone.Core.MetadataSource;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
using NzbDrone.Core.ImportLists.Exclusions;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.ImportListTests
|
||||||
|
{
|
||||||
|
public class ImportListSyncServiceFixture : CoreTest<ImportListSyncService>
|
||||||
|
{
|
||||||
|
private List<ImportListItemInfo> _importListReports;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
var importListItem1 = new ImportListItemInfo
|
||||||
|
{
|
||||||
|
Title = "Breaking Bad"
|
||||||
|
};
|
||||||
|
|
||||||
|
_importListReports = new List<ImportListItemInfo>{importListItem1};
|
||||||
|
|
||||||
|
Mocker.GetMock<IFetchAndParseImportList>()
|
||||||
|
.Setup(v => v.Fetch())
|
||||||
|
.Returns(_importListReports);
|
||||||
|
|
||||||
|
Mocker.GetMock<ISearchForNewSeries>()
|
||||||
|
.Setup(v => v.SearchForNewSeries(It.IsAny<string>()))
|
||||||
|
.Returns(new List<Series>());
|
||||||
|
|
||||||
|
Mocker.GetMock<IImportListFactory>()
|
||||||
|
.Setup(v => v.Get(It.IsAny<int>()))
|
||||||
|
.Returns(new ImportListDefinition{ ShouldMonitor = MonitorTypes.All });
|
||||||
|
|
||||||
|
Mocker.GetMock<IFetchAndParseImportList>()
|
||||||
|
.Setup(v => v.Fetch())
|
||||||
|
.Returns(_importListReports);
|
||||||
|
|
||||||
|
Mocker.GetMock<IImportListExclusionService>()
|
||||||
|
.Setup(v => v.All())
|
||||||
|
.Returns(new List<ImportListExclusion>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WithTvdbId()
|
||||||
|
{
|
||||||
|
_importListReports.First().TvdbId = 81189;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WithExistingSeries()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<ISeriesService>()
|
||||||
|
.Setup(v => v.FindByTvdbId(_importListReports.First().TvdbId))
|
||||||
|
.Returns(new Series{TvdbId = _importListReports.First().TvdbId });
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WithExcludedSeries()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IImportListExclusionService>()
|
||||||
|
.Setup(v => v.All())
|
||||||
|
.Returns(new List<ImportListExclusion> {
|
||||||
|
new ImportListExclusion {
|
||||||
|
TvdbId = 81189
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WithMonitorType(MonitorTypes monitor)
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IImportListFactory>()
|
||||||
|
.Setup(v => v.Get(It.IsAny<int>()))
|
||||||
|
.Returns(new ImportListDefinition{ ShouldMonitor = monitor });
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_search_if_series_title_and_no_series_id()
|
||||||
|
{
|
||||||
|
Subject.Execute(new ImportListSyncCommand());
|
||||||
|
|
||||||
|
Mocker.GetMock<ISearchForNewSeries>()
|
||||||
|
.Verify(v => v.SearchForNewSeries(It.IsAny<string>()), Times.Once());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_search_if_series_title_and_series_id()
|
||||||
|
{
|
||||||
|
WithTvdbId();
|
||||||
|
Subject.Execute(new ImportListSyncCommand());
|
||||||
|
|
||||||
|
Mocker.GetMock<ISearchForNewSeries>()
|
||||||
|
.Verify(v => v.SearchForNewSeries(It.IsAny<string>()), Times.Never());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_add_if_existing_series()
|
||||||
|
{
|
||||||
|
WithTvdbId();
|
||||||
|
WithExistingSeries();
|
||||||
|
|
||||||
|
Subject.Execute(new ImportListSyncCommand());
|
||||||
|
|
||||||
|
Mocker.GetMock<IAddSeriesService>()
|
||||||
|
.Verify(v => v.AddSeries(It.Is<List<Series>>(t=>t.Count == 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(MonitorTypes.None, false)]
|
||||||
|
[TestCase(MonitorTypes.All, true)]
|
||||||
|
public void should_add_if_not_existing_series(MonitorTypes monitor, bool expectedSeriesMonitored)
|
||||||
|
{
|
||||||
|
WithTvdbId();
|
||||||
|
WithMonitorType(monitor);
|
||||||
|
|
||||||
|
Subject.Execute(new ImportListSyncCommand());
|
||||||
|
|
||||||
|
Mocker.GetMock<IAddSeriesService>()
|
||||||
|
.Verify(v => v.AddSeries(It.Is<List<Series>>(t => t.Count == 1 && t.First().Monitored == expectedSeriesMonitored)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_add_if_excluded_series()
|
||||||
|
{
|
||||||
|
WithTvdbId();
|
||||||
|
WithExcludedSeries();
|
||||||
|
|
||||||
|
Subject.Execute(new ImportListSyncCommand());
|
||||||
|
|
||||||
|
Mocker.GetMock<IAddSeriesService>()
|
||||||
|
.Verify(v => v.AddSeries(It.Is<List<Series>>(t => t.Count == 0)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
using FluentMigrator;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
{
|
||||||
|
[Migration(142)]
|
||||||
|
public class import_lists : NzbDroneMigrationBase
|
||||||
|
{
|
||||||
|
protected override void MainDbUpgrade()
|
||||||
|
{
|
||||||
|
Create.TableForModel("ImportLists")
|
||||||
|
.WithColumn("Name").AsString().Unique()
|
||||||
|
.WithColumn("Implementation").AsString()
|
||||||
|
.WithColumn("Settings").AsString().Nullable()
|
||||||
|
.WithColumn("ConfigContract").AsString().Nullable()
|
||||||
|
.WithColumn("EnableAutomaticAdd").AsBoolean().Nullable()
|
||||||
|
.WithColumn("RootFolderPath").AsString()
|
||||||
|
.WithColumn("ShouldMonitor").AsInt32()
|
||||||
|
.WithColumn("QualityProfileId").AsInt32()
|
||||||
|
.WithColumn("LanguageProfileId").AsInt32()
|
||||||
|
.WithColumn("Tags").AsString().Nullable();
|
||||||
|
|
||||||
|
Create.TableForModel("ImportListStatus")
|
||||||
|
.WithColumn("ProviderId").AsInt32().NotNullable().Unique()
|
||||||
|
.WithColumn("InitialFailure").AsDateTime().Nullable()
|
||||||
|
.WithColumn("MostRecentFailure").AsDateTime().Nullable()
|
||||||
|
.WithColumn("EscalationLevel").AsInt32().NotNullable()
|
||||||
|
.WithColumn("DisabledTill").AsDateTime().Nullable()
|
||||||
|
.WithColumn("LastSyncListInfo").AsString().Nullable();
|
||||||
|
|
||||||
|
Create.TableForModel("ImportListExclusions")
|
||||||
|
.WithColumn("TvdbId").AsString().NotNullable().Unique()
|
||||||
|
.WithColumn("Title").AsString().NotNullable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,6 +40,8 @@ using NzbDrone.Core.Languages;
|
||||||
using NzbDrone.Core.Profiles.Languages;
|
using NzbDrone.Core.Profiles.Languages;
|
||||||
using NzbDrone.Core.Profiles.Releases;
|
using NzbDrone.Core.Profiles.Releases;
|
||||||
using NzbDrone.Core.Update.History;
|
using NzbDrone.Core.Update.History;
|
||||||
|
using NzbDrone.Core.ImportLists.Exclusions;
|
||||||
|
using NzbDrone.Core.ImportLists;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore
|
namespace NzbDrone.Core.Datastore
|
||||||
{
|
{
|
||||||
|
@ -81,6 +83,10 @@ namespace NzbDrone.Core.Datastore
|
||||||
.Ignore(d => d.Protocol)
|
.Ignore(d => d.Protocol)
|
||||||
.Ignore(d => d.Tags);
|
.Ignore(d => d.Tags);
|
||||||
|
|
||||||
|
Mapper.Entity<ImportListDefinition>().RegisterDefinition("ImportLists")
|
||||||
|
.Ignore(i => i.ListType)
|
||||||
|
.Ignore(i => i.Enable);
|
||||||
|
|
||||||
Mapper.Entity<SceneMapping>().RegisterModel("SceneMappings");
|
Mapper.Entity<SceneMapping>().RegisterModel("SceneMappings");
|
||||||
|
|
||||||
Mapper.Entity<EpisodeHistory>().RegisterModel("History")
|
Mapper.Entity<EpisodeHistory>().RegisterModel("History")
|
||||||
|
@ -135,6 +141,7 @@ namespace NzbDrone.Core.Datastore
|
||||||
|
|
||||||
Mapper.Entity<IndexerStatus>().RegisterModel("IndexerStatus");
|
Mapper.Entity<IndexerStatus>().RegisterModel("IndexerStatus");
|
||||||
Mapper.Entity<DownloadClientStatus>().RegisterModel("DownloadClientStatus");
|
Mapper.Entity<DownloadClientStatus>().RegisterModel("DownloadClientStatus");
|
||||||
|
Mapper.Entity<ImportListStatus>().RegisterModel("ImportListStatus");
|
||||||
|
|
||||||
Mapper.Entity<CustomFilter>().RegisterModel("CustomFilters");
|
Mapper.Entity<CustomFilter>().RegisterModel("CustomFilters");
|
||||||
|
|
||||||
|
@ -142,6 +149,7 @@ namespace NzbDrone.Core.Datastore
|
||||||
.AutoMapChildModels();
|
.AutoMapChildModels();
|
||||||
|
|
||||||
Mapper.Entity<UpdateHistory>().RegisterModel("UpdateHistory");
|
Mapper.Entity<UpdateHistory>().RegisterModel("UpdateHistory");
|
||||||
|
Mapper.Entity<ImportListExclusion>().RegisterModel("ImportListExclusions");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RegisterMappers()
|
private static void RegisterMappers()
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
using System.Linq;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.ImportLists;
|
||||||
|
using NzbDrone.Core.ThingiProvider.Events;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.HealthCheck.Checks
|
||||||
|
{
|
||||||
|
[CheckOn(typeof(ProviderUpdatedEvent<IImportList>))]
|
||||||
|
[CheckOn(typeof(ProviderDeletedEvent<IImportList>))]
|
||||||
|
[CheckOn(typeof(ProviderStatusChangedEvent<IImportList>))]
|
||||||
|
public class ImportListStatusCheck : HealthCheckBase
|
||||||
|
{
|
||||||
|
private readonly IImportListFactory _providerFactory;
|
||||||
|
private readonly IImportListStatusService _providerStatusService;
|
||||||
|
|
||||||
|
public ImportListStatusCheck(IImportListFactory providerFactory, IImportListStatusService providerStatusService)
|
||||||
|
{
|
||||||
|
_providerFactory = providerFactory;
|
||||||
|
_providerStatusService = providerStatusService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override HealthCheck Check()
|
||||||
|
{
|
||||||
|
var enabledProviders = _providerFactory.GetAvailableProviders();
|
||||||
|
var backOffProviders = enabledProviders.Join(_providerStatusService.GetBlockedProviders(),
|
||||||
|
i => i.Definition.Id,
|
||||||
|
s => s.ProviderId,
|
||||||
|
(i, s) => new { ImportList = i, Status = s })
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (backOffProviders.Empty())
|
||||||
|
{
|
||||||
|
return new HealthCheck(GetType());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backOffProviders.Count == enabledProviders.Count)
|
||||||
|
{
|
||||||
|
return new HealthCheck(GetType(), HealthCheckResult.Error, "All import lists are unavailable due to failures", "#import-lists-are-unavailable-due-to-failures");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HealthCheck(GetType(), HealthCheckResult.Warning, string.Format("Import lists unavailable due to failures: {0}", string.Join(", ", backOffProviders.Select(v => v.ImportList.Definition.Name))), "#import-lsits-are-unavailable-due-to-failures");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Housekeeping.Housekeepers
|
||||||
|
{
|
||||||
|
public class CleanupOrphanedImportListStatus : IHousekeepingTask
|
||||||
|
{
|
||||||
|
private readonly IMainDatabase _database;
|
||||||
|
|
||||||
|
public CleanupOrphanedImportListStatus(IMainDatabase database)
|
||||||
|
{
|
||||||
|
_database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clean()
|
||||||
|
{
|
||||||
|
var mapper = _database.GetDataMapper();
|
||||||
|
|
||||||
|
mapper.ExecuteNonQuery(@"DELETE FROM ImportListStatus
|
||||||
|
WHERE Id IN (
|
||||||
|
SELECT ImportListStatus.Id FROM ImportListStatus
|
||||||
|
LEFT OUTER JOIN ImportLists
|
||||||
|
ON ImportListStatus.ProviderId = ImportLists.Id
|
||||||
|
WHERE ImportLists.Id IS NULL)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
using NzbDrone.Common.Exceptions;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists.Exceptions
|
||||||
|
{
|
||||||
|
public class ImportListException : NzbDroneException
|
||||||
|
{
|
||||||
|
private readonly ImportListResponse _importListResponse;
|
||||||
|
|
||||||
|
public ImportListException(ImportListResponse response, string message, params object[] args)
|
||||||
|
: base(message, args)
|
||||||
|
{
|
||||||
|
_importListResponse = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImportListException(ImportListResponse response, string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
_importListResponse = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImportListResponse Response => _importListResponse;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists.Exclusions
|
||||||
|
{
|
||||||
|
public class ImportListExclusion : ModelBase
|
||||||
|
{
|
||||||
|
public int TvdbId { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
using FluentValidation.Validators;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists.Exclusions
|
||||||
|
{
|
||||||
|
public class ImportListExclusionExistsValidator : PropertyValidator
|
||||||
|
{
|
||||||
|
private readonly IImportListExclusionService _importListExclusionService;
|
||||||
|
|
||||||
|
public ImportListExclusionExistsValidator(IImportListExclusionService importListExclusionService)
|
||||||
|
: base("This exclusion has already been added.")
|
||||||
|
{
|
||||||
|
_importListExclusionService = importListExclusionService;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid(PropertyValidatorContext context)
|
||||||
|
{
|
||||||
|
if (context.PropertyValue == null) return true;
|
||||||
|
|
||||||
|
return (!_importListExclusionService.All().Exists(s => s.TvdbId == (int)context.PropertyValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists.Exclusions
|
||||||
|
{
|
||||||
|
public interface IImportListExclusionRepository : IBasicRepository<ImportListExclusion>
|
||||||
|
{
|
||||||
|
ImportListExclusion FindByTvdbId(int tvdbId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ImportListExclusionRepository : BasicRepository<ImportListExclusion>, IImportListExclusionRepository
|
||||||
|
{
|
||||||
|
public ImportListExclusionRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||||
|
: base(database, eventAggregator)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImportListExclusion FindByTvdbId(int tvdbId)
|
||||||
|
{
|
||||||
|
return Query.Where<ImportListExclusion>(m => m.TvdbId == tvdbId).SingleOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
using NzbDrone.Core.Tv.Events;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists.Exclusions
|
||||||
|
{
|
||||||
|
public interface IImportListExclusionService
|
||||||
|
{
|
||||||
|
ImportListExclusion Add(ImportListExclusion importListExclusion);
|
||||||
|
List<ImportListExclusion> All();
|
||||||
|
void Delete(int id);
|
||||||
|
ImportListExclusion Get(int id);
|
||||||
|
ImportListExclusion FindByTvdbId(int tvdbId);
|
||||||
|
ImportListExclusion Update(ImportListExclusion importListExclusion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ImportListExclusionService : IImportListExclusionService, IHandleAsync<SeriesDeletedEvent>
|
||||||
|
{
|
||||||
|
private readonly IImportListExclusionRepository _repo;
|
||||||
|
|
||||||
|
public ImportListExclusionService(IImportListExclusionRepository repo)
|
||||||
|
{
|
||||||
|
_repo = repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImportListExclusion Add(ImportListExclusion importListExclusion)
|
||||||
|
{
|
||||||
|
return _repo.Insert(importListExclusion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImportListExclusion Update(ImportListExclusion importListExclusion)
|
||||||
|
{
|
||||||
|
return _repo.Update(importListExclusion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Delete(int id)
|
||||||
|
{
|
||||||
|
_repo.Delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImportListExclusion Get(int id)
|
||||||
|
{
|
||||||
|
return _repo.Get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImportListExclusion FindByTvdbId(int tvdbId)
|
||||||
|
{
|
||||||
|
return _repo.FindByTvdbId(tvdbId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ImportListExclusion> All()
|
||||||
|
{
|
||||||
|
return _repo.All().ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HandleAsync(SeriesDeletedEvent message)
|
||||||
|
{
|
||||||
|
if (!message.AddImportListExclusion)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var existingExclusion = _repo.FindByTvdbId(message.Series.TvdbId);
|
||||||
|
|
||||||
|
if (existingExclusion != null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var importExclusion = new ImportListExclusion
|
||||||
|
{
|
||||||
|
TvdbId = message.Series.TvdbId,
|
||||||
|
Title = message.Series.Title
|
||||||
|
};
|
||||||
|
|
||||||
|
_repo.Insert(importExclusion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Common.TPL;
|
||||||
|
using System;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists
|
||||||
|
{
|
||||||
|
public interface IFetchAndParseImportList
|
||||||
|
{
|
||||||
|
List<ImportListItemInfo> Fetch();
|
||||||
|
List<ImportListItemInfo> FetchSingleList(ImportListDefinition definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FetchAndParseImportListService : IFetchAndParseImportList
|
||||||
|
{
|
||||||
|
private readonly IImportListFactory _importListFactory;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
public FetchAndParseImportListService(IImportListFactory importListFactory, Logger logger)
|
||||||
|
{
|
||||||
|
_importListFactory = importListFactory;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ImportListItemInfo> Fetch()
|
||||||
|
{
|
||||||
|
var result = new List<ImportListItemInfo>();
|
||||||
|
|
||||||
|
var importLists = _importListFactory.AutomaticAddEnabled();
|
||||||
|
|
||||||
|
if (!importLists.Any())
|
||||||
|
{
|
||||||
|
_logger.Warn("No available import lists. check your configuration.");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Debug("Available import lists {0}", importLists.Count);
|
||||||
|
|
||||||
|
var taskList = new List<Task>();
|
||||||
|
var taskFactory = new TaskFactory(TaskCreationOptions.LongRunning, TaskContinuationOptions.None);
|
||||||
|
|
||||||
|
foreach (var importList in importLists)
|
||||||
|
{
|
||||||
|
var importListLocal = importList;
|
||||||
|
|
||||||
|
var task = taskFactory.StartNew(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var importListReports = importListLocal.Fetch();
|
||||||
|
|
||||||
|
lock (result)
|
||||||
|
{
|
||||||
|
_logger.Debug("Found {0} from {1}", importListReports.Count, importList.Name);
|
||||||
|
|
||||||
|
result.AddRange(importListReports);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.Error(e, "Error during Import List Sync");
|
||||||
|
}
|
||||||
|
}).LogExceptions();
|
||||||
|
|
||||||
|
taskList.Add(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
Task.WaitAll(taskList.ToArray());
|
||||||
|
|
||||||
|
result = result.DistinctBy(r => new {r.TvdbId, r.Title}).ToList();
|
||||||
|
|
||||||
|
_logger.Debug("Found {0} reports", result.Count);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ImportListItemInfo> FetchSingleList(ImportListDefinition definition)
|
||||||
|
{
|
||||||
|
var result = new List<ImportListItemInfo>();
|
||||||
|
|
||||||
|
var importList = _importListFactory.GetInstance(definition);
|
||||||
|
|
||||||
|
if (importList == null || !definition.EnableAutomaticAdd)
|
||||||
|
{
|
||||||
|
_logger.Warn("No available import lists. check your configuration.");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
var taskList = new List<Task>();
|
||||||
|
var taskFactory = new TaskFactory(TaskCreationOptions.LongRunning, TaskContinuationOptions.None);
|
||||||
|
|
||||||
|
var importListLocal = importList;
|
||||||
|
|
||||||
|
var task = taskFactory.StartNew(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var importListReports = importListLocal.Fetch();
|
||||||
|
|
||||||
|
lock (result)
|
||||||
|
{
|
||||||
|
_logger.Debug("Found {0} from {1}", importListReports.Count, importList.Name);
|
||||||
|
|
||||||
|
result.AddRange(importListReports);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.Error(e, "Error during Import List Sync");
|
||||||
|
}
|
||||||
|
}).LogExceptions();
|
||||||
|
|
||||||
|
taskList.Add(task);
|
||||||
|
|
||||||
|
|
||||||
|
Task.WaitAll(taskList.ToArray());
|
||||||
|
|
||||||
|
result = result.DistinctBy(r => new { r.TvdbId, r.Title }).ToList();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,245 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using FluentValidation.Results;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
using NzbDrone.Core.Http.CloudFlare;
|
||||||
|
using NzbDrone.Core.ImportLists.Exceptions;
|
||||||
|
using NzbDrone.Core.Indexers.Exceptions;
|
||||||
|
using NzbDrone.Core.Parser;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists
|
||||||
|
{
|
||||||
|
public abstract class HttpImportListBase<TSettings> : ImportListBase<TSettings>
|
||||||
|
where TSettings : IImportListSettings, new()
|
||||||
|
{
|
||||||
|
protected const int MaxNumResultsPerQuery = 1000;
|
||||||
|
|
||||||
|
protected readonly IHttpClient _httpClient;
|
||||||
|
|
||||||
|
public bool SupportsPaging => PageSize > 0;
|
||||||
|
|
||||||
|
public virtual int PageSize => 0;
|
||||||
|
public virtual TimeSpan RateLimit => TimeSpan.FromSeconds(2);
|
||||||
|
|
||||||
|
public abstract IImportListRequestGenerator GetRequestGenerator();
|
||||||
|
public abstract IParseImportListResponse GetParser();
|
||||||
|
|
||||||
|
public HttpImportListBase(IHttpClient httpClient, IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, Logger logger)
|
||||||
|
: base(importListStatusService, configService, parsingService, logger)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IList<ImportListItemInfo> Fetch()
|
||||||
|
{
|
||||||
|
return FetchItems(g => g.GetListItems(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual IList<ImportListItemInfo> FetchItems(Func<IImportListRequestGenerator, ImportListPageableRequestChain> pageableRequestChainSelector, bool isRecent = false)
|
||||||
|
{
|
||||||
|
var releases = new List<ImportListItemInfo>();
|
||||||
|
var url = string.Empty;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var generator = GetRequestGenerator();
|
||||||
|
var parser = GetParser();
|
||||||
|
|
||||||
|
var pageableRequestChain = pageableRequestChainSelector(generator);
|
||||||
|
|
||||||
|
for (int i = 0; i < pageableRequestChain.Tiers; i++)
|
||||||
|
{
|
||||||
|
var pageableRequests = pageableRequestChain.GetTier(i);
|
||||||
|
|
||||||
|
foreach (var pageableRequest in pageableRequests)
|
||||||
|
{
|
||||||
|
var pagedReleases = new List<ImportListItemInfo>();
|
||||||
|
|
||||||
|
foreach (var request in pageableRequest)
|
||||||
|
{
|
||||||
|
url = request.Url.FullUri;
|
||||||
|
|
||||||
|
var page = FetchPage(request, parser);
|
||||||
|
|
||||||
|
pagedReleases.AddRange(page);
|
||||||
|
|
||||||
|
if (pagedReleases.Count >= MaxNumResultsPerQuery)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsFullPage(page))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
releases.AddRange(pagedReleases.Where(IsValidItem));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (releases.Any())
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_importListStatusService.RecordSuccess(Definition.Id);
|
||||||
|
}
|
||||||
|
catch (WebException webException)
|
||||||
|
{
|
||||||
|
if (webException.Status == WebExceptionStatus.NameResolutionFailure ||
|
||||||
|
webException.Status == WebExceptionStatus.ConnectFailure)
|
||||||
|
{
|
||||||
|
_importListStatusService.RecordConnectionFailure(Definition.Id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_importListStatusService.RecordFailure(Definition.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (webException.Message.Contains("502") || webException.Message.Contains("503") ||
|
||||||
|
webException.Message.Contains("timed out"))
|
||||||
|
{
|
||||||
|
_logger.Warn("{0} server is currently unavailable. {1} {2}", this, url, webException.Message);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Warn("{0} {1} {2}", this, url, webException.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (TooManyRequestsException ex)
|
||||||
|
{
|
||||||
|
if (ex.RetryAfter != TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
_importListStatusService.RecordFailure(Definition.Id, ex.RetryAfter);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_importListStatusService.RecordFailure(Definition.Id, TimeSpan.FromHours(1));
|
||||||
|
}
|
||||||
|
_logger.Warn("API Request Limit reached for {0}", this);
|
||||||
|
}
|
||||||
|
catch (HttpException ex)
|
||||||
|
{
|
||||||
|
_importListStatusService.RecordFailure(Definition.Id);
|
||||||
|
_logger.Warn("{0} {1}", this, ex.Message);
|
||||||
|
}
|
||||||
|
catch (RequestLimitReachedException)
|
||||||
|
{
|
||||||
|
_importListStatusService.RecordFailure(Definition.Id, TimeSpan.FromHours(1));
|
||||||
|
_logger.Warn("API Request Limit reached for {0}", this);
|
||||||
|
}
|
||||||
|
catch (CloudFlareCaptchaException ex)
|
||||||
|
{
|
||||||
|
_importListStatusService.RecordFailure(Definition.Id);
|
||||||
|
ex.WithData("FeedUrl", url);
|
||||||
|
if (ex.IsExpired)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "Expired CAPTCHA token for {0}, please refresh in import list settings.", this);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "CAPTCHA token required for {0}, check import list settings.", this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ImportListException ex)
|
||||||
|
{
|
||||||
|
_importListStatusService.RecordFailure(Definition.Id);
|
||||||
|
_logger.Warn(ex, "{0}", url);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_importListStatusService.RecordFailure(Definition.Id);
|
||||||
|
ex.WithData("FeedUrl", url);
|
||||||
|
_logger.Error(ex, "An error occurred while processing feed. {0}", url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return CleanupListItems(releases);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual bool IsValidItem(ImportListItemInfo release)
|
||||||
|
{
|
||||||
|
if (release.Title.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual bool IsFullPage(IList<ImportListItemInfo> page)
|
||||||
|
{
|
||||||
|
return PageSize != 0 && page.Count >= PageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual IList<ImportListItemInfo> FetchPage(ImportListRequest request, IParseImportListResponse parser)
|
||||||
|
{
|
||||||
|
var response = FetchImportListResponse(request);
|
||||||
|
|
||||||
|
return parser.ParseResponse(response).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual ImportListResponse FetchImportListResponse(ImportListRequest request)
|
||||||
|
{
|
||||||
|
_logger.Debug("Downloading Feed " + request.HttpRequest.ToString(false));
|
||||||
|
|
||||||
|
if (request.HttpRequest.RateLimit < RateLimit)
|
||||||
|
{
|
||||||
|
request.HttpRequest.RateLimit = RateLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ImportListResponse(request, _httpClient.Execute(request.HttpRequest));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Test(List<ValidationFailure> failures)
|
||||||
|
{
|
||||||
|
failures.AddIfNotNull(TestConnection());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual ValidationFailure TestConnection()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parser = GetParser();
|
||||||
|
var generator = GetRequestGenerator();
|
||||||
|
var releases = FetchPage(generator.GetListItems().GetAllTiers().First().First(), parser);
|
||||||
|
|
||||||
|
if (releases.Empty())
|
||||||
|
{
|
||||||
|
return new ValidationFailure(string.Empty, "No results were returned from your import list, please check your settings.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (RequestLimitReachedException)
|
||||||
|
{
|
||||||
|
_logger.Warn("Request limit reached");
|
||||||
|
}
|
||||||
|
catch (UnsupportedFeedException ex)
|
||||||
|
{
|
||||||
|
_logger.Warn(ex, "Import list feed is not supported");
|
||||||
|
|
||||||
|
return new ValidationFailure(string.Empty, "Import list feed is not supported: " + ex.Message);
|
||||||
|
}
|
||||||
|
catch (ImportListException ex)
|
||||||
|
{
|
||||||
|
_logger.Warn(ex, "Unable to connect to import list");
|
||||||
|
|
||||||
|
return new ValidationFailure(string.Empty, "Unable to connect to import list. " + ex.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Warn(ex, "Unable to connect to import list");
|
||||||
|
|
||||||
|
return new ValidationFailure(string.Empty, "Unable to connect to import list, check the log for more details");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists
|
||||||
|
{
|
||||||
|
public interface IImportList : IProvider
|
||||||
|
{
|
||||||
|
ImportListType ListType { get; }
|
||||||
|
IList<ImportListItemInfo> Fetch();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace NzbDrone.Core.ImportLists
|
||||||
|
{
|
||||||
|
public interface IImportListRequestGenerator
|
||||||
|
{
|
||||||
|
ImportListPageableRequestChain GetListItems();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists
|
||||||
|
{
|
||||||
|
public interface IImportListSettings : IProviderConfig
|
||||||
|
{
|
||||||
|
string BaseUrl { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists
|
||||||
|
{
|
||||||
|
public interface IParseImportListResponse
|
||||||
|
{
|
||||||
|
IList<ImportListItemInfo> ParseResponse(ImportListResponse importListResponse);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentValidation.Results;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
using NzbDrone.Core.Parser;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists
|
||||||
|
{
|
||||||
|
public abstract class ImportListBase<TSettings> : IImportList
|
||||||
|
where TSettings : IImportListSettings, new()
|
||||||
|
{
|
||||||
|
protected readonly IImportListStatusService _importListStatusService;
|
||||||
|
protected readonly IConfigService _configService;
|
||||||
|
protected readonly IParsingService _parsingService;
|
||||||
|
protected readonly Logger _logger;
|
||||||
|
|
||||||
|
public abstract string Name { get; }
|
||||||
|
|
||||||
|
public abstract ImportListType ListType {get; }
|
||||||
|
|
||||||
|
public ImportListBase(IImportListStatusService importListStatusService, IConfigService configService, IParsingService parsingService, Logger logger)
|
||||||
|
{
|
||||||
|
_importListStatusService = importListStatusService;
|
||||||
|
_configService = configService;
|
||||||
|
_parsingService = parsingService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type ConfigContract => typeof(TSettings);
|
||||||
|
|
||||||
|
public virtual ProviderMessage Message => null;
|
||||||
|
|
||||||
|
public virtual IEnumerable<ProviderDefinition> DefaultDefinitions
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var config = (IProviderConfig)new TSettings();
|
||||||
|
|
||||||
|
yield return new ImportListDefinition
|
||||||
|
{
|
||||||
|
Name = GetType().Name,
|
||||||
|
EnableAutomaticAdd = config.Validate().IsValid,
|
||||||
|
Implementation = GetType().Name,
|
||||||
|
Settings = config
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual ProviderDefinition Definition { get; set; }
|
||||||
|
|
||||||
|
public virtual object RequestAction(string action, IDictionary<string, string> query) { return null; }
|
||||||
|
|
||||||
|
protected TSettings Settings => (TSettings)Definition.Settings;
|
||||||
|
|
||||||
|
public abstract IList<ImportListItemInfo> Fetch();
|
||||||
|
|
||||||
|
protected virtual IList<ImportListItemInfo> CleanupListItems(IEnumerable<ImportListItemInfo> releases)
|
||||||
|
{
|
||||||
|
var result = releases.DistinctBy(r => new {r.Title, r.TvdbId}).ToList();
|
||||||
|
|
||||||
|
result.ForEach(c =>
|
||||||
|
{
|
||||||
|
c.ImportListId = Definition.Id;
|
||||||
|
c.ImportList = Definition.Name;
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValidationResult Test()
|
||||||
|
{
|
||||||
|
var failures = new List<ValidationFailure>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Test(failures);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "Test aborted due to exception");
|
||||||
|
failures.Add(new ValidationFailure(string.Empty, "Test was aborted due to an error: " + ex.Message));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ValidationResult(failures);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void Test(List<ValidationFailure> failures);
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return Definition.Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists
|
||||||
|
{
|
||||||
|
public class ImportListDefinition : ProviderDefinition
|
||||||
|
{
|
||||||
|
public bool EnableAutomaticAdd { get; set; }
|
||||||
|
public MonitorTypes ShouldMonitor { get; set; }
|
||||||
|
public int QualityProfileId { get; set; }
|
||||||
|
public int LanguageProfileId { get; set; }
|
||||||
|
public string RootFolderPath { get; set; }
|
||||||
|
|
||||||
|
public override bool Enable => EnableAutomaticAdd;
|
||||||
|
|
||||||
|
public ImportListStatus Status { get; set; }
|
||||||
|
public ImportListType ListType { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentValidation.Results;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Composition;
|
||||||
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists
|
||||||
|
{
|
||||||
|
public interface IImportListFactory : IProviderFactory<IImportList, ImportListDefinition>
|
||||||
|
{
|
||||||
|
List<IImportList> AutomaticAddEnabled(bool filterBlockedImportLists = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ImportListFactory : ProviderFactory<IImportList, ImportListDefinition>, IImportListFactory
|
||||||
|
{
|
||||||
|
private readonly IImportListStatusService _importListStatusService;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
public ImportListFactory(IImportListStatusService importListStatusService,
|
||||||
|
IImportListRepository providerRepository,
|
||||||
|
IEnumerable<IImportList> providers,
|
||||||
|
IContainer container,
|
||||||
|
IEventAggregator eventAggregator,
|
||||||
|
Logger logger)
|
||||||
|
: base(providerRepository, providers, container, eventAggregator, logger)
|
||||||
|
{
|
||||||
|
_importListStatusService = importListStatusService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override List<ImportListDefinition> Active()
|
||||||
|
{
|
||||||
|
return base.Active().Where(c => c.Enable).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void SetProviderCharacteristics(IImportList provider, ImportListDefinition definition)
|
||||||
|
{
|
||||||
|
base.SetProviderCharacteristics(provider, definition);
|
||||||
|
|
||||||
|
definition.ListType = provider.ListType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<IImportList> AutomaticAddEnabled(bool filterBlockedImportLists = true)
|
||||||
|
{
|
||||||
|
var enabledImportLists = GetAvailableProviders().Where(n => ((ImportListDefinition)n.Definition).EnableAutomaticAdd);
|
||||||
|
|
||||||
|
if (filterBlockedImportLists)
|
||||||
|
{
|
||||||
|
return FilterBlockedImportLists(enabledImportLists).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return enabledImportLists.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<IImportList> FilterBlockedImportLists(IEnumerable<IImportList> importLists)
|
||||||
|
{
|
||||||
|
var blockedImportLists = _importListStatusService.GetBlockedProviders().ToDictionary(v => v.ProviderId, v => v);
|
||||||
|
|
||||||
|
foreach (var importList in importLists)
|
||||||
|
{
|
||||||
|
ImportListStatus blockedImportListStatus;
|
||||||
|
if (blockedImportLists.TryGetValue(importList.Definition.Id, out blockedImportListStatus))
|
||||||
|
{
|
||||||
|
_logger.Debug("Temporarily ignoring import list {0} till {1} due to recent failures.", importList.Definition.Name, blockedImportListStatus.DisabledTill.Value.ToLocalTime());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return importList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ValidationResult Test(ImportListDefinition definition)
|
||||||
|
{
|
||||||
|
var result = base.Test(definition);
|
||||||
|
|
||||||
|
if ((result == null || result.IsValid) && definition.Id != 0)
|
||||||
|
{
|
||||||
|
_importListStatusService.RecordSuccess(definition.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists
|
||||||
|
{
|
||||||
|
public class ImportListPageableRequest : IEnumerable<ImportListRequest>
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<ImportListRequest> _enumerable;
|
||||||
|
|
||||||
|
public ImportListPageableRequest(IEnumerable<ImportListRequest> enumerable)
|
||||||
|
{
|
||||||
|
_enumerable = enumerable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<ImportListRequest> GetEnumerator()
|
||||||
|
{
|
||||||
|
return _enumerable.GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
return _enumerable.GetEnumerator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists
|
||||||
|
{
|
||||||
|
public class ImportListPageableRequestChain
|
||||||
|
{
|
||||||
|
private List<List<ImportListPageableRequest>> _chains;
|
||||||
|
|
||||||
|
public ImportListPageableRequestChain()
|
||||||
|
{
|
||||||
|
_chains = new List<List<ImportListPageableRequest>>();
|
||||||
|
_chains.Add(new List<ImportListPageableRequest>());
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Tiers => _chains.Count;
|
||||||
|
|
||||||
|
public IEnumerable<ImportListPageableRequest> GetAllTiers()
|
||||||
|
{
|
||||||
|
return _chains.SelectMany(v => v);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<ImportListPageableRequest> GetTier(int index)
|
||||||
|
{
|
||||||
|
return _chains[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(IEnumerable<ImportListRequest> request)
|
||||||
|
{
|
||||||
|
if (request == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_chains.Last().Add(new ImportListPageableRequest(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddTier(IEnumerable<ImportListRequest> request)
|
||||||
|
{
|
||||||
|
AddTier();
|
||||||
|
Add(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddTier()
|
||||||
|
{
|
||||||
|
if (_chains.Last().Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_chains.Add(new List<ImportListPageableRequest>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
using NzbDrone.Core.ThingiProvider;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists
|
||||||
|
{
|
||||||
|
public interface IImportListRepository : IProviderRepository<ImportListDefinition>
|
||||||
|
{
|
||||||
|
void UpdateSettings(ImportListDefinition model);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ImportListRepository : ProviderRepository<ImportListDefinition>, IImportListRepository
|
||||||
|
{
|
||||||
|
public ImportListRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||||
|
: base(database, eventAggregator)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateSettings(ImportListDefinition model)
|
||||||
|
{
|
||||||
|
SetFields(model, m => m.Settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists
|
||||||
|
{
|
||||||
|
public class ImportListRequest
|
||||||
|
{
|
||||||
|
public HttpRequest HttpRequest { get; private set; }
|
||||||
|
|
||||||
|
public ImportListRequest(string url, HttpAccept httpAccept)
|
||||||
|
{
|
||||||
|
HttpRequest = new HttpRequest(url, httpAccept);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImportListRequest(HttpRequest httpRequest)
|
||||||
|
{
|
||||||
|
HttpRequest = httpRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpUri Url => HttpRequest.Url;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
using NzbDrone.Common.Http;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists
|
||||||
|
{
|
||||||
|
public class ImportListResponse
|
||||||
|
{
|
||||||
|
private readonly ImportListRequest _importListRequest;
|
||||||
|
private readonly HttpResponse _httpResponse;
|
||||||
|
|
||||||
|
public ImportListResponse(ImportListRequest importListRequest, HttpResponse httpResponse)
|
||||||
|
{
|
||||||
|
_importListRequest = importListRequest;
|
||||||
|
_httpResponse = httpResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImportListRequest Request => _importListRequest;
|
||||||
|
|
||||||
|
public HttpRequest HttpRequest => _httpResponse.Request;
|
||||||
|
|
||||||
|
public HttpResponse HttpResponse => _httpResponse;
|
||||||
|
|
||||||
|
public string Content => _httpResponse.Content;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Core.ThingiProvider.Status;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists
|
||||||
|
{
|
||||||
|
public class ImportListStatus : ProviderStatusBase
|
||||||
|
{
|
||||||
|
public ImportListItemInfo LastSyncListInfo { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
using NzbDrone.Core.ThingiProvider.Status;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists
|
||||||
|
{
|
||||||
|
public interface IImportListStatusRepository : IProviderStatusRepository<ImportListStatus>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ImportListStatusRepository : ProviderStatusRepository<ImportListStatus>, IImportListStatusRepository
|
||||||
|
|
||||||
|
{
|
||||||
|
public ImportListStatusRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||||
|
: base(database, eventAggregator)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Core.ThingiProvider.Status;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists
|
||||||
|
{
|
||||||
|
public interface IImportListStatusService : IProviderStatusServiceBase<ImportListStatus>
|
||||||
|
{
|
||||||
|
ImportListItemInfo GetLastSyncListInfo(int importListId);
|
||||||
|
|
||||||
|
void UpdateListSyncStatus(int importListId, ImportListItemInfo listItemInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ImportListStatusService : ProviderStatusServiceBase<IImportList, ImportListStatus>, IImportListStatusService
|
||||||
|
{
|
||||||
|
public ImportListStatusService(IImportListStatusRepository providerStatusRepository, IEventAggregator eventAggregator, IRuntimeInfo runtimeInfo, Logger logger)
|
||||||
|
: base(providerStatusRepository, eventAggregator, runtimeInfo, logger)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImportListItemInfo GetLastSyncListInfo(int importListId)
|
||||||
|
{
|
||||||
|
return GetProviderStatus(importListId).LastSyncListInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void UpdateListSyncStatus(int importListId, ImportListItemInfo listItemInfo)
|
||||||
|
{
|
||||||
|
lock (_syncRoot)
|
||||||
|
{
|
||||||
|
var status = GetProviderStatus(importListId);
|
||||||
|
|
||||||
|
status.LastSyncListInfo = listItemInfo;
|
||||||
|
|
||||||
|
_providerStatusRepository.Upsert(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists
|
||||||
|
{
|
||||||
|
public class ImportListSyncCommand : Command
|
||||||
|
{
|
||||||
|
public int? DefinitionId { get; set; }
|
||||||
|
|
||||||
|
public ImportListSyncCommand()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImportListSyncCommand(int? definition)
|
||||||
|
{
|
||||||
|
DefinitionId = definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool SendUpdatesToClient => true;
|
||||||
|
|
||||||
|
public override bool UpdateScheduledTask => !DefinitionId.HasValue;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Instrumentation.Extensions;
|
||||||
|
using NzbDrone.Core.ImportLists.Exclusions;
|
||||||
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
|
using NzbDrone.Core.MetadataSource;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists
|
||||||
|
{
|
||||||
|
public class ImportListSyncService : IExecute<ImportListSyncCommand>
|
||||||
|
{
|
||||||
|
private readonly IImportListFactory _importListFactory;
|
||||||
|
private readonly IImportListExclusionService _importListExclusionService;
|
||||||
|
private readonly IFetchAndParseImportList _listFetcherAndParser;
|
||||||
|
private readonly ISearchForNewSeries _seriesSearchService;
|
||||||
|
private readonly ISeriesService _seriesService;
|
||||||
|
private readonly IAddSeriesService _addSeriesService;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
public ImportListSyncService(IImportListFactory importListFactory,
|
||||||
|
IImportListExclusionService importListExclusionService,
|
||||||
|
IFetchAndParseImportList listFetcherAndParser,
|
||||||
|
ISearchForNewSeries seriesSearchService,
|
||||||
|
ISeriesService seriesService,
|
||||||
|
IAddSeriesService addSeriesService,
|
||||||
|
Logger logger)
|
||||||
|
{
|
||||||
|
_importListFactory = importListFactory;
|
||||||
|
_importListExclusionService = importListExclusionService;
|
||||||
|
_listFetcherAndParser = listFetcherAndParser;
|
||||||
|
_seriesSearchService = seriesSearchService;
|
||||||
|
_seriesService = seriesService;
|
||||||
|
_addSeriesService = addSeriesService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void SyncAll()
|
||||||
|
{
|
||||||
|
_logger.ProgressInfo("Starting Import List Sync");
|
||||||
|
|
||||||
|
var rssReleases = _listFetcherAndParser.Fetch();
|
||||||
|
|
||||||
|
var reports = rssReleases.ToList();
|
||||||
|
|
||||||
|
ProcessReports(reports);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SyncList(ImportListDefinition definition)
|
||||||
|
{
|
||||||
|
_logger.ProgressInfo(string.Format("Starting Import List Refresh for List {0}", definition.Name));
|
||||||
|
|
||||||
|
var rssReleases = _listFetcherAndParser.FetchSingleList(definition);
|
||||||
|
|
||||||
|
var reports = rssReleases.ToList();
|
||||||
|
|
||||||
|
ProcessReports(reports);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ProcessReports(List<ImportListItemInfo> reports)
|
||||||
|
{
|
||||||
|
var seriesToAdd = new List<Series>();
|
||||||
|
|
||||||
|
_logger.ProgressInfo("Processing {0} list items", reports.Count);
|
||||||
|
|
||||||
|
var reportNumber = 1;
|
||||||
|
|
||||||
|
var listExclusions = _importListExclusionService.All();
|
||||||
|
|
||||||
|
foreach (var report in reports)
|
||||||
|
{
|
||||||
|
_logger.ProgressTrace("Processing list item {0}/{1}", reportNumber, reports.Count);
|
||||||
|
|
||||||
|
reportNumber++;
|
||||||
|
|
||||||
|
var importList = _importListFactory.Get(report.ImportListId);
|
||||||
|
|
||||||
|
// Map TVDb if we only have a series name
|
||||||
|
if (report.TvdbId <= 0 && report.Title.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
var mappedSeries = _seriesSearchService.SearchForNewSeries(report.Title)
|
||||||
|
.FirstOrDefault();
|
||||||
|
report.TvdbId = mappedSeries.TvdbId;
|
||||||
|
report.Title = mappedSeries?.Title;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check to see if series in DB
|
||||||
|
var existingSeries = _seriesService.FindByTvdbId(report.TvdbId);
|
||||||
|
|
||||||
|
// Break if Series Exists in DB
|
||||||
|
if (existingSeries != null)
|
||||||
|
{
|
||||||
|
_logger.Debug("{0} [{1}] Rejected, Series Exists in DB", report.TvdbId, report.Title);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check to see if series excluded
|
||||||
|
var excludedSeries = listExclusions.Where(s => s.TvdbId == report.TvdbId).SingleOrDefault();
|
||||||
|
|
||||||
|
if (excludedSeries != null)
|
||||||
|
{
|
||||||
|
_logger.Debug("{0} [{1}] Rejected due to list exlcusion", report.TvdbId, report.Title);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append Series if not already in DB or already on add list
|
||||||
|
if (seriesToAdd.All(s => s.TvdbId != report.TvdbId))
|
||||||
|
{
|
||||||
|
var monitored = importList.ShouldMonitor != MonitorTypes.None;
|
||||||
|
|
||||||
|
seriesToAdd.Add(new Series
|
||||||
|
{
|
||||||
|
TvdbId = report.TvdbId,
|
||||||
|
Monitored = monitored,
|
||||||
|
RootFolderPath = importList.RootFolderPath,
|
||||||
|
QualityProfileId = importList.QualityProfileId,
|
||||||
|
LanguageProfileId = importList.LanguageProfileId,
|
||||||
|
Tags = importList.Tags,
|
||||||
|
SeasonFolder = true,
|
||||||
|
AddOptions = new AddSeriesOptions
|
||||||
|
{
|
||||||
|
SearchForMissingEpisodes = monitored,
|
||||||
|
Monitor = importList.ShouldMonitor
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_addSeriesService.AddSeries(seriesToAdd);
|
||||||
|
|
||||||
|
var message = string.Format("Import List Sync Completed. Items found: {0}, Series added: {1}", reports.Count, seriesToAdd.Count);
|
||||||
|
|
||||||
|
_logger.ProgressInfo(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute(ImportListSyncCommand message)
|
||||||
|
{
|
||||||
|
if (message.DefinitionId.HasValue)
|
||||||
|
{
|
||||||
|
SyncList(_importListFactory.Get(message.DefinitionId.Value));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SyncAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace NzbDrone.Core.ImportLists
|
||||||
|
{
|
||||||
|
public enum ImportListType
|
||||||
|
{
|
||||||
|
Program,
|
||||||
|
Trakt,
|
||||||
|
Other
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
|
using NzbDrone.Core.Messaging.Events;
|
||||||
|
using NzbDrone.Core.ThingiProvider.Events;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.ImportLists
|
||||||
|
{
|
||||||
|
public class ImportListUpdatedHandler : IHandle<ProviderUpdatedEvent<IImportList>>
|
||||||
|
{
|
||||||
|
private readonly IManageCommandQueue _commandQueueManager;
|
||||||
|
|
||||||
|
public ImportListUpdatedHandler(IManageCommandQueue commandQueueManager)
|
||||||
|
{
|
||||||
|
_commandQueueManager = commandQueueManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Handle(ProviderUpdatedEvent<IImportList> message)
|
||||||
|
{
|
||||||
|
_commandQueueManager.Push(new ImportListSyncCommand(message.Definition.Id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ using NzbDrone.Core.DataAugmentation.Scene;
|
||||||
using NzbDrone.Core.Download;
|
using NzbDrone.Core.Download;
|
||||||
using NzbDrone.Core.HealthCheck;
|
using NzbDrone.Core.HealthCheck;
|
||||||
using NzbDrone.Core.Housekeeping;
|
using NzbDrone.Core.Housekeeping;
|
||||||
|
using NzbDrone.Core.ImportLists;
|
||||||
using NzbDrone.Core.Indexers;
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.Lifecycle;
|
using NzbDrone.Core.Lifecycle;
|
||||||
using NzbDrone.Core.MediaFiles.Commands;
|
using NzbDrone.Core.MediaFiles.Commands;
|
||||||
|
@ -76,6 +77,12 @@ namespace NzbDrone.Core.Jobs
|
||||||
TypeName = typeof(BackupCommand).FullName
|
TypeName = typeof(BackupCommand).FullName
|
||||||
},
|
},
|
||||||
|
|
||||||
|
new ScheduledTask
|
||||||
|
{
|
||||||
|
Interval = 24 * 60,
|
||||||
|
TypeName = typeof(ImportListSyncCommand).FullName
|
||||||
|
},
|
||||||
|
|
||||||
new ScheduledTask
|
new ScheduledTask
|
||||||
{
|
{
|
||||||
Interval = GetRssSyncInterval(),
|
Interval = GetRssSyncInterval(),
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Parser.Model
|
||||||
|
{
|
||||||
|
public class ImportListItemInfo
|
||||||
|
{
|
||||||
|
public int ImportListId { get; set; }
|
||||||
|
public string ImportList { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public int TvdbId { get; set; }
|
||||||
|
public DateTime ReleaseDate { get; set; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("[{0}] {1}", ReleaseDate, Title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ using NzbDrone.Core.Lifecycle;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.Languages;
|
using NzbDrone.Core.Languages;
|
||||||
using NzbDrone.Core.Tv;
|
using NzbDrone.Core.Tv;
|
||||||
|
using NzbDrone.Core.ImportLists;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Profiles.Languages
|
namespace NzbDrone.Core.Profiles.Languages
|
||||||
{
|
{
|
||||||
|
@ -22,12 +23,14 @@ namespace NzbDrone.Core.Profiles.Languages
|
||||||
public class LanguageProfileService : ILanguageProfileService, IHandle<ApplicationStartedEvent>
|
public class LanguageProfileService : ILanguageProfileService, IHandle<ApplicationStartedEvent>
|
||||||
{
|
{
|
||||||
private readonly ILanguageProfileRepository _profileRepository;
|
private readonly ILanguageProfileRepository _profileRepository;
|
||||||
|
private readonly IImportListFactory _importListFactory;
|
||||||
private readonly ISeriesService _seriesService;
|
private readonly ISeriesService _seriesService;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public LanguageProfileService(ILanguageProfileRepository profileRepository, ISeriesService seriesService, Logger logger)
|
public LanguageProfileService(ILanguageProfileRepository profileRepository, IImportListFactory importListFactory, ISeriesService seriesService, Logger logger)
|
||||||
{
|
{
|
||||||
_profileRepository = profileRepository;
|
_profileRepository = profileRepository;
|
||||||
|
_importListFactory = importListFactory;
|
||||||
_seriesService = seriesService;
|
_seriesService = seriesService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
@ -44,7 +47,7 @@ namespace NzbDrone.Core.Profiles.Languages
|
||||||
|
|
||||||
public void Delete(int id)
|
public void Delete(int id)
|
||||||
{
|
{
|
||||||
if (_seriesService.GetAllSeries().Any(c => c.LanguageProfileId == id))
|
if (_seriesService.GetAllSeries().Any(c => c.LanguageProfileId == id) || _importListFactory.All().Any(c => c.LanguageProfileId == id))
|
||||||
{
|
{
|
||||||
throw new LanguageProfileInUseException(id);
|
throw new LanguageProfileInUseException(id);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
|
using NzbDrone.Core.ImportLists;
|
||||||
using NzbDrone.Core.Lifecycle;
|
using NzbDrone.Core.Lifecycle;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
|
@ -22,12 +23,14 @@ namespace NzbDrone.Core.Profiles.Qualities
|
||||||
public class QualityProfileService : IProfileService, IHandle<ApplicationStartedEvent>
|
public class QualityProfileService : IProfileService, IHandle<ApplicationStartedEvent>
|
||||||
{
|
{
|
||||||
private readonly IProfileRepository _profileRepository;
|
private readonly IProfileRepository _profileRepository;
|
||||||
|
private readonly IImportListFactory _importListFactory;
|
||||||
private readonly ISeriesService _seriesService;
|
private readonly ISeriesService _seriesService;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public QualityProfileService(IProfileRepository profileRepository, ISeriesService seriesService, Logger logger)
|
public QualityProfileService(IProfileRepository profileRepository, IImportListFactory importListFactory, ISeriesService seriesService, Logger logger)
|
||||||
{
|
{
|
||||||
_profileRepository = profileRepository;
|
_profileRepository = profileRepository;
|
||||||
|
_importListFactory = importListFactory;
|
||||||
_seriesService = seriesService;
|
_seriesService = seriesService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
@ -44,7 +47,7 @@ namespace NzbDrone.Core.Profiles.Qualities
|
||||||
|
|
||||||
public void Delete(int id)
|
public void Delete(int id)
|
||||||
{
|
{
|
||||||
if (_seriesService.GetAllSeries().Any(c => c.QualityProfileId == id))
|
if (_seriesService.GetAllSeries().Any(c => c.QualityProfileId == id) || _importListFactory.All().Any(c => c.QualityProfileId == id))
|
||||||
{
|
{
|
||||||
var profile = _profileRepository.Get(id);
|
var profile = _profileRepository.Get(id);
|
||||||
throw new QualityProfileInUseException(profile.Name);
|
throw new QualityProfileInUseException(profile.Name);
|
||||||
|
|
|
@ -11,12 +11,13 @@ namespace NzbDrone.Core.Tags
|
||||||
public List<int> NotificationIds { get; set; }
|
public List<int> NotificationIds { get; set; }
|
||||||
public List<int> RestrictionIds { get; set; }
|
public List<int> RestrictionIds { get; set; }
|
||||||
public List<int> DelayProfileIds { get; set; }
|
public List<int> DelayProfileIds { get; set; }
|
||||||
|
public List<int> ImportListIds { get; set; }
|
||||||
|
|
||||||
public bool InUse
|
public bool InUse
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
return (SeriesIds.Any() || NotificationIds.Any() || RestrictionIds.Any() || DelayProfileIds.Any());
|
return (SeriesIds.Any() || NotificationIds.Any() || RestrictionIds.Any() || DelayProfileIds.Any() || ImportListIds.Any());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NzbDrone.Core.Datastore;
|
using NzbDrone.Core.Datastore;
|
||||||
|
using NzbDrone.Core.ImportLists;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.Notifications;
|
using NzbDrone.Core.Notifications;
|
||||||
using NzbDrone.Core.Profiles.Delay;
|
using NzbDrone.Core.Profiles.Delay;
|
||||||
|
@ -28,6 +29,7 @@ namespace NzbDrone.Core.Tags
|
||||||
private readonly ITagRepository _repo;
|
private readonly ITagRepository _repo;
|
||||||
private readonly IEventAggregator _eventAggregator;
|
private readonly IEventAggregator _eventAggregator;
|
||||||
private readonly IDelayProfileService _delayProfileService;
|
private readonly IDelayProfileService _delayProfileService;
|
||||||
|
private readonly IImportListFactory _importListFactory;
|
||||||
private readonly INotificationFactory _notificationFactory;
|
private readonly INotificationFactory _notificationFactory;
|
||||||
private readonly IReleaseProfileService _releaseProfileService;
|
private readonly IReleaseProfileService _releaseProfileService;
|
||||||
private readonly ISeriesService _seriesService;
|
private readonly ISeriesService _seriesService;
|
||||||
|
@ -35,6 +37,7 @@ namespace NzbDrone.Core.Tags
|
||||||
public TagService(ITagRepository repo,
|
public TagService(ITagRepository repo,
|
||||||
IEventAggregator eventAggregator,
|
IEventAggregator eventAggregator,
|
||||||
IDelayProfileService delayProfileService,
|
IDelayProfileService delayProfileService,
|
||||||
|
IImportListFactory importListFactory,
|
||||||
INotificationFactory notificationFactory,
|
INotificationFactory notificationFactory,
|
||||||
IReleaseProfileService releaseProfileService,
|
IReleaseProfileService releaseProfileService,
|
||||||
ISeriesService seriesService)
|
ISeriesService seriesService)
|
||||||
|
@ -42,6 +45,7 @@ namespace NzbDrone.Core.Tags
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
_delayProfileService = delayProfileService;
|
_delayProfileService = delayProfileService;
|
||||||
|
_importListFactory = importListFactory;
|
||||||
_notificationFactory = notificationFactory;
|
_notificationFactory = notificationFactory;
|
||||||
_releaseProfileService = releaseProfileService;
|
_releaseProfileService = releaseProfileService;
|
||||||
_seriesService = seriesService;
|
_seriesService = seriesService;
|
||||||
|
@ -73,6 +77,7 @@ namespace NzbDrone.Core.Tags
|
||||||
{
|
{
|
||||||
var tag = GetTag(tagId);
|
var tag = GetTag(tagId);
|
||||||
var delayProfiles = _delayProfileService.AllForTag(tagId);
|
var delayProfiles = _delayProfileService.AllForTag(tagId);
|
||||||
|
var importLists = _importListFactory.AllForTag(tagId);
|
||||||
var notifications = _notificationFactory.AllForTag(tagId);
|
var notifications = _notificationFactory.AllForTag(tagId);
|
||||||
var restrictions = _releaseProfileService.AllForTag(tagId);
|
var restrictions = _releaseProfileService.AllForTag(tagId);
|
||||||
var series = _seriesService.AllForTag(tagId);
|
var series = _seriesService.AllForTag(tagId);
|
||||||
|
@ -82,6 +87,7 @@ namespace NzbDrone.Core.Tags
|
||||||
Id = tagId,
|
Id = tagId,
|
||||||
Label = tag.Label,
|
Label = tag.Label,
|
||||||
DelayProfileIds = delayProfiles.Select(c => c.Id).ToList(),
|
DelayProfileIds = delayProfiles.Select(c => c.Id).ToList(),
|
||||||
|
ImportListIds = importLists.Select(c => c.Id).ToList(),
|
||||||
NotificationIds = notifications.Select(c => c.Id).ToList(),
|
NotificationIds = notifications.Select(c => c.Id).ToList(),
|
||||||
RestrictionIds = restrictions.Select(c => c.Id).ToList(),
|
RestrictionIds = restrictions.Select(c => c.Id).ToList(),
|
||||||
SeriesIds = series.Select(c => c.Id).ToList()
|
SeriesIds = series.Select(c => c.Id).ToList()
|
||||||
|
@ -92,6 +98,7 @@ namespace NzbDrone.Core.Tags
|
||||||
{
|
{
|
||||||
var tags = All();
|
var tags = All();
|
||||||
var delayProfiles = _delayProfileService.All();
|
var delayProfiles = _delayProfileService.All();
|
||||||
|
var importLists = _importListFactory.All();
|
||||||
var notifications = _notificationFactory.All();
|
var notifications = _notificationFactory.All();
|
||||||
var restrictions = _releaseProfileService.All();
|
var restrictions = _releaseProfileService.All();
|
||||||
var series = _seriesService.GetAllSeries();
|
var series = _seriesService.GetAllSeries();
|
||||||
|
@ -105,6 +112,7 @@ namespace NzbDrone.Core.Tags
|
||||||
Id = tag.Id,
|
Id = tag.Id,
|
||||||
Label = tag.Label,
|
Label = tag.Label,
|
||||||
DelayProfileIds = delayProfiles.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
DelayProfileIds = delayProfiles.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
||||||
|
ImportListIds = importLists.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
||||||
NotificationIds = notifications.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
NotificationIds = notifications.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
||||||
RestrictionIds = restrictions.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
RestrictionIds = restrictions.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
||||||
SeriesIds = series.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList()
|
SeriesIds = series.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList()
|
||||||
|
|
|
@ -6,11 +6,13 @@ namespace NzbDrone.Core.Tv.Events
|
||||||
{
|
{
|
||||||
public Series Series { get; private set; }
|
public Series Series { get; private set; }
|
||||||
public bool DeleteFiles { get; private set; }
|
public bool DeleteFiles { get; private set; }
|
||||||
|
public bool AddImportListExclusion { get; private set; }
|
||||||
|
|
||||||
public SeriesDeletedEvent(Series series, bool deleteFiles)
|
public SeriesDeletedEvent(Series series, bool deleteFiles, bool addImportListExclusion)
|
||||||
{
|
{
|
||||||
Series = series;
|
Series = series;
|
||||||
DeleteFiles = deleteFiles;
|
DeleteFiles = deleteFiles;
|
||||||
|
AddImportListExclusion = addImportListExclusion;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -22,7 +22,7 @@ namespace NzbDrone.Core.Tv
|
||||||
Series FindByTitle(string title, int year);
|
Series FindByTitle(string title, int year);
|
||||||
Series FindByTitleInexact(string title);
|
Series FindByTitleInexact(string title);
|
||||||
Series FindByPath(string path);
|
Series FindByPath(string path);
|
||||||
void DeleteSeries(int seriesId, bool deleteFiles);
|
void DeleteSeries(int seriesId, bool deleteFiles, bool addImportListExclusion = false);
|
||||||
List<Series> GetAllSeries();
|
List<Series> GetAllSeries();
|
||||||
List<Series> AllForTag(int tagId);
|
List<Series> AllForTag(int tagId);
|
||||||
Series UpdateSeries(Series series, bool updateEpisodesToMatchSeason = true, bool publishUpdatedEvent = true);
|
Series UpdateSeries(Series series, bool updateEpisodesToMatchSeason = true, bool publishUpdatedEvent = true);
|
||||||
|
@ -145,11 +145,11 @@ namespace NzbDrone.Core.Tv
|
||||||
return _seriesRepository.FindByTitle(title.CleanSeriesTitle(), year);
|
return _seriesRepository.FindByTitle(title.CleanSeriesTitle(), year);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeleteSeries(int seriesId, bool deleteFiles)
|
public void DeleteSeries(int seriesId, bool deleteFiles, bool addImportListExclusion = false)
|
||||||
{
|
{
|
||||||
var series = _seriesRepository.Get(seriesId);
|
var series = _seriesRepository.Get(seriesId);
|
||||||
_seriesRepository.Delete(seriesId);
|
_seriesRepository.Delete(seriesId);
|
||||||
_eventAggregator.PublishEvent(new SeriesDeletedEvent(series, deleteFiles));
|
_eventAggregator.PublishEvent(new SeriesDeletedEvent(series, deleteFiles, addImportListExclusion));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Series> GetAllSeries()
|
public List<Series> GetAllSeries()
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using NzbDrone.Core.ImportLists.Exclusions;
|
||||||
|
using Sonarr.Http;
|
||||||
|
using FluentValidation;
|
||||||
|
using NzbDrone.Core.Validation;
|
||||||
|
|
||||||
|
namespace Sonarr.Api.V3.ImportLists
|
||||||
|
{
|
||||||
|
public class ImportListExclusionModule : SonarrRestModule<ImportListExclusionResource>
|
||||||
|
{
|
||||||
|
private readonly IImportListExclusionService _importListExclusionService;
|
||||||
|
|
||||||
|
public ImportListExclusionModule(IImportListExclusionService importListExclusionService,
|
||||||
|
ImportListExclusionExistsValidator importListExclusionExistsValidator)
|
||||||
|
{
|
||||||
|
_importListExclusionService = importListExclusionService;
|
||||||
|
|
||||||
|
GetResourceById = GetImportListExclusion;
|
||||||
|
GetResourceAll = GetImportListExclusions;
|
||||||
|
CreateResource = AddImportListExclusion;
|
||||||
|
UpdateResource = UpdateImportListExclusion;
|
||||||
|
DeleteResource = DeleteImportListExclusionResource;
|
||||||
|
|
||||||
|
SharedValidator.RuleFor(c => c.TvdbId).NotEmpty().SetValidator(importListExclusionExistsValidator);
|
||||||
|
SharedValidator.RuleFor(c => c.Title).NotEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ImportListExclusionResource GetImportListExclusion(int id)
|
||||||
|
{
|
||||||
|
return _importListExclusionService.Get(id).ToResource();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ImportListExclusionResource> GetImportListExclusions()
|
||||||
|
{
|
||||||
|
return _importListExclusionService.All().ToResource();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int AddImportListExclusion(ImportListExclusionResource resource)
|
||||||
|
{
|
||||||
|
var customFilter = _importListExclusionService.Add(resource.ToModel());
|
||||||
|
|
||||||
|
return customFilter.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateImportListExclusion(ImportListExclusionResource resource)
|
||||||
|
{
|
||||||
|
_importListExclusionService.Update(resource.ToModel());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DeleteImportListExclusionResource(int id)
|
||||||
|
{
|
||||||
|
_importListExclusionService.Delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NzbDrone.Core.ImportLists.Exclusions;
|
||||||
|
using Sonarr.Http.REST;
|
||||||
|
|
||||||
|
namespace Sonarr.Api.V3.ImportLists
|
||||||
|
{
|
||||||
|
public class ImportListExclusionResource : RestResource
|
||||||
|
{
|
||||||
|
public int TvdbId { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ImportListExclusionResourceMapper
|
||||||
|
{
|
||||||
|
public static ImportListExclusionResource ToResource(this ImportListExclusion model)
|
||||||
|
{
|
||||||
|
if (model == null) return null;
|
||||||
|
|
||||||
|
return new ImportListExclusionResource
|
||||||
|
{
|
||||||
|
Id = model.Id,
|
||||||
|
TvdbId = model.TvdbId,
|
||||||
|
Title = model.Title,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ImportListExclusion ToModel(this ImportListExclusionResource resource)
|
||||||
|
{
|
||||||
|
if (resource == null) return null;
|
||||||
|
|
||||||
|
return new ImportListExclusion
|
||||||
|
{
|
||||||
|
Id = resource.Id,
|
||||||
|
TvdbId = resource.TvdbId,
|
||||||
|
Title = resource.Title
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ImportListExclusionResource> ToResource(this IEnumerable<ImportListExclusion> filters)
|
||||||
|
{
|
||||||
|
return filters.Select(ToResource).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
using NzbDrone.Core.ImportLists;
|
||||||
|
using NzbDrone.Core.Validation;
|
||||||
|
using NzbDrone.Core.Validation.Paths;
|
||||||
|
|
||||||
|
namespace Sonarr.Api.V3.ImportLists
|
||||||
|
{
|
||||||
|
public class ImportListModule : ProviderModuleBase<ImportListResource, IImportList, ImportListDefinition>
|
||||||
|
{
|
||||||
|
public static readonly ImportListResourceMapper ResourceMapper = new ImportListResourceMapper();
|
||||||
|
|
||||||
|
public ImportListModule(ImportListFactory importListFactory,
|
||||||
|
ProfileExistsValidator profileExistsValidator,
|
||||||
|
LanguageProfileExistsValidator languageProfileExistsValidator
|
||||||
|
)
|
||||||
|
: base(importListFactory, "importlist", ResourceMapper)
|
||||||
|
{
|
||||||
|
Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.QualityProfileId));
|
||||||
|
Http.Validation.RuleBuilderExtensions.ValidId(SharedValidator.RuleFor(s => s.LanguageProfileId));
|
||||||
|
|
||||||
|
SharedValidator.RuleFor(c => c.RootFolderPath).IsValidPath();
|
||||||
|
SharedValidator.RuleFor(c => c.QualityProfileId).SetValidator(profileExistsValidator);
|
||||||
|
SharedValidator.RuleFor(c => c.LanguageProfileId).SetValidator(languageProfileExistsValidator);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Validate(ImportListDefinition definition, bool includeWarnings)
|
||||||
|
{
|
||||||
|
if (!definition.Enable)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
base.Validate(definition, includeWarnings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
using NzbDrone.Core.ImportLists;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
|
||||||
|
namespace Sonarr.Api.V3.ImportLists
|
||||||
|
{
|
||||||
|
public class ImportListResource : ProviderResource
|
||||||
|
{
|
||||||
|
public bool EnableAutomaticAdd { get; set; }
|
||||||
|
public MonitorTypes ShouldMonitor { get; set; }
|
||||||
|
public string RootFolderPath { get; set; }
|
||||||
|
public int QualityProfileId { get; set; }
|
||||||
|
public int LanguageProfileId { get; set; }
|
||||||
|
public ImportListType ListType { get; set; }
|
||||||
|
public int ListOrder { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ImportListResourceMapper : ProviderResourceMapper<ImportListResource, ImportListDefinition>
|
||||||
|
{
|
||||||
|
public override ImportListResource ToResource(ImportListDefinition definition)
|
||||||
|
{
|
||||||
|
if (definition == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resource = base.ToResource(definition);
|
||||||
|
|
||||||
|
resource.EnableAutomaticAdd = definition.EnableAutomaticAdd;
|
||||||
|
resource.ShouldMonitor = definition.ShouldMonitor;
|
||||||
|
resource.RootFolderPath = definition.RootFolderPath;
|
||||||
|
resource.QualityProfileId = definition.QualityProfileId;
|
||||||
|
resource.LanguageProfileId = definition.LanguageProfileId;
|
||||||
|
resource.ListType = definition.ListType;
|
||||||
|
resource.ListOrder = (int) definition.ListType;
|
||||||
|
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ImportListDefinition ToModel(ImportListResource resource)
|
||||||
|
{
|
||||||
|
if (resource == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var definition = base.ToModel(resource);
|
||||||
|
|
||||||
|
definition.EnableAutomaticAdd = resource.EnableAutomaticAdd;
|
||||||
|
definition.ShouldMonitor = resource.ShouldMonitor;
|
||||||
|
definition.RootFolderPath = resource.RootFolderPath;
|
||||||
|
definition.QualityProfileId = resource.QualityProfileId;
|
||||||
|
definition.LanguageProfileId = resource.LanguageProfileId;
|
||||||
|
definition.ListType = resource.ListType;
|
||||||
|
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ namespace Sonarr.Api.V3.Tags
|
||||||
{
|
{
|
||||||
public string Label { get; set; }
|
public string Label { get; set; }
|
||||||
public List<int> DelayProfileIds { get; set; }
|
public List<int> DelayProfileIds { get; set; }
|
||||||
|
public List<int> ImportListIds { get; set; }
|
||||||
public List<int> NotificationIds { get; set; }
|
public List<int> NotificationIds { get; set; }
|
||||||
public List<int> RestrictionIds { get; set; }
|
public List<int> RestrictionIds { get; set; }
|
||||||
public List<int> SeriesIds { get; set; }
|
public List<int> SeriesIds { get; set; }
|
||||||
|
@ -25,6 +26,7 @@ namespace Sonarr.Api.V3.Tags
|
||||||
Id = model.Id,
|
Id = model.Id,
|
||||||
Label = model.Label,
|
Label = model.Label,
|
||||||
DelayProfileIds = model.DelayProfileIds,
|
DelayProfileIds = model.DelayProfileIds,
|
||||||
|
ImportListIds = model.ImportListIds,
|
||||||
NotificationIds = model.NotificationIds,
|
NotificationIds = model.NotificationIds,
|
||||||
RestrictionIds = model.RestrictionIds,
|
RestrictionIds = model.RestrictionIds,
|
||||||
SeriesIds = model.SeriesIds
|
SeriesIds = model.SeriesIds
|
||||||
|
|
Loading…
Reference in New Issue