From 06b1c03053c731b44db48b93fc21d1ff181457d2 Mon Sep 17 00:00:00 2001 From: Qstick Date: Fri, 5 Jul 2019 22:26:16 -0400 Subject: [PATCH] Fixed: List Exclusions, List Processing Tweaks --- .../AddNewMovie/AddNewMovieConnector.js | 8 +- .../AddNewMovie/AddNewMovieSearchResult.css | 5 + .../AddNewMovie/AddNewMovieSearchResult.js | 18 ++- .../AddNewMovieSearchResultConnector.js | 5 +- .../Movie/Delete/DeleteMovieModalContent.js | 26 +++- .../DeleteMovieModalContentConnector.js | 5 +- .../EditNetImportExclusionModal.js | 27 ++++ .../EditNetImportExclusionModalConnector.js | 43 ++++++ .../EditNetImportExclusionModalContent.css | 11 ++ .../EditNetImportExclusionModalContent.js | 143 ++++++++++++++++++ ...NetImportExclusionModalContentConnector.js | 118 +++++++++++++++ .../NetImportExclusion.css | 24 +++ .../NetImportExclusions/NetImportExclusion.js | 114 ++++++++++++++ .../NetImportExclusions.css | 24 +++ .../NetImportExclusions.js | 101 +++++++++++++ .../NetImportExclusionsConnector.js | 59 ++++++++ .../Settings/NetImport/NetImportSettings.js | 4 +- .../Actions/Settings/netImportExclusions.js | 69 +++++++++ frontend/src/Store/Actions/settingsActions.js | 5 + .../Selectors/createExclusionMovieSelector.js | 14 ++ .../NetImport/NetImportSearchService.cs | 43 ++---- src/Radarr.Api.V2/Movies/MovieModule.cs | 2 +- .../NetImport/ImportExclusionsModule.cs | 8 +- 23 files changed, 834 insertions(+), 42 deletions(-) create mode 100644 frontend/src/Settings/NetImport/NetImportExclusions/EditNetImportExclusionModal.js create mode 100644 frontend/src/Settings/NetImport/NetImportExclusions/EditNetImportExclusionModalConnector.js create mode 100644 frontend/src/Settings/NetImport/NetImportExclusions/EditNetImportExclusionModalContent.css create mode 100644 frontend/src/Settings/NetImport/NetImportExclusions/EditNetImportExclusionModalContent.js create mode 100644 frontend/src/Settings/NetImport/NetImportExclusions/EditNetImportExclusionModalContentConnector.js create mode 100644 frontend/src/Settings/NetImport/NetImportExclusions/NetImportExclusion.css create mode 100644 frontend/src/Settings/NetImport/NetImportExclusions/NetImportExclusion.js create mode 100644 frontend/src/Settings/NetImport/NetImportExclusions/NetImportExclusions.css create mode 100644 frontend/src/Settings/NetImport/NetImportExclusions/NetImportExclusions.js create mode 100644 frontend/src/Settings/NetImport/NetImportExclusions/NetImportExclusionsConnector.js create mode 100644 frontend/src/Store/Actions/Settings/netImportExclusions.js create mode 100644 frontend/src/Store/Selectors/createExclusionMovieSelector.js diff --git a/frontend/src/AddMovie/AddNewMovie/AddNewMovieConnector.js b/frontend/src/AddMovie/AddNewMovie/AddNewMovieConnector.js index bc4d71435..6c238a10d 100644 --- a/frontend/src/AddMovie/AddNewMovie/AddNewMovieConnector.js +++ b/frontend/src/AddMovie/AddNewMovie/AddNewMovieConnector.js @@ -5,6 +5,7 @@ import { createSelector } from 'reselect'; import parseUrl from 'Utilities/String/parseUrl'; import { lookupMovie, clearAddMovie } from 'Store/Actions/addMovieActions'; import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; +import { fetchNetImportExclusions } from 'Store/Actions/Settings/netImportExclusions'; import AddNewMovie from './AddNewMovie'; function createMapStateToProps() { @@ -25,7 +26,8 @@ function createMapStateToProps() { const mapDispatchToProps = { lookupMovie, clearAddMovie, - fetchRootFolders + fetchRootFolders, + fetchNetImportExclusions }; class AddNewMovieConnector extends Component { @@ -41,6 +43,7 @@ class AddNewMovieConnector extends Component { componentDidMount() { this.props.fetchRootFolders(); + this.props.fetchNetImportExclusions(); } componentWillUnmount() { @@ -96,7 +99,8 @@ AddNewMovieConnector.propTypes = { term: PropTypes.string, lookupMovie: PropTypes.func.isRequired, clearAddMovie: PropTypes.func.isRequired, - fetchRootFolders: PropTypes.func.isRequired + fetchRootFolders: PropTypes.func.isRequired, + fetchNetImportExclusions: PropTypes.func.isRequired }; export default connect(createMapStateToProps, mapDispatchToProps)(AddNewMovieConnector); diff --git a/frontend/src/AddMovie/AddNewMovie/AddNewMovieSearchResult.css b/frontend/src/AddMovie/AddNewMovie/AddNewMovieSearchResult.css index 38ccffb4d..87959fe45 100644 --- a/frontend/src/AddMovie/AddNewMovie/AddNewMovieSearchResult.css +++ b/frontend/src/AddMovie/AddNewMovie/AddNewMovieSearchResult.css @@ -35,6 +35,11 @@ color: #37bc9b; } +.exclusionIcon { + margin-left: 10px; + color: #bc3737; +} + .overview { margin-top: 20px; } diff --git a/frontend/src/AddMovie/AddNewMovie/AddNewMovieSearchResult.js b/frontend/src/AddMovie/AddNewMovie/AddNewMovieSearchResult.js index 186459e95..1afdbb478 100644 --- a/frontend/src/AddMovie/AddNewMovie/AddNewMovieSearchResult.js +++ b/frontend/src/AddMovie/AddNewMovie/AddNewMovieSearchResult.js @@ -24,7 +24,7 @@ class AddNewMovieSearchResult extends Component { componentDidUpdate(prevProps) { if (!prevProps.isExistingMovie && this.props.isExistingMovie) { - this.onAddSerisModalClose(); + this.onAddMovieModalClose(); } } @@ -35,7 +35,7 @@ class AddNewMovieSearchResult extends Component { this.setState({ isNewAddMovieModalOpen: true }); } - onAddSerisModalClose = () => { + onAddMovieModalClose = () => { this.setState({ isNewAddMovieModalOpen: false }); } @@ -54,6 +54,7 @@ class AddNewMovieSearchResult extends Component { ratings, images, isExistingMovie, + isExclusionMovie, isSmallScreen } = this.props; const { @@ -97,6 +98,16 @@ class AddNewMovieSearchResult extends Component { title="Already in your library" /> } + + { + isExclusionMovie && + + }
@@ -138,7 +149,7 @@ class AddNewMovieSearchResult extends Component { year={year} overview={overview} images={images} - onModalClose={this.onAddSerisModalClose} + onModalClose={this.onAddMovieModalClose} />
); @@ -156,6 +167,7 @@ AddNewMovieSearchResult.propTypes = { ratings: PropTypes.object.isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired, isExistingMovie: PropTypes.bool.isRequired, + isExclusionMovie: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired }; diff --git a/frontend/src/AddMovie/AddNewMovie/AddNewMovieSearchResultConnector.js b/frontend/src/AddMovie/AddNewMovie/AddNewMovieSearchResultConnector.js index fccbe6120..54bc03881 100644 --- a/frontend/src/AddMovie/AddNewMovie/AddNewMovieSearchResultConnector.js +++ b/frontend/src/AddMovie/AddNewMovie/AddNewMovieSearchResultConnector.js @@ -1,16 +1,19 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import createExistingMovieSelector from 'Store/Selectors/createExistingMovieSelector'; +import createExclusionMovieSelector from 'Store/Selectors/createExclusionMovieSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import AddNewMovieSearchResult from './AddNewMovieSearchResult'; function createMapStateToProps() { return createSelector( createExistingMovieSelector(), + createExclusionMovieSelector(), createDimensionsSelector(), - (isExistingMovie, dimensions) => { + (isExistingMovie, isExclusionMovie, dimensions) => { return { isExistingMovie, + isExclusionMovie, isSmallScreen: dimensions.isSmallScreen }; } diff --git a/frontend/src/Movie/Delete/DeleteMovieModalContent.js b/frontend/src/Movie/Delete/DeleteMovieModalContent.js index f76a4de35..f0e288573 100644 --- a/frontend/src/Movie/Delete/DeleteMovieModalContent.js +++ b/frontend/src/Movie/Delete/DeleteMovieModalContent.js @@ -22,7 +22,8 @@ class DeleteMovieModalContent extends Component { super(props, context); this.state = { - deleteFiles: false + deleteFiles: false, + addNetImportExclusion: false }; } @@ -33,11 +34,17 @@ class DeleteMovieModalContent extends Component { this.setState({ deleteFiles: value }); } + onAddNetImportExclusionChange = ({ value }) => { + this.setState({ addNetImportExclusion: value }); + } + onDeleteMovieConfirmed = () => { const deleteFiles = this.state.deleteFiles; + const addNetImportExclusion = this.state.addNetImportExclusion; this.setState({ deleteFiles: false }); - this.props.onDeletePress(deleteFiles); + this.setState({ addNetImportExclusion: false }); + this.props.onDeletePress(deleteFiles, addNetImportExclusion); } // @@ -57,6 +64,8 @@ class DeleteMovieModalContent extends Component { } = statistics; const deleteFiles = this.state.deleteFiles; + const addNetImportExclusion = this.state.addNetImportExclusion; + let deleteFilesLabel = `Delete ${movieFileCount} Movie Files`; let deleteFilesHelpText = 'Delete the movie files and movie folder'; @@ -108,6 +117,19 @@ class DeleteMovieModalContent extends Component { } + + Add List Exclusion + + + + diff --git a/frontend/src/Movie/Delete/DeleteMovieModalContentConnector.js b/frontend/src/Movie/Delete/DeleteMovieModalContentConnector.js index d02c30294..2f2df1489 100644 --- a/frontend/src/Movie/Delete/DeleteMovieModalContentConnector.js +++ b/frontend/src/Movie/Delete/DeleteMovieModalContentConnector.js @@ -24,10 +24,11 @@ class DeleteMovieModalContentConnector extends Component { // // Listeners - onDeletePress = (deleteFiles) => { + onDeletePress = (deleteFiles, addNetImportExclusion) => { this.props.deleteMovie({ id: this.props.movieId, - deleteFiles + deleteFiles, + addNetImportExclusion }); this.props.onModalClose(true); diff --git a/frontend/src/Settings/NetImport/NetImportExclusions/EditNetImportExclusionModal.js b/frontend/src/Settings/NetImport/NetImportExclusions/EditNetImportExclusionModal.js new file mode 100644 index 000000000..0a9601cf9 --- /dev/null +++ b/frontend/src/Settings/NetImport/NetImportExclusions/EditNetImportExclusionModal.js @@ -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 EditNetImportExclusionModalContentConnector from './EditNetImportExclusionModalContentConnector'; + +function EditNetImportExclusionModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditNetImportExclusionModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditNetImportExclusionModal; diff --git a/frontend/src/Settings/NetImport/NetImportExclusions/EditNetImportExclusionModalConnector.js b/frontend/src/Settings/NetImport/NetImportExclusions/EditNetImportExclusionModalConnector.js new file mode 100644 index 000000000..3d5833f8e --- /dev/null +++ b/frontend/src/Settings/NetImport/NetImportExclusions/EditNetImportExclusionModalConnector.js @@ -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 EditNetImportExclusionModal from './EditNetImportExclusionModal'; + +function mapStateToProps() { + return {}; +} + +const mapDispatchToProps = { + clearPendingChanges +}; + +class EditNetImportExclusionModalConnector extends Component { + + // + // Listeners + + onModalClose = () => { + this.props.clearPendingChanges({ section: 'settings.netImportExclusions' }); + this.props.onModalClose(); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditNetImportExclusionModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(mapStateToProps, mapDispatchToProps)(EditNetImportExclusionModalConnector); diff --git a/frontend/src/Settings/NetImport/NetImportExclusions/EditNetImportExclusionModalContent.css b/frontend/src/Settings/NetImport/NetImportExclusions/EditNetImportExclusionModalContent.css new file mode 100644 index 000000000..97e132552 --- /dev/null +++ b/frontend/src/Settings/NetImport/NetImportExclusions/EditNetImportExclusionModalContent.css @@ -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; +} diff --git a/frontend/src/Settings/NetImport/NetImportExclusions/EditNetImportExclusionModalContent.js b/frontend/src/Settings/NetImport/NetImportExclusions/EditNetImportExclusionModalContent.js new file mode 100644 index 000000000..47349e737 --- /dev/null +++ b/frontend/src/Settings/NetImport/NetImportExclusions/EditNetImportExclusionModalContent.js @@ -0,0 +1,143 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +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 './EditNetImportExclusionModalContent.css'; + +function EditNetImportExclusionModalContent(props) { + const { + id, + isFetching, + error, + isSaving, + saveError, + item, + onInputChange, + onSavePress, + onModalClose, + onDeleteNetImportExclusionPress, + ...otherProps + } = props; + + const { + movieTitle = '', + tmdbId, + movieYear + } = item; + + return ( + + + {id ? 'Edit List Exclusion' : 'Add List Exclusion'} + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to add a new list exclusion, please try again.
+ } + + { + !isFetching && !error && +
+ + TMDB Id + + + + + + Movie Title + + + + + + Movie Year + + + + +
+ } +
+ + + { + id && + + } + + + + + Save + + +
+ ); +} + +EditNetImportExclusionModalContent.propTypes = { + id: PropTypes.number, + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + onInputChange: PropTypes.func.isRequired, + onSavePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + onDeleteNetImportExclusionPress: PropTypes.func +}; + +export default EditNetImportExclusionModalContent; diff --git a/frontend/src/Settings/NetImport/NetImportExclusions/EditNetImportExclusionModalContentConnector.js b/frontend/src/Settings/NetImport/NetImportExclusions/EditNetImportExclusionModalContentConnector.js new file mode 100644 index 000000000..967eb8aca --- /dev/null +++ b/frontend/src/Settings/NetImport/NetImportExclusions/EditNetImportExclusionModalContentConnector.js @@ -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 { setNetImportExclusionValue, saveNetImportExclusion } from 'Store/Actions/settingsActions'; +import EditNetImportExclusionModalContent from './EditNetImportExclusionModalContent'; + +const newNetImportExclusion = { + artistName: '', + foreignId: '' +}; + +function createNetImportExclusionSelector() { + return createSelector( + (state, { id }) => id, + (state) => state.settings.netImportExclusions, + (id, netImportExclusions) => { + const { + isFetching, + error, + isSaving, + saveError, + pendingChanges, + items + } = netImportExclusions; + + const mapping = id ? _.find(items, { id }) : newNetImportExclusion; + const settings = selectSettings(mapping, pendingChanges, saveError); + + return { + id, + isFetching, + error, + isSaving, + saveError, + item: settings.settings, + ...settings + }; + } + ); +} + +function createMapStateToProps() { + return createSelector( + createNetImportExclusionSelector(), + (netImportExclusion) => { + return { + ...netImportExclusion + }; + } + ); +} + +const mapDispatchToProps = { + setNetImportExclusionValue, + saveNetImportExclusion +}; + +class EditNetImportExclusionModalContentConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + if (!this.props.id) { + Object.keys(newNetImportExclusion).forEach((name) => { + this.props.setNetImportExclusionValue({ + name, + value: newNetImportExclusion[name] + }); + }); + } + } + + componentDidUpdate(prevProps, prevState) { + if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) { + this.props.onModalClose(); + } + } + + // + // Listeners + + onInputChange = ({ name, value }) => { + this.props.setNetImportExclusionValue({ name, value }); + } + + onSavePress = () => { + this.props.saveNetImportExclusion({ id: this.props.id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +EditNetImportExclusionModalContentConnector.propTypes = { + id: PropTypes.number, + isSaving: PropTypes.bool.isRequired, + saveError: PropTypes.object, + item: PropTypes.object.isRequired, + setNetImportExclusionValue: PropTypes.func.isRequired, + saveNetImportExclusion: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(EditNetImportExclusionModalContentConnector); diff --git a/frontend/src/Settings/NetImport/NetImportExclusions/NetImportExclusion.css b/frontend/src/Settings/NetImport/NetImportExclusions/NetImportExclusion.css new file mode 100644 index 000000000..780d6f8f1 --- /dev/null +++ b/frontend/src/Settings/NetImport/NetImportExclusions/NetImportExclusion.css @@ -0,0 +1,24 @@ +.netImportExclusion { + display: flex; + align-items: stretch; + margin-bottom: 10px; + height: 30px; + border-bottom: 1px solid $borderColor; + line-height: 30px; +} + +.movieTitle { + flex: 0 0 400px; +} + +.tmdbId, +.movieYear { + flex: 0 0 200px; +} + +.actions { + display: flex; + justify-content: flex-end; + flex: 1 0 auto; + padding-right: 10px; +} diff --git a/frontend/src/Settings/NetImport/NetImportExclusions/NetImportExclusion.js b/frontend/src/Settings/NetImport/NetImportExclusions/NetImportExclusion.js new file mode 100644 index 000000000..f766e3685 --- /dev/null +++ b/frontend/src/Settings/NetImport/NetImportExclusions/NetImportExclusion.js @@ -0,0 +1,114 @@ +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 EditNetImportExclusionModalConnector from './EditNetImportExclusionModalConnector'; +import styles from './NetImportExclusion.css'; + +class NetImportExclusion extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isEditNetImportExclusionModalOpen: false, + isDeleteNetImportExclusionModalOpen: false + }; + } + + // + // Listeners + + onEditNetImportExclusionPress = () => { + this.setState({ isEditNetImportExclusionModalOpen: true }); + } + + onEditNetImportExclusionModalClose = () => { + this.setState({ isEditNetImportExclusionModalOpen: false }); + } + + onDeleteNetImportExclusionPress = () => { + this.setState({ + isEditNetImportExclusionModalOpen: false, + isDeleteNetImportExclusionModalOpen: true + }); + } + + onDeleteNetImportExclusionModalClose = () => { + this.setState({ isDeleteNetImportExclusionModalOpen: false }); + } + + onConfirmDeleteNetImportExclusion = () => { + this.props.onConfirmDeleteNetImportExclusion(this.props.id); + } + + // + // Render + + render() { + const { + id, + movieTitle, + tmdbId, + movieYear + } = this.props; + + return ( +
+
{tmdbId}
+
{movieTitle}
+
{movieYear}
+ +
+ + + +
+ + + + +
+ ); + } +} + +NetImportExclusion.propTypes = { + id: PropTypes.number.isRequired, + movieTitle: PropTypes.string.isRequired, + tmdbId: PropTypes.number.isRequired, + movieYear: PropTypes.number.isRequired, + onConfirmDeleteNetImportExclusion: PropTypes.func.isRequired +}; + +NetImportExclusion.defaultProps = { + // The drag preview will not connect the drag handle. + connectDragSource: (node) => node +}; + +export default NetImportExclusion; diff --git a/frontend/src/Settings/NetImport/NetImportExclusions/NetImportExclusions.css b/frontend/src/Settings/NetImport/NetImportExclusions/NetImportExclusions.css new file mode 100644 index 000000000..00b917a84 --- /dev/null +++ b/frontend/src/Settings/NetImport/NetImportExclusions/NetImportExclusions.css @@ -0,0 +1,24 @@ +.netImportExclusionsHeader { + display: flex; + margin-bottom: 10px; + font-weight: bold; +} + +.title { + flex: 0 0 400px; +} + +.tmdbId, +.movieYear { + flex: 0 0 200px; +} + +.addNetImportExclusion { + display: flex; + justify-content: flex-end; + padding-right: 10px; +} + +.addButton { + text-align: center; +} diff --git a/frontend/src/Settings/NetImport/NetImportExclusions/NetImportExclusions.js b/frontend/src/Settings/NetImport/NetImportExclusions/NetImportExclusions.js new file mode 100644 index 000000000..198bf08a2 --- /dev/null +++ b/frontend/src/Settings/NetImport/NetImportExclusions/NetImportExclusions.js @@ -0,0 +1,101 @@ +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 NetImportExclusion from './NetImportExclusion'; +import EditNetImportExclusionModalConnector from './EditNetImportExclusionModalConnector'; +import styles from './NetImportExclusions.css'; + +class NetImportExclusions extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + isAddNetImportExclusionModalOpen: false + }; + } + + // + // Listeners + + onAddNetImportExclusionPress = () => { + this.setState({ isAddNetImportExclusionModalOpen: true }); + } + + onModalClose = () => { + this.setState({ isAddNetImportExclusionModalOpen: false }); + } + + // + // Render + + render() { + const { + items, + onConfirmDeleteNetImportExclusion, + ...otherProps + } = this.props; + + return ( +
+ +
+
TMDB Id
+
Title
+
Year
+
+ +
+ { + items.map((item, index) => { + return ( + + ); + }) + } +
+ +
+ + + +
+ + + +
+
+ ); + } +} + +NetImportExclusions.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onConfirmDeleteNetImportExclusion: PropTypes.func.isRequired +}; + +export default NetImportExclusions; diff --git a/frontend/src/Settings/NetImport/NetImportExclusions/NetImportExclusionsConnector.js b/frontend/src/Settings/NetImport/NetImportExclusions/NetImportExclusionsConnector.js new file mode 100644 index 000000000..5d561d431 --- /dev/null +++ b/frontend/src/Settings/NetImport/NetImportExclusions/NetImportExclusionsConnector.js @@ -0,0 +1,59 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { fetchNetImportExclusions, deleteNetImportExclusion } from 'Store/Actions/settingsActions'; +import NetImportExclusions from './NetImportExclusions'; + +function createMapStateToProps() { + return createSelector( + (state) => state.settings.netImportExclusions, + (netImportExclusions) => { + return { + ...netImportExclusions + }; + } + ); +} + +const mapDispatchToProps = { + fetchNetImportExclusions, + deleteNetImportExclusion +}; + +class NetImportExclusionsConnector extends Component { + + // + // Lifecycle + + componentDidMount() { + this.props.fetchNetImportExclusions(); + } + + // + // Listeners + + onConfirmDeleteNetImportExclusion = (id) => { + this.props.deleteNetImportExclusion({ id }); + } + + // + // Render + + render() { + return ( + + ); + } +} + +NetImportExclusionsConnector.propTypes = { + fetchNetImportExclusions: PropTypes.func.isRequired, + deleteNetImportExclusion: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(NetImportExclusionsConnector); diff --git a/frontend/src/Settings/NetImport/NetImportSettings.js b/frontend/src/Settings/NetImport/NetImportSettings.js index 9da0370a8..eb614d68c 100644 --- a/frontend/src/Settings/NetImport/NetImportSettings.js +++ b/frontend/src/Settings/NetImport/NetImportSettings.js @@ -8,7 +8,7 @@ import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import NetImportsConnector from './NetImport/NetImportsConnector'; import NetImportOptionsConnector from './Options/NetImportOptionsConnector'; -// import ImportExclusionsConnector from './ImportExclusions/ImportExclusionsConnector'; +import NetImportExclusionsConnector from './NetImportExclusions/NetImportExclusionsConnector'; class NetImportSettings extends Component { @@ -85,6 +85,8 @@ class NetImportSettings extends Component { onChildStateChange={this.onChildStateChange} /> + + ); diff --git a/frontend/src/Store/Actions/Settings/netImportExclusions.js b/frontend/src/Store/Actions/Settings/netImportExclusions.js new file mode 100644 index 000000000..01d82f999 --- /dev/null +++ b/frontend/src/Store/Actions/Settings/netImportExclusions.js @@ -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.netImportExclusions'; + +// +// Actions Types + +export const FETCH_NET_IMPORT_EXCLUSIONS = 'settings/netImportExclusions/fetchNetImportExclusions'; +export const SAVE_NET_IMPORT_EXCLUSION = 'settings/netImportExclusions/saveNetImportExclusion'; +export const DELETE_NET_IMPORT_EXCLUSION = 'settings/netImportExclusions/deleteNetImportExclusion'; +export const SET_NET_IMPORT_EXCLUSION_VALUE = 'settings/netImportExclusions/setNetImportExclusionValue'; + +// +// Action Creators + +export const fetchNetImportExclusions = createThunk(FETCH_NET_IMPORT_EXCLUSIONS); +export const saveNetImportExclusion = createThunk(SAVE_NET_IMPORT_EXCLUSION); +export const deleteNetImportExclusion = createThunk(DELETE_NET_IMPORT_EXCLUSION); + +export const setNetImportExclusionValue = createAction(SET_NET_IMPORT_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_NET_IMPORT_EXCLUSIONS]: createFetchHandler(section, '/exclusions'), + [SAVE_NET_IMPORT_EXCLUSION]: createSaveProviderHandler(section, '/exclusions'), + [DELETE_NET_IMPORT_EXCLUSION]: createRemoveItemHandler(section, '/exclusions') + }, + + // + // Reducers + + reducers: { + [SET_NET_IMPORT_EXCLUSION_VALUE]: createSetSettingValueReducer(section) + } + +}; diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js index 2c8fd4f65..7cb2a3edd 100644 --- a/frontend/src/Store/Actions/settingsActions.js +++ b/frontend/src/Store/Actions/settingsActions.js @@ -9,6 +9,7 @@ import general from './Settings/general'; import indexerOptions from './Settings/indexerOptions'; import indexers from './Settings/indexers'; import languages from './Settings/languages'; +import netImportExclusions from './Settings/netImportExclusions'; import netImportOptions from './Settings/netImportOptions'; import netImports from './Settings/netImports'; import mediaManagement from './Settings/mediaManagement'; @@ -30,6 +31,7 @@ export * from './Settings/general'; export * from './Settings/indexerOptions'; export * from './Settings/indexers'; export * from './Settings/languages'; +export * from './Settings/netImportExclusions'; export * from './Settings/netImportOptions'; export * from './Settings/netImports'; export * from './Settings/mediaManagement'; @@ -62,6 +64,7 @@ export const defaultState = { indexerOptions: indexerOptions.defaultState, indexers: indexers.defaultState, languages: languages.defaultState, + netImportExclusions: netImportExclusions.defaultState, netImportOptions: netImportOptions.defaultState, netImports: netImports.defaultState, mediaManagement: mediaManagement.defaultState, @@ -102,6 +105,7 @@ export const actionHandlers = handleThunks({ ...indexerOptions.actionHandlers, ...indexers.actionHandlers, ...languages.actionHandlers, + ...netImportExclusions.actionHandlers, ...netImportOptions.actionHandlers, ...netImports.actionHandlers, ...mediaManagement.actionHandlers, @@ -133,6 +137,7 @@ export const reducers = createHandleActions({ ...indexerOptions.reducers, ...indexers.reducers, ...languages.reducers, + ...netImportExclusions.reducers, ...netImportOptions.reducers, ...netImports.reducers, ...mediaManagement.reducers, diff --git a/frontend/src/Store/Selectors/createExclusionMovieSelector.js b/frontend/src/Store/Selectors/createExclusionMovieSelector.js new file mode 100644 index 000000000..12adfd854 --- /dev/null +++ b/frontend/src/Store/Selectors/createExclusionMovieSelector.js @@ -0,0 +1,14 @@ +import _ from 'lodash'; +import { createSelector } from 'reselect'; + +function createExclusionMovieSelector() { + return createSelector( + (state, { tmdbId }) => tmdbId, + (state) => state.settings.netImportExclusions, + (tmdbId, netImportExclusions) => { + return _.some(netImportExclusions.items, { tmdbId }); + } + ); +} + +export default createExclusionMovieSelector; diff --git a/src/NzbDrone.Core/NetImport/NetImportSearchService.cs b/src/NzbDrone.Core/NetImport/NetImportSearchService.cs index 706a82d31..ce7ac9dca 100644 --- a/src/NzbDrone.Core/NetImport/NetImportSearchService.cs +++ b/src/NzbDrone.Core/NetImport/NetImportSearchService.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System; using System.Linq; -using System.Text.RegularExpressions; using NLog; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.MetadataSource; @@ -9,8 +8,6 @@ using NzbDrone.Core.RootFolders; using NzbDrone.Core.Movies; using NzbDrone.Core.Configuration; using NzbDrone.Common.Extensions; -using NzbDrone.Common.Instrumentation.Extensions; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; using NzbDrone.Core.IndexerSearch; using NzbDrone.Core.NetImport.ImportExclusions; @@ -134,43 +131,33 @@ namespace NzbDrone.Core.NetImport _logger.Info($"Found {listedMovies.Count()} movies on your auto enabled lists not in your library"); } - var importExclusions = new List(); + var moviesToAdd = new List(); - //var downloadedCount = 0; foreach (var movie in listedMovies) { var mapped = _movieSearch.MapMovieToTmdbMovie(movie); - if (mapped != null && !_exclusionService.IsMovieExcluded(mapped.TmdbId)) + + if (mapped == null) + { + _logger.Debug($"{movie.Title} could not be mapped to a valid tmdb ID, will not be added from list"); + } + else if (_exclusionService.IsMovieExcluded(mapped.TmdbId)) { - //List decisions; - mapped.AddOptions = new AddMovieOptions {SearchForMovie = true}; - _movieService.AddMovie(mapped); - - //// Search for movie - //try - //{ - // decisions = _nzbSearchService.MovieSearch(mapped.Id, false); - //} - //catch (Exception ex) - //{ - // _logger.Error(ex, $"Unable to search in list for movie {mapped.Id}"); - // continue; - //} - - //var processed = _processDownloadDecisions.ProcessDecisions(decisions); - //downloadedCount += processed.Grabbed.Count; + _logger.Debug($"{mapped.Title} ({mapped.TitleSlug}) will not be added since it was found on the exclusions list"); + } + else if (_movieService.MovieExists(mapped)) + { + _logger.Debug($"{mapped.Title} ({mapped.TitleSlug}) will not be added since it exists in DB"); } else { - if (mapped != null) - { - _logger.Info($"{mapped.Title} ({mapped.TitleSlug}) will not be added since it was found on the exclusions list"); - } + mapped.AddOptions = new AddMovieOptions { SearchForMovie = true }; + moviesToAdd.Add(mapped); } } - //_logger.ProgressInfo("Movie search completed. {0} reports downloaded.", downloadedCount); + _movieService.AddMovies(moviesToAdd); } private void CleanLibrary(List movies) diff --git a/src/Radarr.Api.V2/Movies/MovieModule.cs b/src/Radarr.Api.V2/Movies/MovieModule.cs index 3b213ba71..3b850394b 100644 --- a/src/Radarr.Api.V2/Movies/MovieModule.cs +++ b/src/Radarr.Api.V2/Movies/MovieModule.cs @@ -117,7 +117,7 @@ namespace Radarr.Api.V2.Movies private void DeleteMovie(int id) { var addExclusion = false; - var addExclusionQuery = Request.Query.addExclusion; + var addExclusionQuery = Request.Query.addNetImportExclusion; var deleteFiles = Request.GetBooleanQueryParameter("deleteFiles"); diff --git a/src/Radarr.Api.V2/NetImport/ImportExclusionsModule.cs b/src/Radarr.Api.V2/NetImport/ImportExclusionsModule.cs index 34d5599fd..c3a42696d 100644 --- a/src/Radarr.Api.V2/NetImport/ImportExclusionsModule.cs +++ b/src/Radarr.Api.V2/NetImport/ImportExclusionsModule.cs @@ -1,9 +1,7 @@ using System.Collections.Generic; using FluentValidation; -using Radarr.Http.ClientSchema; using NzbDrone.Core.NetImport; using NzbDrone.Core.NetImport.ImportExclusions; -using NzbDrone.Core.Validation.Paths; using Radarr.Http; namespace Radarr.Api.V2.NetImport @@ -19,6 +17,10 @@ namespace Radarr.Api.V2.NetImport CreateResource = AddExclusion; DeleteResource = RemoveExclusion; GetResourceById = GetById; + + SharedValidator.RuleFor(c => c.TmdbId).GreaterThan(0); + SharedValidator.RuleFor(c => c.MovieTitle).NotEmpty(); + SharedValidator.RuleFor(c => c.MovieYear).GreaterThan(0); } public List GetAll() @@ -35,6 +37,8 @@ namespace Radarr.Api.V2.NetImport { var model = exclusionResource.ToModel(); + // TODO: Add some more validation here and auto pull the title if not provided + return _exclusionService.AddExclusion(model).Id; }