mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-16 10:03:51 -07:00
New: Import List Exclusions (#608)
* New: Import List Exclusions * Fixed: ImportExclusion ForeignId Checks, Unique. RefreshArtist Duplicate * Fixed: Copy/Paste typos
This commit is contained in:
parent
b9cc94aa46
commit
42c16c227e
35 changed files with 1160 additions and 20 deletions
|
@ -22,7 +22,8 @@ class DeleteArtistModalContent extends Component {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
deleteFiles: false
|
deleteFiles: false,
|
||||||
|
addImportListExclusion: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,11 +34,17 @@ class DeleteArtistModalContent extends Component {
|
||||||
this.setState({ deleteFiles: value });
|
this.setState({ deleteFiles: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onAddImportListExclusionChange = ({ value }) => {
|
||||||
|
this.setState({ addImportListExclusion: value });
|
||||||
|
}
|
||||||
|
|
||||||
onDeleteArtistConfirmed = () => {
|
onDeleteArtistConfirmed = () => {
|
||||||
const deleteFiles = this.state.deleteFiles;
|
const deleteFiles = this.state.deleteFiles;
|
||||||
|
const addImportListExclusion = this.state.addImportListExclusion;
|
||||||
|
|
||||||
this.setState({ deleteFiles: false });
|
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;
|
} = statistics;
|
||||||
|
|
||||||
const deleteFiles = this.state.deleteFiles;
|
const deleteFiles = this.state.deleteFiles;
|
||||||
|
const addImportListExclusion = this.state.addImportListExclusion;
|
||||||
|
|
||||||
let deleteFilesLabel = `Delete ${trackFileCount} Track Files`;
|
let deleteFilesLabel = `Delete ${trackFileCount} Track Files`;
|
||||||
let deleteFilesHelpText = 'Delete the track files and artist folder';
|
let deleteFilesHelpText = 'Delete the track files and artist folder';
|
||||||
|
|
||||||
|
@ -96,6 +105,19 @@ class DeleteArtistModalContent extends Component {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Add List Exclusion</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="addImportListExclusion"
|
||||||
|
value={addImportListExclusion}
|
||||||
|
helpText="Prevent artist from being added to Lidarr by Import lists"
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
onChange={this.onAddImportListExclusionChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
{
|
{
|
||||||
deleteFiles &&
|
deleteFiles &&
|
||||||
<div className={styles.deleteFilesMessage}>
|
<div className={styles.deleteFilesMessage}>
|
||||||
|
|
|
@ -24,10 +24,11 @@ class DeleteArtistModalContentConnector extends Component {
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onDeletePress = (deleteFiles) => {
|
onDeletePress = (deleteFiles, addImportListExclusion) => {
|
||||||
this.props.deleteArtist({
|
this.props.deleteArtist({
|
||||||
id: this.props.artistId,
|
id: this.props.artistId,
|
||||||
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 } 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 (
|
||||||
|
<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>Artist Name</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="artistName"
|
||||||
|
helpText="The name of the artist to exclude (can be anything meaningful)"
|
||||||
|
{...artistName}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Musicbrainz Id</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="foreignId"
|
||||||
|
helpText="The Musicbrainz Id of the artist to exclude"
|
||||||
|
{...foreignId}
|
||||||
|
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 = {
|
||||||
|
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;
|
|
@ -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 (
|
||||||
|
<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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artistName {
|
||||||
|
flex: 0 0 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.foreignId {
|
||||||
|
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,
|
||||||
|
artistName,
|
||||||
|
foreignId
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.importListExclusion,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={styles.artistName}>{artistName}</div>
|
||||||
|
<div className={styles.foreignId}>{foreignId}</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,
|
||||||
|
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;
|
|
@ -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}>Name</div>
|
||||||
|
<div className={styles.path}>Foreign 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);
|
|
@ -7,6 +7,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||||
import ImportListsConnector from './ImportLists/ImportListsConnector';
|
import ImportListsConnector from './ImportLists/ImportListsConnector';
|
||||||
|
import ImportListsExclusionsConnector from './ImportListExclusions/ImportListExclusionsConnector';
|
||||||
|
|
||||||
class ImportListSettings extends Component {
|
class ImportListSettings extends Component {
|
||||||
|
|
||||||
|
@ -74,6 +75,7 @@ class ImportListSettings extends Component {
|
||||||
|
|
||||||
<PageContentBodyConnector>
|
<PageContentBodyConnector>
|
||||||
<ImportListsConnector />
|
<ImportListsConnector />
|
||||||
|
<ImportListsExclusionsConnector />
|
||||||
</PageContentBodyConnector>
|
</PageContentBodyConnector>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
|
|
69
frontend/src/Store/Actions/Settings/importListExclusions.js
Normal file
69
frontend/src/Store/Actions/Settings/importListExclusions.js
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
|
@ -186,7 +186,8 @@ export const deleteArtist = createThunk(DELETE_ARTIST, (payload) => {
|
||||||
return {
|
return {
|
||||||
...payload,
|
...payload,
|
||||||
queryParams: {
|
queryParams: {
|
||||||
deleteFiles: payload.deleteFiles
|
deleteFiles: payload.deleteFiles,
|
||||||
|
addImportListExclusion: payload.addImportListExclusion
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,6 +9,7 @@ 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';
|
||||||
import importLists from './Settings/importLists';
|
import importLists from './Settings/importLists';
|
||||||
|
import importListExclusions from './Settings/importListExclusions';
|
||||||
import metadataProfiles from './Settings/metadataProfiles';
|
import metadataProfiles from './Settings/metadataProfiles';
|
||||||
import mediaManagement from './Settings/mediaManagement';
|
import mediaManagement from './Settings/mediaManagement';
|
||||||
import metadata from './Settings/metadata';
|
import metadata from './Settings/metadata';
|
||||||
|
@ -27,6 +28,7 @@ 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/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';
|
||||||
|
@ -62,6 +64,7 @@ export const defaultState = {
|
||||||
indexers: indexers.defaultState,
|
indexers: indexers.defaultState,
|
||||||
languageProfiles: languageProfiles.defaultState,
|
languageProfiles: languageProfiles.defaultState,
|
||||||
importLists: importLists.defaultState,
|
importLists: importLists.defaultState,
|
||||||
|
importListExclusions: importListExclusions.defaultState,
|
||||||
metadataProfiles: metadataProfiles.defaultState,
|
metadataProfiles: metadataProfiles.defaultState,
|
||||||
mediaManagement: mediaManagement.defaultState,
|
mediaManagement: mediaManagement.defaultState,
|
||||||
metadata: metadata.defaultState,
|
metadata: metadata.defaultState,
|
||||||
|
@ -102,6 +105,7 @@ export const actionHandlers = handleThunks({
|
||||||
...indexers.actionHandlers,
|
...indexers.actionHandlers,
|
||||||
...languageProfiles.actionHandlers,
|
...languageProfiles.actionHandlers,
|
||||||
...importLists.actionHandlers,
|
...importLists.actionHandlers,
|
||||||
|
...importListExclusions.actionHandlers,
|
||||||
...metadataProfiles.actionHandlers,
|
...metadataProfiles.actionHandlers,
|
||||||
...mediaManagement.actionHandlers,
|
...mediaManagement.actionHandlers,
|
||||||
...metadata.actionHandlers,
|
...metadata.actionHandlers,
|
||||||
|
@ -133,6 +137,7 @@ export const reducers = createHandleActions({
|
||||||
...indexers.reducers,
|
...indexers.reducers,
|
||||||
...languageProfiles.reducers,
|
...languageProfiles.reducers,
|
||||||
...importLists.reducers,
|
...importLists.reducers,
|
||||||
|
...importListExclusions.reducers,
|
||||||
...metadataProfiles.reducers,
|
...metadataProfiles.reducers,
|
||||||
...mediaManagement.reducers,
|
...mediaManagement.reducers,
|
||||||
...metadata.reducers,
|
...metadata.reducers,
|
||||||
|
|
|
@ -171,8 +171,9 @@ namespace Lidarr.Api.V1.Artist
|
||||||
private void DeleteArtist(int id)
|
private void DeleteArtist(int id)
|
||||||
{
|
{
|
||||||
var deleteFiles = Request.GetBooleanQueryParameter("deleteFiles");
|
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)
|
private void MapCoversToLocal(params ArtistResource[] artists)
|
||||||
|
|
56
src/Lidarr.Api.V1/ImportLists/ImportListExclusionModule.cs
Normal file
56
src/Lidarr.Api.V1/ImportLists/ImportListExclusionModule.cs
Normal file
|
@ -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<ImportListExclusionResource>
|
||||||
|
{
|
||||||
|
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<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
src/Lidarr.Api.V1/ImportLists/ImportListExclusionResource.cs
Normal file
45
src/Lidarr.Api.V1/ImportLists/ImportListExclusionResource.cs
Normal file
|
@ -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<ImportListExclusionResource> ToResource(this IEnumerable<ImportListExclusion> filters)
|
||||||
|
{
|
||||||
|
return filters.Select(ToResource).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -99,6 +99,8 @@
|
||||||
<Compile Include="Config\MetadataProviderConfigResource.cs" />
|
<Compile Include="Config\MetadataProviderConfigResource.cs" />
|
||||||
<Compile Include="CustomFilters\CustomFilterModule.cs" />
|
<Compile Include="CustomFilters\CustomFilterModule.cs" />
|
||||||
<Compile Include="CustomFilters\CustomFilterResource.cs" />
|
<Compile Include="CustomFilters\CustomFilterResource.cs" />
|
||||||
|
<Compile Include="ImportLists\ImportListExclusionModule.cs" />
|
||||||
|
<Compile Include="ImportLists\ImportListExclusionResource.cs" />
|
||||||
<Compile Include="ImportLists\ImportListModule.cs" />
|
<Compile Include="ImportLists\ImportListModule.cs" />
|
||||||
<Compile Include="ImportLists\ImportListResource.cs" />
|
<Compile Include="ImportLists\ImportListResource.cs" />
|
||||||
<Compile Include="Profiles\Metadata\MetadataProfileModule.cs" />
|
<Compile Include="Profiles\Metadata\MetadataProfileModule.cs" />
|
||||||
|
|
|
@ -7,6 +7,7 @@ using NzbDrone.Core.MetadataSource;
|
||||||
using NzbDrone.Core.Music;
|
using NzbDrone.Core.Music;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
using NzbDrone.Core.ImportLists.Exclusions;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.ImportListTests
|
namespace NzbDrone.Core.Test.ImportListTests
|
||||||
{
|
{
|
||||||
|
@ -43,6 +44,10 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||||
Mocker.GetMock<IFetchAndParseImportList>()
|
Mocker.GetMock<IFetchAndParseImportList>()
|
||||||
.Setup(v => v.Fetch())
|
.Setup(v => v.Fetch())
|
||||||
.Returns(_importListReports);
|
.Returns(_importListReports);
|
||||||
|
|
||||||
|
Mocker.GetMock<IImportListExclusionService>()
|
||||||
|
.Setup(v => v.All())
|
||||||
|
.Returns(new List<ImportListExclusion>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WithAlbum()
|
private void WithAlbum()
|
||||||
|
@ -67,6 +72,17 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||||
.Returns(new Artist{ForeignArtistId = _importListReports.First().ArtistMusicBrainzId });
|
.Returns(new Artist{ForeignArtistId = _importListReports.First().ArtistMusicBrainzId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void WithExcludedArtist()
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IImportListExclusionService>()
|
||||||
|
.Setup(v => v.All())
|
||||||
|
.Returns(new List<ImportListExclusion> {
|
||||||
|
new ImportListExclusion {
|
||||||
|
ForeignId = "f59c5520-5f46-4d2c-b2c4-822eabf53419"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_search_if_artist_title_and_no_artist_id()
|
public void should_search_if_artist_title_and_no_artist_id()
|
||||||
{
|
{
|
||||||
|
@ -123,7 +139,7 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_not_try_add_if_existing_artist()
|
public void should_not_add_if_existing_artist()
|
||||||
{
|
{
|
||||||
WithArtistId();
|
WithArtistId();
|
||||||
WithAlbum();
|
WithAlbum();
|
||||||
|
@ -149,6 +165,20 @@ namespace NzbDrone.Core.Test.ImportListTests
|
||||||
.Verify(v => v.AddArtists(It.Is<List<Artist>>(t => t.Count == 1)));
|
.Verify(v => v.AddArtists(It.Is<List<Artist>>(t => t.Count == 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_add_if_excluded_artist()
|
||||||
|
{
|
||||||
|
WithArtistId();
|
||||||
|
WithAlbum();
|
||||||
|
WithAlbumId();
|
||||||
|
WithExcludedArtist();
|
||||||
|
|
||||||
|
Subject.Execute(new ImportListSyncCommand());
|
||||||
|
|
||||||
|
Mocker.GetMock<IAddArtistService>()
|
||||||
|
.Verify(v => v.AddArtists(It.Is<List<Artist>>(t => t.Count == 0)));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_mark_album_for_monitor_if_album_id()
|
public void should_mark_album_for_monitor_if_album_id()
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
@ -387,6 +387,7 @@
|
||||||
<Compile Include="ThingiProviderTests\ProviderStatusServiceFixture.cs" />
|
<Compile Include="ThingiProviderTests\ProviderStatusServiceFixture.cs" />
|
||||||
<Compile Include="UpdateTests\UpdatePackageProviderFixture.cs" />
|
<Compile Include="UpdateTests\UpdatePackageProviderFixture.cs" />
|
||||||
<Compile Include="UpdateTests\UpdateServiceFixture.cs" />
|
<Compile Include="UpdateTests\UpdateServiceFixture.cs" />
|
||||||
|
<Compile Include="ValidationTests\GuidValidationFixture.cs" />
|
||||||
<Compile Include="ValidationTests\SystemFolderValidatorFixture.cs" />
|
<Compile Include="ValidationTests\SystemFolderValidatorFixture.cs" />
|
||||||
<Compile Include="XbmcVersionTests.cs" />
|
<Compile Include="XbmcVersionTests.cs" />
|
||||||
<None Include="Files\Nzbs\NoFiles.nzb">
|
<None Include="Files\Nzbs\NoFiles.nzb">
|
||||||
|
@ -610,9 +611,6 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<IdentificationTestCases Include="Files\Identification\*.json" />
|
<IdentificationTestCases Include="Files\Identification\*.json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Copy
|
<Copy SourceFiles="@(IdentificationTestCases)" DestinationFolder="$(OutputPath)\Files\Identification\" SkipUnchangedFiles="true" />
|
||||||
SourceFiles="@(IdentificationTestCases)"
|
|
||||||
DestinationFolder="$(OutputPath)\Files\Identification\"
|
|
||||||
SkipUnchangedFiles="true"/>
|
|
||||||
</Target>
|
</Target>
|
||||||
</Project>
|
</Project>
|
|
@ -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<GuidValidator>
|
||||||
|
{
|
||||||
|
private TestValidator<ImportListExclusion> _validator;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_validator = new TestValidator<ImportListExclusion>
|
||||||
|
{
|
||||||
|
v => v.RuleFor(s => s.ForeignId).SetValidator(Subject)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_be_valid_if_invalid_guid()
|
||||||
|
{
|
||||||
|
var listExclusion = Builder<ImportListExclusion>.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<ImportListExclusion>.CreateNew()
|
||||||
|
.With(s => s.ForeignId = "e1f1e33e-2e4c-4d43-b91b-7064068d3283")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_validator.Validate(listExclusion).IsValid.Should().BeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ using NzbDrone.Core.Download;
|
||||||
using NzbDrone.Core.Download.Pending;
|
using NzbDrone.Core.Download.Pending;
|
||||||
using NzbDrone.Core.Indexers;
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.ImportLists;
|
using NzbDrone.Core.ImportLists;
|
||||||
|
using NzbDrone.Core.ImportLists.Exclusions;
|
||||||
using NzbDrone.Core.Instrumentation;
|
using NzbDrone.Core.Instrumentation;
|
||||||
using NzbDrone.Core.Jobs;
|
using NzbDrone.Core.Jobs;
|
||||||
using NzbDrone.Core.MediaFiles;
|
using NzbDrone.Core.MediaFiles;
|
||||||
|
@ -191,6 +192,7 @@ namespace NzbDrone.Core.Datastore
|
||||||
Mapper.Entity<ImportListStatus>().RegisterModel("ImportListStatus");
|
Mapper.Entity<ImportListStatus>().RegisterModel("ImportListStatus");
|
||||||
|
|
||||||
Mapper.Entity<CustomFilter>().RegisterModel("CustomFilters");
|
Mapper.Entity<CustomFilter>().RegisterModel("CustomFilters");
|
||||||
|
Mapper.Entity<ImportListExclusion>().RegisterModel("ImportListExclusions");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RegisterMappers()
|
private static void RegisterMappers()
|
||||||
|
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 FindByForeignId(string foreignId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ImportListExclusionRepository : BasicRepository<ImportListExclusion>, IImportListExclusionRepository
|
||||||
|
{
|
||||||
|
public ImportListExclusionRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||||
|
: base(database, eventAggregator)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImportListExclusion FindByForeignId(string foreignId)
|
||||||
|
{
|
||||||
|
return Query.Where<ImportListExclusion>(m => m.ForeignId == foreignId).SingleOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ImportListExclusion> All();
|
||||||
|
void Delete(int id);
|
||||||
|
ImportListExclusion Get(int id);
|
||||||
|
ImportListExclusion FindByForeignId(string foreignId);
|
||||||
|
ImportListExclusion Update(ImportListExclusion importListExclusion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ImportListExclusionService : IImportListExclusionService, IHandleAsync<ArtistDeletedEvent>
|
||||||
|
{
|
||||||
|
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<ImportListExclusion> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Instrumentation.Extensions;
|
using NzbDrone.Common.Instrumentation.Extensions;
|
||||||
|
using NzbDrone.Core.ImportLists.Exclusions;
|
||||||
using NzbDrone.Core.Messaging.Commands;
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.MetadataSource;
|
using NzbDrone.Core.MetadataSource;
|
||||||
|
@ -15,6 +16,7 @@ namespace NzbDrone.Core.ImportLists
|
||||||
{
|
{
|
||||||
private readonly IImportListStatusService _importListStatusService;
|
private readonly IImportListStatusService _importListStatusService;
|
||||||
private readonly IImportListFactory _importListFactory;
|
private readonly IImportListFactory _importListFactory;
|
||||||
|
private readonly IImportListExclusionService _importListExclusionService;
|
||||||
private readonly IFetchAndParseImportList _listFetcherAndParser;
|
private readonly IFetchAndParseImportList _listFetcherAndParser;
|
||||||
private readonly ISearchForNewAlbum _albumSearchService;
|
private readonly ISearchForNewAlbum _albumSearchService;
|
||||||
private readonly ISearchForNewArtist _artistSearchService;
|
private readonly ISearchForNewArtist _artistSearchService;
|
||||||
|
@ -25,6 +27,7 @@ namespace NzbDrone.Core.ImportLists
|
||||||
|
|
||||||
public ImportListSyncService(IImportListStatusService importListStatusService,
|
public ImportListSyncService(IImportListStatusService importListStatusService,
|
||||||
IImportListFactory importListFactory,
|
IImportListFactory importListFactory,
|
||||||
|
IImportListExclusionService importListExclusionService,
|
||||||
IFetchAndParseImportList listFetcherAndParser,
|
IFetchAndParseImportList listFetcherAndParser,
|
||||||
ISearchForNewAlbum albumSearchService,
|
ISearchForNewAlbum albumSearchService,
|
||||||
ISearchForNewArtist artistSearchService,
|
ISearchForNewArtist artistSearchService,
|
||||||
|
@ -35,6 +38,7 @@ namespace NzbDrone.Core.ImportLists
|
||||||
{
|
{
|
||||||
_importListStatusService = importListStatusService;
|
_importListStatusService = importListStatusService;
|
||||||
_importListFactory = importListFactory;
|
_importListFactory = importListFactory;
|
||||||
|
_importListExclusionService = importListExclusionService;
|
||||||
_listFetcherAndParser = listFetcherAndParser;
|
_listFetcherAndParser = listFetcherAndParser;
|
||||||
_albumSearchService = albumSearchService;
|
_albumSearchService = albumSearchService;
|
||||||
_artistSearchService = artistSearchService;
|
_artistSearchService = artistSearchService;
|
||||||
|
@ -78,6 +82,8 @@ namespace NzbDrone.Core.ImportLists
|
||||||
|
|
||||||
var reportNumber = 1;
|
var reportNumber = 1;
|
||||||
|
|
||||||
|
var listExclusions = _importListExclusionService.All();
|
||||||
|
|
||||||
foreach (var report in reports)
|
foreach (var report in reports)
|
||||||
{
|
{
|
||||||
_logger.ProgressTrace("Processing list item {0}/{1}", reportNumber, reports.Count);
|
_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
|
// Check to see if artist in DB
|
||||||
var existingArtist = _artistService.FindById(report.ArtistMusicBrainzId);
|
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
|
// 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
|
artistsToAdd.Add(new Artist
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,9 +4,10 @@ using NzbDrone.Core.Music.Events;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NzbDrone.Core.Parser;
|
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Common.Cache;
|
using NzbDrone.Common.Cache;
|
||||||
|
using NzbDrone.Core.ImportLists.Exclusions;
|
||||||
|
using NzbDrone.Core.Parser;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Music
|
namespace NzbDrone.Core.Music
|
||||||
{
|
{
|
||||||
|
@ -21,7 +22,7 @@ namespace NzbDrone.Core.Music
|
||||||
Artist FindByName(string title);
|
Artist FindByName(string title);
|
||||||
Artist FindByNameInexact(string title);
|
Artist FindByNameInexact(string title);
|
||||||
List<Artist> GetCandidates(string title);
|
List<Artist> GetCandidates(string title);
|
||||||
void DeleteArtist(int artistId, bool deleteFiles);
|
void DeleteArtist(int artistId, bool deleteFiles, bool addImportListExclusion = false);
|
||||||
List<Artist> GetAllArtists();
|
List<Artist> GetAllArtists();
|
||||||
List<Artist> AllForTag(int tagId);
|
List<Artist> AllForTag(int tagId);
|
||||||
Artist UpdateArtist(Artist artist);
|
Artist UpdateArtist(Artist artist);
|
||||||
|
@ -36,6 +37,7 @@ namespace NzbDrone.Core.Music
|
||||||
private readonly IArtistMetadataRepository _artistMetadataRepository;
|
private readonly IArtistMetadataRepository _artistMetadataRepository;
|
||||||
private readonly IEventAggregator _eventAggregator;
|
private readonly IEventAggregator _eventAggregator;
|
||||||
private readonly ITrackService _trackService;
|
private readonly ITrackService _trackService;
|
||||||
|
private readonly IImportListExclusionService _importListExclusionService;
|
||||||
private readonly IBuildArtistPaths _artistPathBuilder;
|
private readonly IBuildArtistPaths _artistPathBuilder;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
private readonly ICached<List<Artist>> _cache;
|
private readonly ICached<List<Artist>> _cache;
|
||||||
|
@ -44,6 +46,7 @@ namespace NzbDrone.Core.Music
|
||||||
IArtistMetadataRepository artistMetadataRepository,
|
IArtistMetadataRepository artistMetadataRepository,
|
||||||
IEventAggregator eventAggregator,
|
IEventAggregator eventAggregator,
|
||||||
ITrackService trackService,
|
ITrackService trackService,
|
||||||
|
IImportListExclusionService importListExclusionService,
|
||||||
IBuildArtistPaths artistPathBuilder,
|
IBuildArtistPaths artistPathBuilder,
|
||||||
ICacheManager cacheManager,
|
ICacheManager cacheManager,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
|
@ -52,6 +55,7 @@ namespace NzbDrone.Core.Music
|
||||||
_artistMetadataRepository = artistMetadataRepository;
|
_artistMetadataRepository = artistMetadataRepository;
|
||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
_trackService = trackService;
|
_trackService = trackService;
|
||||||
|
_importListExclusionService = importListExclusionService;
|
||||||
_artistPathBuilder = artistPathBuilder;
|
_artistPathBuilder = artistPathBuilder;
|
||||||
_cache = cacheManager.GetCache<List<Artist>>(GetType());
|
_cache = cacheManager.GetCache<List<Artist>>(GetType());
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
@ -82,12 +86,12 @@ namespace NzbDrone.Core.Music
|
||||||
return _artistRepository.ArtistPathExists(folder);
|
return _artistRepository.ArtistPathExists(folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeleteArtist(int artistId, bool deleteFiles)
|
public void DeleteArtist(int artistId, bool deleteFiles, bool addImportListExclusion = false)
|
||||||
{
|
{
|
||||||
_cache.Clear();
|
_cache.Clear();
|
||||||
var artist = _artistRepository.Get(artistId);
|
var artist = _artistRepository.Get(artistId);
|
||||||
_artistRepository.Delete(artistId);
|
_artistRepository.Delete(artistId);
|
||||||
_eventAggregator.PublishEvent(new ArtistDeletedEvent(artist, deleteFiles));
|
_eventAggregator.PublishEvent(new ArtistDeletedEvent(artist, deleteFiles, addImportListExclusion));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Artist FindById(string spotifyId)
|
public Artist FindById(string spotifyId)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using NzbDrone.Common.Messaging;
|
using NzbDrone.Common.Messaging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
@ -10,11 +10,13 @@ namespace NzbDrone.Core.Music.Events
|
||||||
{
|
{
|
||||||
public Artist Artist { get; private set; }
|
public Artist Artist { get; private set; }
|
||||||
public bool DeleteFiles { 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;
|
Artist = artist;
|
||||||
DeleteFiles = deleteFiles;
|
DeleteFiles = deleteFiles;
|
||||||
|
AddImportListExclusion = addImportListExclusion;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using NzbDrone.Core.ImportLists.Exclusions;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Music
|
namespace NzbDrone.Core.Music
|
||||||
{
|
{
|
||||||
|
@ -29,6 +30,7 @@ namespace NzbDrone.Core.Music
|
||||||
private readonly IDiskScanService _diskScanService;
|
private readonly IDiskScanService _diskScanService;
|
||||||
private readonly ICheckIfArtistShouldBeRefreshed _checkIfArtistShouldBeRefreshed;
|
private readonly ICheckIfArtistShouldBeRefreshed _checkIfArtistShouldBeRefreshed;
|
||||||
private readonly IConfigService _configService;
|
private readonly IConfigService _configService;
|
||||||
|
private readonly IImportListExclusionService _importListExclusionService;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public RefreshArtistService(IProvideArtistInfo artistInfo,
|
public RefreshArtistService(IProvideArtistInfo artistInfo,
|
||||||
|
@ -41,6 +43,7 @@ namespace NzbDrone.Core.Music
|
||||||
IDiskScanService diskScanService,
|
IDiskScanService diskScanService,
|
||||||
ICheckIfArtistShouldBeRefreshed checkIfArtistShouldBeRefreshed,
|
ICheckIfArtistShouldBeRefreshed checkIfArtistShouldBeRefreshed,
|
||||||
IConfigService configService,
|
IConfigService configService,
|
||||||
|
IImportListExclusionService importListExclusionService,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
{
|
{
|
||||||
_artistInfo = artistInfo;
|
_artistInfo = artistInfo;
|
||||||
|
@ -53,6 +56,7 @@ namespace NzbDrone.Core.Music
|
||||||
_diskScanService = diskScanService;
|
_diskScanService = diskScanService;
|
||||||
_checkIfArtistShouldBeRefreshed = checkIfArtistShouldBeRefreshed;
|
_checkIfArtistShouldBeRefreshed = checkIfArtistShouldBeRefreshed;
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
|
_importListExclusionService = importListExclusionService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,6 +79,16 @@ namespace NzbDrone.Core.Music
|
||||||
if (artist.Metadata.Value.ForeignArtistId != artistInfo.Metadata.Value.ForeignArtistId)
|
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);
|
_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;
|
artist.Metadata.Value.ForeignArtistId = artistInfo.Metadata.Value.ForeignArtistId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -194,6 +194,7 @@
|
||||||
<Compile Include="Datastore\Migration\017_remove_nma.cs" />
|
<Compile Include="Datastore\Migration\017_remove_nma.cs" />
|
||||||
<Compile Include="Datastore\Migration\018_album_disambiguation.cs" />
|
<Compile Include="Datastore\Migration\018_album_disambiguation.cs" />
|
||||||
<Compile Include="Datastore\Migration\019_add_ape_quality_in_profiles.cs" />
|
<Compile Include="Datastore\Migration\019_add_ape_quality_in_profiles.cs" />
|
||||||
|
<Compile Include="Datastore\Migration\027_add_import_exclusions.cs" />
|
||||||
<Compile Include="Datastore\Migration\021_add_custom_filters.cs" />
|
<Compile Include="Datastore\Migration\021_add_custom_filters.cs" />
|
||||||
<Compile Include="Datastore\Migration\022_import_list_tags.cs" />
|
<Compile Include="Datastore\Migration\022_import_list_tags.cs" />
|
||||||
<Compile Include="Datastore\Migration\023_add_release_groups_etc.cs" />
|
<Compile Include="Datastore\Migration\023_add_release_groups_etc.cs" />
|
||||||
|
@ -545,6 +546,10 @@
|
||||||
<Compile Include="Http\HttpProxySettingsProvider.cs" />
|
<Compile Include="Http\HttpProxySettingsProvider.cs" />
|
||||||
<Compile Include="Http\TorcacheHttpInterceptor.cs" />
|
<Compile Include="Http\TorcacheHttpInterceptor.cs" />
|
||||||
<Compile Include="ImportLists\Exceptions\ImportListException.cs" />
|
<Compile Include="ImportLists\Exceptions\ImportListException.cs" />
|
||||||
|
<Compile Include="ImportLists\Exclusions\ImportListExclusionExistsValidator.cs" />
|
||||||
|
<Compile Include="ImportLists\Exclusions\ImportListExclusionService.cs" />
|
||||||
|
<Compile Include="ImportLists\Exclusions\ImportListExclusionRepository.cs" />
|
||||||
|
<Compile Include="ImportLists\Exclusions\ImportListExclusion.cs" />
|
||||||
<Compile Include="ImportLists\FetchAndParseImportListService.cs" />
|
<Compile Include="ImportLists\FetchAndParseImportListService.cs" />
|
||||||
<Compile Include="ImportLists\HeadphonesImport\HeadphonesImport.cs" />
|
<Compile Include="ImportLists\HeadphonesImport\HeadphonesImport.cs" />
|
||||||
<Compile Include="ImportLists\HeadphonesImport\HeadphonesImportApi.cs" />
|
<Compile Include="ImportLists\HeadphonesImport\HeadphonesImportApi.cs" />
|
||||||
|
@ -1200,6 +1205,7 @@
|
||||||
<Compile Include="Update\UpdateVerification.cs" />
|
<Compile Include="Update\UpdateVerification.cs" />
|
||||||
<Compile Include="Update\UpdateVerificationFailedException.cs" />
|
<Compile Include="Update\UpdateVerificationFailedException.cs" />
|
||||||
<Compile Include="Validation\FolderValidator.cs" />
|
<Compile Include="Validation\FolderValidator.cs" />
|
||||||
|
<Compile Include="Validation\GuidValidator.cs" />
|
||||||
<Compile Include="Validation\IpValidation.cs" />
|
<Compile Include="Validation\IpValidation.cs" />
|
||||||
<Compile Include="Validation\LanguageProfileExistsValidator.cs" />
|
<Compile Include="Validation\LanguageProfileExistsValidator.cs" />
|
||||||
<Compile Include="Validation\MetadataProfileExistsValidator.cs" />
|
<Compile Include="Validation\MetadataProfileExistsValidator.cs" />
|
||||||
|
|
20
src/NzbDrone.Core/Validation/GuidValidator.cs
Normal file
20
src/NzbDrone.Core/Validation/GuidValidator.cs
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue