From 42c16c227ee5194cf7f06053b89257f0db206fd3 Mon Sep 17 00:00:00 2001 From: Qstick Date: Fri, 1 Mar 2019 17:26:36 -0500 Subject: [PATCH] New: Import List Exclusions (#608) * New: Import List Exclusions * Fixed: ImportExclusion ForeignId Checks, Unique. RefreshArtist Duplicate * Fixed: Copy/Paste typos --- .../Artist/Delete/DeleteArtistModalContent.js | 26 +++- .../DeleteArtistModalContentConnector.js | 5 +- .../EditImportListExclusionModal.js | 27 ++++ .../EditImportListExclusionModalConnector.js | 43 ++++++ .../EditImportListExclusionModalContent.css | 11 ++ .../EditImportListExclusionModalContent.js | 135 ++++++++++++++++++ ...mportListExclusionModalContentConnector.js | 118 +++++++++++++++ .../ImportListExclusion.css | 23 +++ .../ImportListExclusion.js | 111 ++++++++++++++ .../ImportListExclusions.css | 23 +++ .../ImportListExclusions.js | 100 +++++++++++++ .../ImportListExclusionsConnector.js | 59 ++++++++ .../ImportLists/ImportListSettings.js | 2 + .../Actions/Settings/importListExclusions.js | 69 +++++++++ frontend/src/Store/Actions/artistActions.js | 3 +- frontend/src/Store/Actions/settingsActions.js | 5 + src/Lidarr.Api.V1/Artist/ArtistModule.cs | 3 +- .../ImportLists/ImportListExclusionModule.cs | 56 ++++++++ .../ImportListExclusionResource.cs | 45 ++++++ src/Lidarr.Api.V1/Lidarr.Api.V1.csproj | 2 + .../ImportListSyncServiceFixture.cs | 32 ++++- .../NzbDrone.Core.Test.csproj | 10 +- .../ValidationTests/GuidValidationFixture.cs | 44 ++++++ .../Migration/027_add_import_exclusions.cs | 16 +++ src/NzbDrone.Core/Datastore/TableMapping.cs | 2 + .../Exclusions/ImportListExclusion.cs | 10 ++ .../ImportListExclusionExistsValidator.cs | 22 +++ .../ImportListExclusionRepository.cs | 24 ++++ .../Exclusions/ImportListExclusionService.cs | 80 +++++++++++ .../ImportLists/ImportListSyncService.cs | 16 ++- src/NzbDrone.Core/Music/ArtistService.cs | 12 +- .../Music/Events/ArtistDeletedEvent.cs | 6 +- .../Music/RefreshArtistService.cs | 14 ++ src/NzbDrone.Core/NzbDrone.Core.csproj | 6 + src/NzbDrone.Core/Validation/GuidValidator.cs | 20 +++ 35 files changed, 1160 insertions(+), 20 deletions(-) create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.js create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalConnector.js create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.css create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.js create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContentConnector.js create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.js create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.js create mode 100644 frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionsConnector.js create mode 100644 frontend/src/Store/Actions/Settings/importListExclusions.js create mode 100644 src/Lidarr.Api.V1/ImportLists/ImportListExclusionModule.cs create mode 100644 src/Lidarr.Api.V1/ImportLists/ImportListExclusionResource.cs create mode 100644 src/NzbDrone.Core.Test/ValidationTests/GuidValidationFixture.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/027_add_import_exclusions.cs create mode 100644 src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusion.cs create mode 100644 src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionExistsValidator.cs create mode 100644 src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionRepository.cs create mode 100644 src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs create mode 100644 src/NzbDrone.Core/Validation/GuidValidator.cs diff --git a/frontend/src/Artist/Delete/DeleteArtistModalContent.js b/frontend/src/Artist/Delete/DeleteArtistModalContent.js index c17cf69f9..c80dca7d9 100644 --- a/frontend/src/Artist/Delete/DeleteArtistModalContent.js +++ b/frontend/src/Artist/Delete/DeleteArtistModalContent.js @@ -22,7 +22,8 @@ class DeleteArtistModalContent extends Component { super(props, context); this.state = { - deleteFiles: false + deleteFiles: false, + addImportListExclusion: false }; } @@ -33,11 +34,17 @@ class DeleteArtistModalContent extends Component { this.setState({ deleteFiles: value }); } + onAddImportListExclusionChange = ({ value }) => { + this.setState({ addImportListExclusion: value }); + } + onDeleteArtistConfirmed = () => { const deleteFiles = this.state.deleteFiles; + const addImportListExclusion = this.state.addImportListExclusion; this.setState({ deleteFiles: false }); - this.props.onDeletePress(deleteFiles); + this.setState({ addImportListExclusion: false }); + this.props.onDeletePress(deleteFiles, addImportListExclusion); } // @@ -57,6 +64,8 @@ class DeleteArtistModalContent extends Component { } = statistics; const deleteFiles = this.state.deleteFiles; + const addImportListExclusion = this.state.addImportListExclusion; + let deleteFilesLabel = `Delete ${trackFileCount} Track Files`; let deleteFilesHelpText = 'Delete the track files and artist folder'; @@ -96,6 +105,19 @@ class DeleteArtistModalContent extends Component { /> + + Add List Exclusion + + + + { deleteFiles &&
diff --git a/frontend/src/Artist/Delete/DeleteArtistModalContentConnector.js b/frontend/src/Artist/Delete/DeleteArtistModalContentConnector.js index 938ac5a96..e0ea034ab 100644 --- a/frontend/src/Artist/Delete/DeleteArtistModalContentConnector.js +++ b/frontend/src/Artist/Delete/DeleteArtistModalContentConnector.js @@ -24,10 +24,11 @@ class DeleteArtistModalContentConnector extends Component { // // Listeners - onDeletePress = (deleteFiles) => { + onDeletePress = (deleteFiles, addImportListExclusion) => { this.props.deleteArtist({ id: this.props.artistId, - deleteFiles + deleteFiles, + addImportListExclusion }); this.props.onModalClose(true); diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.js b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.js new file mode 100644 index 000000000..72566b289 --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModal.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 EditImportListExclusionModalContentConnector from './EditImportListExclusionModalContentConnector'; + +function EditImportListExclusionModal({ isOpen, onModalClose, ...otherProps }) { + return ( + + + + ); +} + +EditImportListExclusionModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default EditImportListExclusionModal; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalConnector.js b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalConnector.js new file mode 100644 index 000000000..f9a511675 --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalConnector.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 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 ( + + ); + } +} + +EditImportListExclusionModalConnector.propTypes = { + onModalClose: PropTypes.func.isRequired, + clearPendingChanges: PropTypes.func.isRequired +}; + +export default connect(mapStateToProps, mapDispatchToProps)(EditImportListExclusionModalConnector); diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.css b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.css new file mode 100644 index 000000000..5481cccc0 --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.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/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.js b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.js new file mode 100644 index 000000000..ccb2fa04a --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContent.js @@ -0,0 +1,135 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { inputTypes, kinds } from 'Helpers/Props'; +import { stringSettingShape } 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 { + artistName, + foreignId + } = item; + + return ( + + + {id ? 'Edit Import List Exclusion' : 'Add Import List Exclusion'} + + + + { + isFetching && + + } + + { + !isFetching && !!error && +
Unable to add a new import list exclusion, please try again.
+ } + + { + !isFetching && !error && +
+ + Artist Name + + + + + + Musicbrainz Id + + + +
+ } +
+ + + { + id && + + } + + + + + Save + + +
+ ); +} + +const ImportListExclusionShape = { + artistName: PropTypes.shape(stringSettingShape).isRequired, + foreignId: PropTypes.shape(stringSettingShape).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; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContentConnector.js b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContentConnector.js new file mode 100644 index 000000000..2516ca25b --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/EditImportListExclusionModalContentConnector.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 { setImportListExclusionValue, saveImportListExclusion } from 'Store/Actions/settingsActions'; +import EditImportListExclusionModalContent from './EditImportListExclusionModalContent'; + +const newImportListExclusion = { + artistName: '', + foreignId: '' +}; + +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 ( + + ); + } +} + +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); diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css new file mode 100644 index 000000000..4c274831c --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.css @@ -0,0 +1,23 @@ +.importListExclusion { + display: flex; + align-items: stretch; + margin-bottom: 10px; + height: 30px; + border-bottom: 1px solid $borderColor; + line-height: 30px; +} + +.artistName { + flex: 0 0 300px; +} + +.foreignId { + flex: 0 0 400px; +} + +.actions { + display: flex; + justify-content: flex-end; + flex: 1 0 auto; + padding-right: 10px; +} diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.js b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.js new file mode 100644 index 000000000..9de6c45ee --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusion.js @@ -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, + artistName, + foreignId + } = this.props; + + return ( +
+
{artistName}
+
{foreignId}
+ +
+ + + +
+ + + + +
+ ); + } +} + +ImportListExclusion.propTypes = { + id: PropTypes.number.isRequired, + artistName: PropTypes.string.isRequired, + foreignId: PropTypes.string.isRequired, + onConfirmDeleteImportListExclusion: PropTypes.func.isRequired +}; + +ImportListExclusion.defaultProps = { + // The drag preview will not connect the drag handle. + connectDragSource: (node) => node +}; + +export default ImportListExclusion; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css new file mode 100644 index 000000000..99e1c1e99 --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.css @@ -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; +} diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.js b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.js new file mode 100644 index 000000000..f84015e56 --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusions.js @@ -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 ( +
+ +
+
Name
+
Foreign Id
+
+ +
+ { + items.map((item, index) => { + return ( + + ); + }) + } +
+ +
+ + + +
+ + + +
+
+ ); + } +} + +ImportListExclusions.propTypes = { + isFetching: PropTypes.bool.isRequired, + error: PropTypes.object, + items: PropTypes.arrayOf(PropTypes.object).isRequired, + onConfirmDeleteImportListExclusion: PropTypes.func.isRequired +}; + +export default ImportListExclusions; diff --git a/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionsConnector.js b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionsConnector.js new file mode 100644 index 000000000..c5f15f43d --- /dev/null +++ b/frontend/src/Settings/ImportLists/ImportListExclusions/ImportListExclusionsConnector.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 { 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 ( + + ); + } +} + +ImportListExclusionsConnector.propTypes = { + fetchImportListExclusions: PropTypes.func.isRequired, + deleteImportListExclusion: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(ImportListExclusionsConnector); diff --git a/frontend/src/Settings/ImportLists/ImportListSettings.js b/frontend/src/Settings/ImportLists/ImportListSettings.js index 561bcfd5a..63a8a6733 100644 --- a/frontend/src/Settings/ImportLists/ImportListSettings.js +++ b/frontend/src/Settings/ImportLists/ImportListSettings.js @@ -7,6 +7,7 @@ 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 { @@ -74,6 +75,7 @@ class ImportListSettings extends Component { + ); diff --git a/frontend/src/Store/Actions/Settings/importListExclusions.js b/frontend/src/Store/Actions/Settings/importListExclusions.js new file mode 100644 index 000000000..b584e9e28 --- /dev/null +++ b/frontend/src/Store/Actions/Settings/importListExclusions.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.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) + } + +}; diff --git a/frontend/src/Store/Actions/artistActions.js b/frontend/src/Store/Actions/artistActions.js index 500c5e84c..b7fe3069e 100644 --- a/frontend/src/Store/Actions/artistActions.js +++ b/frontend/src/Store/Actions/artistActions.js @@ -186,7 +186,8 @@ export const deleteArtist = createThunk(DELETE_ARTIST, (payload) => { return { ...payload, queryParams: { - deleteFiles: payload.deleteFiles + deleteFiles: payload.deleteFiles, + addImportListExclusion: payload.addImportListExclusion } }; }); diff --git a/frontend/src/Store/Actions/settingsActions.js b/frontend/src/Store/Actions/settingsActions.js index 1455a6935..b549c914d 100644 --- a/frontend/src/Store/Actions/settingsActions.js +++ b/frontend/src/Store/Actions/settingsActions.js @@ -9,6 +9,7 @@ import indexerOptions from './Settings/indexerOptions'; import indexers from './Settings/indexers'; import languageProfiles from './Settings/languageProfiles'; import importLists from './Settings/importLists'; +import importListExclusions from './Settings/importListExclusions'; import metadataProfiles from './Settings/metadataProfiles'; import mediaManagement from './Settings/mediaManagement'; import metadata from './Settings/metadata'; @@ -27,6 +28,7 @@ export * from './Settings/downloadClients'; export * from './Settings/downloadClientOptions'; export * from './Settings/general'; export * from './Settings/importLists'; +export * from './Settings/importListExclusions'; export * from './Settings/indexerOptions'; export * from './Settings/indexers'; export * from './Settings/languageProfiles'; @@ -62,6 +64,7 @@ export const defaultState = { indexers: indexers.defaultState, languageProfiles: languageProfiles.defaultState, importLists: importLists.defaultState, + importListExclusions: importListExclusions.defaultState, metadataProfiles: metadataProfiles.defaultState, mediaManagement: mediaManagement.defaultState, metadata: metadata.defaultState, @@ -102,6 +105,7 @@ export const actionHandlers = handleThunks({ ...indexers.actionHandlers, ...languageProfiles.actionHandlers, ...importLists.actionHandlers, + ...importListExclusions.actionHandlers, ...metadataProfiles.actionHandlers, ...mediaManagement.actionHandlers, ...metadata.actionHandlers, @@ -133,6 +137,7 @@ export const reducers = createHandleActions({ ...indexers.reducers, ...languageProfiles.reducers, ...importLists.reducers, + ...importListExclusions.reducers, ...metadataProfiles.reducers, ...mediaManagement.reducers, ...metadata.reducers, diff --git a/src/Lidarr.Api.V1/Artist/ArtistModule.cs b/src/Lidarr.Api.V1/Artist/ArtistModule.cs index faeaf0b06..3f72ea898 100644 --- a/src/Lidarr.Api.V1/Artist/ArtistModule.cs +++ b/src/Lidarr.Api.V1/Artist/ArtistModule.cs @@ -171,8 +171,9 @@ namespace Lidarr.Api.V1.Artist private void DeleteArtist(int id) { var deleteFiles = Request.GetBooleanQueryParameter("deleteFiles"); + var addImportListExclusion = Request.GetBooleanQueryParameter("addImportListExclusion"); - _artistService.DeleteArtist(id, deleteFiles); + _artistService.DeleteArtist(id, deleteFiles, addImportListExclusion); } private void MapCoversToLocal(params ArtistResource[] artists) diff --git a/src/Lidarr.Api.V1/ImportLists/ImportListExclusionModule.cs b/src/Lidarr.Api.V1/ImportLists/ImportListExclusionModule.cs new file mode 100644 index 000000000..c5fdd0bbe --- /dev/null +++ b/src/Lidarr.Api.V1/ImportLists/ImportListExclusionModule.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using NzbDrone.Core.ImportLists.Exclusions; +using Lidarr.Http; +using FluentValidation; +using NzbDrone.Core.Validation; + +namespace Lidarr.Api.V1.ImportLists +{ + public class ImportListExclusionModule : LidarrRestModule + { + private readonly IImportListExclusionService _importListExclusionService; + + public ImportListExclusionModule(IImportListExclusionService importListExclusionService, + ImportListExclusionExistsValidator importListExclusionExistsValidator, + GuidValidator guidValidator) + { + _importListExclusionService = importListExclusionService; + + GetResourceById = GetImportListExclusion; + GetResourceAll = GetImportListExclusions; + CreateResource = AddImportListExclusion; + UpdateResource = UpdateImportListExclusion; + DeleteResource = DeleteImportListExclusionResource; + + SharedValidator.RuleFor(c => c.ForeignId).NotEmpty().SetValidator(guidValidator).SetValidator(importListExclusionExistsValidator); + SharedValidator.RuleFor(c => c.ArtistName).NotEmpty(); + } + + private ImportListExclusionResource GetImportListExclusion(int id) + { + return _importListExclusionService.Get(id).ToResource(); + } + + private List 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); + } + } +} diff --git a/src/Lidarr.Api.V1/ImportLists/ImportListExclusionResource.cs b/src/Lidarr.Api.V1/ImportLists/ImportListExclusionResource.cs new file mode 100644 index 000000000..91d7f52d3 --- /dev/null +++ b/src/Lidarr.Api.V1/ImportLists/ImportListExclusionResource.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; +using NzbDrone.Core.ImportLists.Exclusions; +using Lidarr.Http.REST; + +namespace Lidarr.Api.V1.ImportLists +{ + public class ImportListExclusionResource : RestResource + { + public string ForeignId { get; set; } + public string ArtistName { get; set; } + } + + public static class ImportListExclusionResourceMapper + { + public static ImportListExclusionResource ToResource(this ImportListExclusion model) + { + if (model == null) return null; + + return new ImportListExclusionResource + { + Id = model.Id, + ForeignId = model.ForeignId, + ArtistName = model.Name, + }; + } + + public static ImportListExclusion ToModel(this ImportListExclusionResource resource) + { + if (resource == null) return null; + + return new ImportListExclusion + { + Id = resource.Id, + ForeignId = resource.ForeignId, + Name = resource.ArtistName + }; + } + + public static List ToResource(this IEnumerable filters) + { + return filters.Select(ToResource).ToList(); + } + } +} diff --git a/src/Lidarr.Api.V1/Lidarr.Api.V1.csproj b/src/Lidarr.Api.V1/Lidarr.Api.V1.csproj index 4a4fd06fd..9dd489523 100644 --- a/src/Lidarr.Api.V1/Lidarr.Api.V1.csproj +++ b/src/Lidarr.Api.V1/Lidarr.Api.V1.csproj @@ -99,6 +99,8 @@ + + diff --git a/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs b/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs index 307e66759..df76a7cc8 100644 --- a/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs +++ b/src/NzbDrone.Core.Test/ImportListTests/ImportListSyncServiceFixture.cs @@ -7,6 +7,7 @@ using NzbDrone.Core.MetadataSource; using NzbDrone.Core.Music; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.ImportLists.Exclusions; namespace NzbDrone.Core.Test.ImportListTests { @@ -43,6 +44,10 @@ namespace NzbDrone.Core.Test.ImportListTests Mocker.GetMock() .Setup(v => v.Fetch()) .Returns(_importListReports); + + Mocker.GetMock() + .Setup(v => v.All()) + .Returns(new List()); } private void WithAlbum() @@ -67,6 +72,17 @@ namespace NzbDrone.Core.Test.ImportListTests .Returns(new Artist{ForeignArtistId = _importListReports.First().ArtistMusicBrainzId }); } + private void WithExcludedArtist() + { + Mocker.GetMock() + .Setup(v => v.All()) + .Returns(new List { + new ImportListExclusion { + ForeignId = "f59c5520-5f46-4d2c-b2c4-822eabf53419" + } + }); + } + [Test] public void should_search_if_artist_title_and_no_artist_id() { @@ -123,7 +139,7 @@ namespace NzbDrone.Core.Test.ImportListTests } [Test] - public void should_not_try_add_if_existing_artist() + public void should_not_add_if_existing_artist() { WithArtistId(); WithAlbum(); @@ -149,6 +165,20 @@ namespace NzbDrone.Core.Test.ImportListTests .Verify(v => v.AddArtists(It.Is>(t => t.Count == 1))); } + [Test] + public void should_not_add_if_excluded_artist() + { + WithArtistId(); + WithAlbum(); + WithAlbumId(); + WithExcludedArtist(); + + Subject.Execute(new ImportListSyncCommand()); + + Mocker.GetMock() + .Verify(v => v.AddArtists(It.Is>(t => t.Count == 0))); + } + [Test] public void should_mark_album_for_monitor_if_album_id() { diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 00dcad6ae..dce67f8c2 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -1,4 +1,4 @@ - + Debug @@ -387,6 +387,7 @@ + @@ -610,9 +611,6 @@ - + - + \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/ValidationTests/GuidValidationFixture.cs b/src/NzbDrone.Core.Test/ValidationTests/GuidValidationFixture.cs new file mode 100644 index 000000000..6c03bedf4 --- /dev/null +++ b/src/NzbDrone.Core.Test/ValidationTests/GuidValidationFixture.cs @@ -0,0 +1,44 @@ +using FizzWare.NBuilder; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Validation; +using NzbDrone.Test.Common; +using NzbDrone.Core.ImportLists.Exclusions; + +namespace NzbDrone.Core.Test.ValidationTests +{ + public class GuidValidationFixture : CoreTest + { + private TestValidator _validator; + + [SetUp] + public void Setup() + { + _validator = new TestValidator + { + v => v.RuleFor(s => s.ForeignId).SetValidator(Subject) + }; + } + + [Test] + public void should_not_be_valid_if_invalid_guid() + { + var listExclusion = Builder.CreateNew() + .With(s => s.ForeignId = "e1f1e33e-2e4c-4d43-b91b-7064068d328") + .Build(); + + _validator.Validate(listExclusion).IsValid.Should().BeFalse(); + } + + [Test] + public void should_be_valid_if_valid_guid() + { + var listExclusion = Builder.CreateNew() + .With(s => s.ForeignId = "e1f1e33e-2e4c-4d43-b91b-7064068d3283") + .Build(); + + _validator.Validate(listExclusion).IsValid.Should().BeTrue(); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/027_add_import_exclusions.cs b/src/NzbDrone.Core/Datastore/Migration/027_add_import_exclusions.cs new file mode 100644 index 000000000..7832ea365 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/027_add_import_exclusions.cs @@ -0,0 +1,16 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(027)] + public class add_import_exclusions : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Create.TableForModel("ImportListExclusions") + .WithColumn("ForeignId").AsString().NotNullable().Unique() + .WithColumn("Name").AsString().NotNullable(); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 20bf4042e..23060b636 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -12,6 +12,7 @@ using NzbDrone.Core.Download; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Indexers; using NzbDrone.Core.ImportLists; +using NzbDrone.Core.ImportLists.Exclusions; using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Jobs; using NzbDrone.Core.MediaFiles; @@ -191,6 +192,7 @@ namespace NzbDrone.Core.Datastore Mapper.Entity().RegisterModel("ImportListStatus"); Mapper.Entity().RegisterModel("CustomFilters"); + Mapper.Entity().RegisterModel("ImportListExclusions"); } private static void RegisterMappers() diff --git a/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusion.cs b/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusion.cs new file mode 100644 index 000000000..e91f58b30 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusion.cs @@ -0,0 +1,10 @@ +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.ImportLists.Exclusions +{ + public class ImportListExclusion : ModelBase + { + public string ForeignId { get; set; } + public string Name { get; set; } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionExistsValidator.cs b/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionExistsValidator.cs new file mode 100644 index 000000000..db2179493 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionExistsValidator.cs @@ -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.ForeignId == context.PropertyValue.ToString())); + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionRepository.cs b/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionRepository.cs new file mode 100644 index 000000000..11e538374 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionRepository.cs @@ -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 FindByForeignId(string foreignId); + } + + public class ImportListExclusionRepository : BasicRepository, IImportListExclusionRepository + { + public ImportListExclusionRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + + public ImportListExclusion FindByForeignId(string foreignId) + { + return Query.Where(m => m.ForeignId == foreignId).SingleOrDefault(); + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs b/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs new file mode 100644 index 000000000..343e04a88 --- /dev/null +++ b/src/NzbDrone.Core/ImportLists/Exclusions/ImportListExclusionService.cs @@ -0,0 +1,80 @@ +using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.Music.Events; +using System.Collections.Generic; +using System.Linq; + +namespace NzbDrone.Core.ImportLists.Exclusions +{ + public interface IImportListExclusionService + { + ImportListExclusion Add(ImportListExclusion importListExclusion); + List All(); + void Delete(int id); + ImportListExclusion Get(int id); + ImportListExclusion FindByForeignId(string foreignId); + ImportListExclusion Update(ImportListExclusion importListExclusion); + } + + public class ImportListExclusionService : IImportListExclusionService, IHandleAsync + { + 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 FindByForeignId(string foreignId) + { + return _repo.FindByForeignId(foreignId); + } + + public List All() + { + return _repo.All().ToList(); + } + + public void HandleAsync(ArtistDeletedEvent message) + { + if (!message.AddImportListExclusion) + { + return; + } + + var existingExclusion = _repo.FindByForeignId(message.Artist.ForeignArtistId); + + if (existingExclusion != null) + { + return; + } + + var importExclusion = new ImportListExclusion + { + ForeignId = message.Artist.ForeignArtistId, + Name = message.Artist.Name + }; + + _repo.Insert(importExclusion); + } + } +} diff --git a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs index 700de0cbb..c771254c9 100644 --- a/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs +++ b/src/NzbDrone.Core/ImportLists/ImportListSyncService.cs @@ -3,6 +3,7 @@ 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.Messaging.Events; using NzbDrone.Core.MetadataSource; @@ -15,6 +16,7 @@ namespace NzbDrone.Core.ImportLists { private readonly IImportListStatusService _importListStatusService; private readonly IImportListFactory _importListFactory; + private readonly IImportListExclusionService _importListExclusionService; private readonly IFetchAndParseImportList _listFetcherAndParser; private readonly ISearchForNewAlbum _albumSearchService; private readonly ISearchForNewArtist _artistSearchService; @@ -25,6 +27,7 @@ namespace NzbDrone.Core.ImportLists public ImportListSyncService(IImportListStatusService importListStatusService, IImportListFactory importListFactory, + IImportListExclusionService importListExclusionService, IFetchAndParseImportList listFetcherAndParser, ISearchForNewAlbum albumSearchService, ISearchForNewArtist artistSearchService, @@ -35,6 +38,7 @@ namespace NzbDrone.Core.ImportLists { _importListStatusService = importListStatusService; _importListFactory = importListFactory; + _importListExclusionService = importListExclusionService; _listFetcherAndParser = listFetcherAndParser; _albumSearchService = albumSearchService; _artistSearchService = artistSearchService; @@ -78,6 +82,8 @@ namespace NzbDrone.Core.ImportLists var reportNumber = 1; + var listExclusions = _importListExclusionService.All(); + foreach (var report in reports) { _logger.ProgressTrace("Processing list item {0}/{1}", reportNumber, reports.Count); @@ -112,9 +118,17 @@ namespace NzbDrone.Core.ImportLists // Check to see if artist in DB var existingArtist = _artistService.FindById(report.ArtistMusicBrainzId); + + // Check to see if artist excluded + var excludedArtist = listExclusions.Where(s => s.ForeignId == report.ArtistMusicBrainzId).SingleOrDefault(); + + if (excludedArtist != null) + { + _logger.Debug("{0} [{1}] Rejected due to list exlcusion", report.ArtistMusicBrainzId, report.Artist); + } // Append Artist if not already in DB or already on add list - if (existingArtist == null && artistsToAdd.All(s => s.Metadata.Value.ForeignArtistId != report.ArtistMusicBrainzId)) + if (existingArtist == null && excludedArtist == null && artistsToAdd.All(s => s.Metadata.Value.ForeignArtistId != report.ArtistMusicBrainzId)) { artistsToAdd.Add(new Artist { diff --git a/src/NzbDrone.Core/Music/ArtistService.cs b/src/NzbDrone.Core/Music/ArtistService.cs index 9d708ec77..8f977ef19 100644 --- a/src/NzbDrone.Core/Music/ArtistService.cs +++ b/src/NzbDrone.Core/Music/ArtistService.cs @@ -4,9 +4,10 @@ using NzbDrone.Core.Music.Events; using System; using System.Collections.Generic; using System.Linq; -using NzbDrone.Core.Parser; using NzbDrone.Common.Extensions; using NzbDrone.Common.Cache; +using NzbDrone.Core.ImportLists.Exclusions; +using NzbDrone.Core.Parser; namespace NzbDrone.Core.Music { @@ -21,7 +22,7 @@ namespace NzbDrone.Core.Music Artist FindByName(string title); Artist FindByNameInexact(string title); List GetCandidates(string title); - void DeleteArtist(int artistId, bool deleteFiles); + void DeleteArtist(int artistId, bool deleteFiles, bool addImportListExclusion = false); List GetAllArtists(); List AllForTag(int tagId); Artist UpdateArtist(Artist artist); @@ -36,6 +37,7 @@ namespace NzbDrone.Core.Music private readonly IArtistMetadataRepository _artistMetadataRepository; private readonly IEventAggregator _eventAggregator; private readonly ITrackService _trackService; + private readonly IImportListExclusionService _importListExclusionService; private readonly IBuildArtistPaths _artistPathBuilder; private readonly Logger _logger; private readonly ICached> _cache; @@ -44,6 +46,7 @@ namespace NzbDrone.Core.Music IArtistMetadataRepository artistMetadataRepository, IEventAggregator eventAggregator, ITrackService trackService, + IImportListExclusionService importListExclusionService, IBuildArtistPaths artistPathBuilder, ICacheManager cacheManager, Logger logger) @@ -52,6 +55,7 @@ namespace NzbDrone.Core.Music _artistMetadataRepository = artistMetadataRepository; _eventAggregator = eventAggregator; _trackService = trackService; + _importListExclusionService = importListExclusionService; _artistPathBuilder = artistPathBuilder; _cache = cacheManager.GetCache>(GetType()); _logger = logger; @@ -82,12 +86,12 @@ namespace NzbDrone.Core.Music return _artistRepository.ArtistPathExists(folder); } - public void DeleteArtist(int artistId, bool deleteFiles) + public void DeleteArtist(int artistId, bool deleteFiles, bool addImportListExclusion = false) { _cache.Clear(); var artist = _artistRepository.Get(artistId); _artistRepository.Delete(artistId); - _eventAggregator.PublishEvent(new ArtistDeletedEvent(artist, deleteFiles)); + _eventAggregator.PublishEvent(new ArtistDeletedEvent(artist, deleteFiles, addImportListExclusion)); } public Artist FindById(string spotifyId) diff --git a/src/NzbDrone.Core/Music/Events/ArtistDeletedEvent.cs b/src/NzbDrone.Core/Music/Events/ArtistDeletedEvent.cs index b889816a6..5c448f133 100644 --- a/src/NzbDrone.Core/Music/Events/ArtistDeletedEvent.cs +++ b/src/NzbDrone.Core/Music/Events/ArtistDeletedEvent.cs @@ -1,4 +1,4 @@ -using NzbDrone.Common.Messaging; +using NzbDrone.Common.Messaging; using System; using System.Collections.Generic; using System.Linq; @@ -10,11 +10,13 @@ namespace NzbDrone.Core.Music.Events { public Artist Artist { get; private set; } public bool DeleteFiles { get; private set; } + public bool AddImportListExclusion { get; private set; } - public ArtistDeletedEvent(Artist artist, bool deleteFiles) + public ArtistDeletedEvent(Artist artist, bool deleteFiles, bool addImportListExclusion) { Artist = artist; DeleteFiles = deleteFiles; + AddImportListExclusion = addImportListExclusion; } } } diff --git a/src/NzbDrone.Core/Music/RefreshArtistService.cs b/src/NzbDrone.Core/Music/RefreshArtistService.cs index a4e0235e9..e916815dc 100644 --- a/src/NzbDrone.Core/Music/RefreshArtistService.cs +++ b/src/NzbDrone.Core/Music/RefreshArtistService.cs @@ -14,6 +14,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using NzbDrone.Core.ImportLists.Exclusions; namespace NzbDrone.Core.Music { @@ -29,6 +30,7 @@ namespace NzbDrone.Core.Music private readonly IDiskScanService _diskScanService; private readonly ICheckIfArtistShouldBeRefreshed _checkIfArtistShouldBeRefreshed; private readonly IConfigService _configService; + private readonly IImportListExclusionService _importListExclusionService; private readonly Logger _logger; public RefreshArtistService(IProvideArtistInfo artistInfo, @@ -41,6 +43,7 @@ namespace NzbDrone.Core.Music IDiskScanService diskScanService, ICheckIfArtistShouldBeRefreshed checkIfArtistShouldBeRefreshed, IConfigService configService, + IImportListExclusionService importListExclusionService, Logger logger) { _artistInfo = artistInfo; @@ -53,6 +56,7 @@ namespace NzbDrone.Core.Music _diskScanService = diskScanService; _checkIfArtistShouldBeRefreshed = checkIfArtistShouldBeRefreshed; _configService = configService; + _importListExclusionService = importListExclusionService; _logger = logger; } @@ -75,6 +79,16 @@ namespace NzbDrone.Core.Music if (artist.Metadata.Value.ForeignArtistId != artistInfo.Metadata.Value.ForeignArtistId) { _logger.Warn("Artist '{0}' (Artist {1}) was replaced with '{2}' (LidarrAPI {3}), because the original was a duplicate.", artist.Name, artist.Metadata.Value.ForeignArtistId, artistInfo.Name, artistInfo.Metadata.Value.ForeignArtistId); + + // Update list exclusion if one exists + var importExclusion = _importListExclusionService.FindByForeignId(artist.Metadata.Value.ForeignArtistId); + + if (importExclusion != null) + { + importExclusion.ForeignId = artistInfo.Metadata.Value.ForeignArtistId; + _importListExclusionService.Update(importExclusion); + } + artist.Metadata.Value.ForeignArtistId = artistInfo.Metadata.Value.ForeignArtistId; } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index e8abd34ae..d20c63480 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -194,6 +194,7 @@ + @@ -545,6 +546,10 @@ + + + + @@ -1200,6 +1205,7 @@ + diff --git a/src/NzbDrone.Core/Validation/GuidValidator.cs b/src/NzbDrone.Core/Validation/GuidValidator.cs new file mode 100644 index 000000000..37e9d0a9d --- /dev/null +++ b/src/NzbDrone.Core/Validation/GuidValidator.cs @@ -0,0 +1,20 @@ +using System; +using FluentValidation.Validators; + +namespace NzbDrone.Core.Validation +{ + public class GuidValidator : PropertyValidator + { + public GuidValidator() + : base("String is not a valid Guid") + { + } + + protected override bool IsValid(PropertyValidatorContext context) + { + if (context.PropertyValue == null) return false; + + return Guid.TryParse(context.PropertyValue.ToString(), out Guid guidOutput); + } + } +}