1
0
mirror of https://github.com/Radarr/Radarr.git synced 2024-10-05 15:47:20 +02:00

New: Convert restrictions to release profiles

This commit is contained in:
ricci2511 2023-07-17 17:07:59 +02:00 committed by Qstick
parent ca93a72d63
commit 99ff6aa9c4
59 changed files with 1238 additions and 778 deletions

View File

@ -13,6 +13,7 @@ import EnhancedSelectInput from './EnhancedSelectInput';
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector'; import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
import FormInputHelpText from './FormInputHelpText'; import FormInputHelpText from './FormInputHelpText';
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector'; import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
import KeyValueListInput from './KeyValueListInput'; import KeyValueListInput from './KeyValueListInput';
import LanguageSelectInputConnector from './LanguageSelectInputConnector'; import LanguageSelectInputConnector from './LanguageSelectInputConnector';
import MovieMonitoredSelectInput from './MovieMonitoredSelectInput'; import MovieMonitoredSelectInput from './MovieMonitoredSelectInput';
@ -65,6 +66,9 @@ function getComponent(type) {
case inputTypes.QUALITY_PROFILE_SELECT: case inputTypes.QUALITY_PROFILE_SELECT:
return QualityProfileSelectInputConnector; return QualityProfileSelectInputConnector;
case inputTypes.INDEXER_SELECT:
return IndexerSelectInputConnector;
case inputTypes.MOVIE_MONITORED_SELECT: case inputTypes.MOVIE_MONITORED_SELECT:
return MovieMonitoredSelectInput; return MovieMonitoredSelectInput;

View File

@ -0,0 +1,93 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchIndexers } from 'Store/Actions/settingsActions';
import sortByName from 'Utilities/Array/sortByName';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.indexers,
(state, { includeAny }) => includeAny,
(indexers, includeAny) => {
const {
isFetching,
isPopulated,
error,
items
} = indexers;
const values = items.sort(sortByName).map((indexer) => ({
key: indexer.id,
value: indexer.name
}));
if (includeAny) {
values.unshift({
key: 0,
value: '(Any)'
});
}
return {
isFetching,
isPopulated,
error,
values
};
}
);
}
const mapDispatchToProps = {
dispatchFetchIndexers: fetchIndexers
};
class IndexerSelectInputConnector extends Component {
//
// Lifecycle
componentDidMount() {
if (!this.props.isPopulated) {
this.props.dispatchFetchIndexers();
}
}
//
// Listeners
onChange = ({ name, value }) => {
this.props.onChange({ name, value: parseInt(value) });
};
//
// Render
render() {
return (
<EnhancedSelectInput
{...this.props}
onChange={this.onChange}
/>
);
}
}
IndexerSelectInputConnector.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
includeAny: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
dispatchFetchIndexers: PropTypes.func.isRequired
};
IndexerSelectInputConnector.defaultProps = {
includeAny: false
};
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerSelectInputConnector);

View File

@ -46,13 +46,13 @@ class TextTagInputConnector extends Component {
// to oddities with restrictions (as an example). // to oddities with restrictions (as an example).
const newValue = [...valueArray]; const newValue = [...valueArray];
const newTags = split(tag.name); const newTags = tag.name.startsWith('/') ? [tag.name] : split(tag.name);
newTags.forEach((newTag) => { newTags.forEach((newTag) => {
newValue.push(newTag.trim()); newValue.push(newTag.trim());
}); });
onChange({ name, value: newValue.join(',') }); onChange({ name, value: newValue });
}; };
onTagDelete = ({ index }) => { onTagDelete = ({ index }) => {
@ -67,7 +67,7 @@ class TextTagInputConnector extends Component {
onChange({ onChange({
name, name,
value: newValue.join(',') value: newValue
}); });
}; };

View File

@ -10,6 +10,7 @@ export const OAUTH = 'oauth';
export const PASSWORD = 'password'; export const PASSWORD = 'password';
export const PATH = 'path'; export const PATH = 'path';
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect'; export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const INDEXER_SELECT = 'indexerSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect'; export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect'; export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
export const LANGUAGE_SELECT = 'languageSelect'; export const LANGUAGE_SELECT = 'languageSelect';
@ -36,6 +37,7 @@ export const all = [
PASSWORD, PASSWORD,
PATH, PATH,
QUALITY_PROFILE_SELECT, QUALITY_PROFILE_SELECT,
INDEXER_SELECT,
DOWNLOAD_CLIENT_SELECT, DOWNLOAD_CLIENT_SELECT,
ROOT_FOLDER_SELECT, ROOT_FOLDER_SELECT,
INDEXER_FLAGS_SELECT, INDEXER_FLAGS_SELECT,

View File

@ -10,7 +10,6 @@ import translate from 'Utilities/String/translate';
import IndexersConnector from './Indexers/IndexersConnector'; import IndexersConnector from './Indexers/IndexersConnector';
import ManageIndexersModal from './Indexers/Manage/ManageIndexersModal'; import ManageIndexersModal from './Indexers/Manage/ManageIndexersModal';
import IndexerOptionsConnector from './Options/IndexerOptionsConnector'; import IndexerOptionsConnector from './Options/IndexerOptionsConnector';
import RestrictionsConnector from './Restrictions/RestrictionsConnector';
class IndexerSettings extends Component { class IndexerSettings extends Component {
@ -103,8 +102,6 @@ class IndexerSettings extends Component {
onChildStateChange={this.onChildStateChange} onChildStateChange={this.onChildStateChange}
/> />
<RestrictionsConnector />
<ManageIndexersModal <ManageIndexersModal
isOpen={isManageIndexersOpen} isOpen={isManageIndexersOpen}
onModalClose={this.onManageIndexersModalClose} onModalClose={this.onManageIndexersModalClose}

View File

@ -1,5 +0,0 @@
.deleteButton {
composes: button from '~Components/Link/Button.css';
margin-right: auto;
}

View File

@ -1,151 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
import Label from 'Components/Label';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import { kinds } from 'Helpers/Props';
import split from 'Utilities/String/split';
import translate from 'Utilities/String/translate';
import EditRestrictionModalConnector from './EditRestrictionModalConnector';
import styles from './Restriction.css';
class Restriction extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditRestrictionModalOpen: false,
isDeleteRestrictionModalOpen: false
};
}
//
// Listeners
onEditRestrictionPress = () => {
this.setState({ isEditRestrictionModalOpen: true });
};
onEditRestrictionModalClose = () => {
this.setState({ isEditRestrictionModalOpen: false });
};
onDeleteRestrictionPress = () => {
this.setState({
isEditRestrictionModalOpen: false,
isDeleteRestrictionModalOpen: true
});
};
onDeleteRestrictionModalClose= () => {
this.setState({ isDeleteRestrictionModalOpen: false });
};
onConfirmDeleteRestriction = () => {
this.props.onConfirmDeleteRestriction(this.props.id);
};
//
// Render
render() {
const {
id,
required,
ignored,
tags,
tagList
} = this.props;
return (
<Card
className={styles.restriction}
overlayContent={true}
onPress={this.onEditRestrictionPress}
>
<div>
{
split(required).map((item) => {
if (!item) {
return null;
}
return (
<Label
className={styles.label}
key={item}
kind={kinds.SUCCESS}
>
{item}
</Label>
);
})
}
</div>
<div>
{
split(ignored).map((item) => {
if (!item) {
return null;
}
return (
<Label
className={styles.label}
key={item}
kind={kinds.DANGER}
>
{item}
</Label>
);
})
}
</div>
<TagList
tags={tags}
tagList={tagList}
/>
<EditRestrictionModalConnector
id={id}
isOpen={this.state.isEditRestrictionModalOpen}
onModalClose={this.onEditRestrictionModalClose}
onDeleteRestrictionPress={this.onDeleteRestrictionPress}
/>
<ConfirmModal
isOpen={this.state.isDeleteRestrictionModalOpen}
kind={kinds.DANGER}
title={translate('DeleteRestriction')}
message={translate('DeleteRestrictionHelpText')}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteRestriction}
onCancel={this.onDeleteRestrictionModalClose}
/>
</Card>
);
}
}
Restriction.propTypes = {
id: PropTypes.number.isRequired,
required: PropTypes.string.isRequired,
ignored: PropTypes.string.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteRestriction: PropTypes.func.isRequired
};
Restriction.defaultProps = {
required: '',
ignored: ''
};
export default Restriction;

View File

@ -1,61 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteRestriction, fetchRestrictions } from 'Store/Actions/settingsActions';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import Restrictions from './Restrictions';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.restrictions,
createTagsSelector(),
(restrictions, tagList) => {
return {
...restrictions,
tagList
};
}
);
}
const mapDispatchToProps = {
fetchRestrictions,
deleteRestriction
};
class RestrictionsConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchRestrictions();
}
//
// Listeners
onConfirmDeleteRestriction = (id) => {
this.props.deleteRestriction({ id });
};
//
// Render
render() {
return (
<Restrictions
{...this.props}
onConfirmDeleteRestriction={this.onConfirmDeleteRestriction}
/>
);
}
}
RestrictionsConnector.propTypes = {
fetchRestrictions: PropTypes.func.isRequired,
deleteRestriction: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(RestrictionsConnector);

View File

@ -1,6 +0,0 @@
.addCustomFormatMessage {
color: var(--helpTextColor);
text-align: center;
font-weight: 300;
font-size: 20px;
}

View File

@ -1,7 +0,0 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'addCustomFormatMessage': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@ -1,14 +1,13 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import { DndProvider } from 'react-dnd-multi-backend'; import { DndProvider } from 'react-dnd-multi-backend';
import HTML5toTouch from 'react-dnd-multi-backend/dist/esm/HTML5toTouch'; import HTML5toTouch from 'react-dnd-multi-backend/dist/esm/HTML5toTouch';
import Link from 'Components/Link/Link';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody'; import PageContentBody from 'Components/Page/PageContentBody';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import DelayProfilesConnector from './Delay/DelayProfilesConnector'; import DelayProfilesConnector from './Delay/DelayProfilesConnector';
import QualityProfilesConnector from './Quality/QualityProfilesConnector'; import QualityProfilesConnector from './Quality/QualityProfilesConnector';
import styles from './Profiles.css'; import ReleaseProfilesConnector from './Release/ReleaseProfilesConnector';
// Only a single DragDrop Context can exist so it's done here to allow editing // Only a single DragDrop Context can exist so it's done here to allow editing
// quality profiles and reordering delay profiles to work. // quality profiles and reordering delay profiles to work.
@ -28,11 +27,7 @@ class Profiles extends Component {
<DndProvider options={HTML5toTouch}> <DndProvider options={HTML5toTouch}>
<QualityProfilesConnector /> <QualityProfilesConnector />
<DelayProfilesConnector /> <DelayProfilesConnector />
<div className={styles.addCustomFormatMessage}> <ReleaseProfilesConnector />
{translate('LookingForReleaseProfiles1')}
<Link to='/settings/customformats'> {translate('CustomFormats')} </Link>
{translate('LookingForReleaseProfiles2')}
</div>
</DndProvider> </DndProvider>
</PageContentBody> </PageContentBody>
</PageContent> </PageContent>

View File

@ -2,16 +2,16 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props'; import { sizes } from 'Helpers/Props';
import EditRestrictionModalContentConnector from './EditRestrictionModalContentConnector'; import EditReleaseProfileModalContentConnector from './EditReleaseProfileModalContentConnector';
function EditRestrictionModal({ isOpen, onModalClose, ...otherProps }) { function EditReleaseProfileModal({ isOpen, onModalClose, ...otherProps }) {
return ( return (
<Modal <Modal
size={sizes.MEDIUM} size={sizes.MEDIUM}
isOpen={isOpen} isOpen={isOpen}
onModalClose={onModalClose} onModalClose={onModalClose}
> >
<EditRestrictionModalContentConnector <EditReleaseProfileModalContentConnector
{...otherProps} {...otherProps}
onModalClose={onModalClose} onModalClose={onModalClose}
/> />
@ -19,9 +19,9 @@ function EditRestrictionModal({ isOpen, onModalClose, ...otherProps }) {
); );
} }
EditRestrictionModal.propTypes = { EditReleaseProfileModal.propTypes = {
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
export default EditRestrictionModal; export default EditReleaseProfileModal;

View File

@ -2,19 +2,19 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions'; import { clearPendingChanges } from 'Store/Actions/baseActions';
import EditRestrictionModal from './EditRestrictionModal'; import EditReleaseProfileModal from './EditReleaseProfileModal';
const mapDispatchToProps = { const mapDispatchToProps = {
clearPendingChanges clearPendingChanges
}; };
class EditRestrictionModalConnector extends Component { class EditReleaseProfileModalConnector extends Component {
// //
// Listeners // Listeners
onModalClose = () => { onModalClose = () => {
this.props.clearPendingChanges({ section: 'settings.restrictions' }); this.props.clearPendingChanges({ section: 'settings.releaseProfiles' });
this.props.onModalClose(); this.props.onModalClose();
}; };
@ -23,7 +23,7 @@ class EditRestrictionModalConnector extends Component {
render() { render() {
return ( return (
<EditRestrictionModal <EditReleaseProfileModal
{...this.props} {...this.props}
onModalClose={this.onModalClose} onModalClose={this.onModalClose}
/> />
@ -31,9 +31,9 @@ class EditRestrictionModalConnector extends Component {
} }
} }
EditRestrictionModalConnector.propTypes = { EditReleaseProfileModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired clearPendingChanges: PropTypes.func.isRequired
}; };
export default connect(null, mapDispatchToProps)(EditRestrictionModalConnector); export default connect(null, mapDispatchToProps)(EditReleaseProfileModalConnector);

View File

@ -0,0 +1,12 @@
.deleteButton {
composes: button from '~Components/Link/Button.css';
margin-right: auto;
}
.tagInternalInput {
composes: internalInput from '~Components/Form/TagInput.css';
flex: 0 0 100%;
}

View File

@ -2,6 +2,7 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'deleteButton': string; 'deleteButton': string;
'tagInternalInput': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;

View File

@ -12,9 +12,11 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props'; import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './EditRestrictionModalContent.css'; import styles from './EditReleaseProfileModalContent.css';
function EditRestrictionModalContent(props) { const tagInputDelimiters = ['Tab', 'Enter'];
function EditReleaseProfileModalContent(props) {
const { const {
isSaving, isSaving,
saveError, saveError,
@ -22,27 +24,54 @@ function EditRestrictionModalContent(props) {
onInputChange, onInputChange,
onModalClose, onModalClose,
onSavePress, onSavePress,
onDeleteRestrictionPress, onDeleteReleaseProfilePress,
...otherProps ...otherProps
} = props; } = props;
const { const {
id, id,
name,
enabled,
required, required,
ignored, ignored,
tags tags,
indexerId
} = item; } = item;
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
{id ? translate('EditRestriction') : translate('AddRestriction')} {id ? translate('Edit Release Profile') : translate('Add Release Profile')}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<Form <Form {...otherProps}>
{...otherProps}
> <FormGroup>
<FormLabel>{translate('Name')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
placeholder="Optional name"
canEdit={true}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableProfile')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enabled"
helpText="Check to enable release profile"
{...enabled}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup> <FormGroup>
<FormLabel>{translate('MustContain')}</FormLabel> <FormLabel>{translate('MustContain')}</FormLabel>
@ -51,9 +80,10 @@ function EditRestrictionModalContent(props) {
inputClassName={styles.tagInternalInput} inputClassName={styles.tagInternalInput}
type={inputTypes.TEXT_TAG} type={inputTypes.TEXT_TAG}
name="required" name="required"
helpText={translate('RequiredRestrictionHelpText')} helpText="The release must contain at least one of these terms (case insensitive)"
kind={kinds.SUCCESS} kind={kinds.SUCCESS}
placeholder={translate('RequiredRestrictionPlaceHolder')} placeholder={translate('RequiredRestrictionPlaceHolder')}
delimiters={tagInputDelimiters}
canEdit={true} canEdit={true}
onChange={onInputChange} onChange={onInputChange}
/> />
@ -67,21 +97,36 @@ function EditRestrictionModalContent(props) {
inputClassName={styles.tagInternalInput} inputClassName={styles.tagInternalInput}
type={inputTypes.TEXT_TAG} type={inputTypes.TEXT_TAG}
name="ignored" name="ignored"
helpText={translate('IgnoredHelpText')} helpText="The release will be rejected if it contains one or more of terms (case insensitive)"
kind={kinds.DANGER} kind={kinds.DANGER}
placeholder={translate('IgnoredPlaceHolder')} placeholder={translate('IgnoredPlaceHolder')}
delimiters={tagInputDelimiters}
canEdit={true} canEdit={true}
onChange={onInputChange} onChange={onInputChange}
/> />
</FormGroup> </FormGroup>
<FormGroup>
<FormLabel>{translate('Indexer')}</FormLabel>
<FormInputGroup
type={inputTypes.INDEXER_SELECT}
name="indexerId"
helpText="Specify what indexer the profile applies to"
helpTextWarning="Using a specific indexer with release profiles can lead to duplicate releases being grabbed"
{...indexerId}
includeAny={true}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup> <FormGroup>
<FormLabel>{translate('Tags')}</FormLabel> <FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.TAG} type={inputTypes.TAG}
name="tags" name="tags"
helpText={translate('TagsHelpText')} helpText="Release profiles will apply to movies with at least one matching tag. Leave blank to apply to all movies"
{...tags} {...tags}
onChange={onInputChange} onChange={onInputChange}
/> />
@ -94,7 +139,7 @@ function EditRestrictionModalContent(props) {
<Button <Button
className={styles.deleteButton} className={styles.deleteButton}
kind={kinds.DANGER} kind={kinds.DANGER}
onPress={onDeleteRestrictionPress} onPress={onDeleteReleaseProfilePress}
> >
{translate('Delete')} {translate('Delete')}
</Button> </Button>
@ -118,14 +163,14 @@ function EditRestrictionModalContent(props) {
); );
} }
EditRestrictionModalContent.propTypes = { EditReleaseProfileModalContent.propTypes = {
isSaving: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object, saveError: PropTypes.object,
item: PropTypes.object.isRequired, item: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired, onInputChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired, onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired, onSavePress: PropTypes.func.isRequired,
onDeleteRestrictionPress: PropTypes.func onDeleteReleaseProfilePress: PropTypes.func
}; };
export default EditRestrictionModalContent; export default EditReleaseProfileModalContent;

View File

@ -1,23 +1,24 @@
import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { saveRestriction, setRestrictionValue } from 'Store/Actions/settingsActions'; import { saveReleaseProfile, setReleaseProfileValue } from 'Store/Actions/settingsActions';
import selectSettings from 'Store/Selectors/selectSettings'; import selectSettings from 'Store/Selectors/selectSettings';
import EditRestrictionModalContent from './EditRestrictionModalContent'; import EditReleaseProfileModalContent from './EditReleaseProfileModalContent';
const newRestriction = { const newReleaseProfile = {
required: '', enabled: true,
ignored: '', required: [],
tags: [] ignored: [],
tags: [],
indexerId: 0
}; };
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state, { id }) => id, (state, { id }) => id,
(state) => state.settings.restrictions, (state) => state.settings.releaseProfiles,
(id, restrictions) => { (id, releaseProfiles) => {
const { const {
isFetching, isFetching,
error, error,
@ -25,9 +26,9 @@ function createMapStateToProps() {
saveError, saveError,
pendingChanges, pendingChanges,
items items
} = restrictions; } = releaseProfiles;
const profile = id ? _.find(items, { id }) : newRestriction; const profile = id ? items.find((i) => i.id === id) : newReleaseProfile;
const settings = selectSettings(profile, pendingChanges, saveError); const settings = selectSettings(profile, pendingChanges, saveError);
return { return {
@ -44,21 +45,21 @@ function createMapStateToProps() {
} }
const mapDispatchToProps = { const mapDispatchToProps = {
setRestrictionValue, setReleaseProfileValue,
saveRestriction saveReleaseProfile
}; };
class EditRestrictionModalContentConnector extends Component { class EditReleaseProfileModalContentConnector extends Component {
// //
// Lifecycle // Lifecycle
componentDidMount() { componentDidMount() {
if (!this.props.id) { if (!this.props.id) {
Object.keys(newRestriction).forEach((name) => { Object.keys(newReleaseProfile).forEach((name) => {
this.props.setRestrictionValue({ this.props.setReleaseProfileValue({
name, name,
value: newRestriction[name] value: newReleaseProfile[name]
}); });
}); });
} }
@ -74,11 +75,11 @@ class EditRestrictionModalContentConnector extends Component {
// Listeners // Listeners
onInputChange = ({ name, value }) => { onInputChange = ({ name, value }) => {
this.props.setRestrictionValue({ name, value }); this.props.setReleaseProfileValue({ name, value });
}; };
onSavePress = () => { onSavePress = () => {
this.props.saveRestriction({ id: this.props.id }); this.props.saveReleaseProfile({ id: this.props.id });
}; };
// //
@ -86,7 +87,7 @@ class EditRestrictionModalContentConnector extends Component {
render() { render() {
return ( return (
<EditRestrictionModalContent <EditReleaseProfileModalContent
{...this.props} {...this.props}
onSavePress={this.onSavePress} onSavePress={this.onSavePress}
onTestPress={this.onTestPress} onTestPress={this.onTestPress}
@ -97,15 +98,15 @@ class EditRestrictionModalContentConnector extends Component {
} }
} }
EditRestrictionModalContentConnector.propTypes = { EditReleaseProfileModalContentConnector.propTypes = {
id: PropTypes.number, id: PropTypes.number,
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object, saveError: PropTypes.object,
item: PropTypes.object.isRequired, item: PropTypes.object.isRequired,
setRestrictionValue: PropTypes.func.isRequired, setReleaseProfileValue: PropTypes.func.isRequired,
saveRestriction: PropTypes.func.isRequired, saveReleaseProfile: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
export default connect(createMapStateToProps, mapDispatchToProps)(EditRestrictionModalContentConnector); export default connect(createMapStateToProps, mapDispatchToProps)(EditReleaseProfileModalContentConnector);

View File

@ -1,4 +1,4 @@
.restriction { .releaseProfile {
composes: card from '~Components/Card.css'; composes: card from '~Components/Card.css';
width: 290px; width: 290px;
@ -10,6 +10,14 @@
margin-top: 5px; margin-top: 5px;
} }
.name {
@add-mixin truncate;
margin-bottom: 20px;
font-weight: 300;
font-size: 24px;
}
.label { .label {
composes: label from '~Components/Label.css'; composes: label from '~Components/Label.css';

View File

@ -3,7 +3,8 @@
interface CssExports { interface CssExports {
'enabled': string; 'enabled': string;
'label': string; 'label': string;
'restriction': string; 'name': string;
'releaseProfile': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;

View File

@ -0,0 +1,196 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
import Label from 'Components/Label';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import { kinds } from 'Helpers/Props';
import EditReleaseProfileModalConnector from './EditReleaseProfileModalConnector';
import styles from './ReleaseProfile.css';
class ReleaseProfile extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditReleaseProfileModalOpen: false,
isDeleteReleaseProfileModalOpen: false
};
}
//
// Listeners
onEditReleaseProfilePress = () => {
this.setState({ isEditReleaseProfileModalOpen: true });
};
onEditReleaseProfileModalClose = () => {
this.setState({ isEditReleaseProfileModalOpen: false });
};
onDeleteReleaseProfilePress = () => {
this.setState({
isEditReleaseProfileModalOpen: false,
isDeleteReleaseProfileModalOpen: true
});
};
onDeleteReleaseProfileModalClose= () => {
this.setState({ isDeleteReleaseProfileModalOpen: false });
};
onConfirmDeleteReleaseProfile = () => {
this.props.onConfirmDeleteReleaseProfile(this.props.id);
};
//
// Render
render() {
const {
id,
name,
enabled,
required,
ignored,
tags,
indexerId,
tagList,
indexerList
} = this.props;
const {
isEditReleaseProfileModalOpen,
isDeleteReleaseProfileModalOpen
} = this.state;
const indexer = indexerList.find((i) => i.id === indexerId);
return (
<Card
className={styles.releaseProfile}
overlayContent={true}
onPress={this.onEditReleaseProfilePress}
>
{
name ?
<div className={styles.name}>
{name}
</div> :
null
}
<div>
{
required.map((item) => {
if (!item) {
return null;
}
return (
<Label
className={styles.label}
key={item}
kind={kinds.SUCCESS}
>
{item}
</Label>
);
})
}
</div>
<div>
{
ignored.map((item) => {
if (!item) {
return null;
}
return (
<Label
className={styles.label}
key={item}
kind={kinds.DANGER}
>
{item}
</Label>
);
})
}
</div>
<TagList
tags={tags}
tagList={tagList}
/>
<div>
{
!enabled &&
<Label
kind={kinds.DISABLED}
outline={true}
>
Disabled
</Label>
}
{
indexer &&
<Label
kind={kinds.INFO}
outline={true}
>
{indexer.name}
</Label>
}
</div>
<EditReleaseProfileModalConnector
id={id}
isOpen={isEditReleaseProfileModalOpen}
onModalClose={this.onEditReleaseProfileModalClose}
onDeleteReleaseProfilePress={this.onDeleteReleaseProfilePress}
/>
<ConfirmModal
isOpen={isDeleteReleaseProfileModalOpen}
kind={kinds.DANGER}
title="Delete ReleaseProfile"
message={'Are you sure you want to delete this releaseProfile?'}
confirmLabel="Delete"
onConfirm={this.onConfirmDeleteReleaseProfile}
onCancel={this.onDeleteReleaseProfileModalClose}
/>
</Card>
);
}
}
ReleaseProfile.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string,
enabled: PropTypes.bool.isRequired,
required: PropTypes.arrayOf(PropTypes.string).isRequired,
ignored: PropTypes.arrayOf(PropTypes.string).isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
indexerId: PropTypes.number.isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteReleaseProfile: PropTypes.func.isRequired
};
ReleaseProfile.defaultProps = {
enabled: true,
required: [],
ignored: [],
indexerId: 0
};
export default ReleaseProfile;

View File

@ -1,10 +1,10 @@
.restrictions { .releaseProfiles {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
} }
.addRestriction { .addReleaseProfile {
composes: restriction from '~./Restriction.css'; composes: releaseProfile from '~./ReleaseProfile.css';
background-color: var(--cardAlternateBackgroundColor); background-color: var(--cardAlternateBackgroundColor);
color: var(--gray); color: var(--gray);

View File

@ -1,9 +1,9 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'addRestriction': string; 'addReleaseProfile': string;
'center': string; 'center': string;
'restrictions': string; 'releaseProfiles': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;

View File

@ -6,11 +6,11 @@ import Icon from 'Components/Icon';
import PageSectionContent from 'Components/Page/PageSectionContent'; import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import EditRestrictionModalConnector from './EditRestrictionModalConnector'; import EditReleaseProfileModalConnector from './EditReleaseProfileModalConnector';
import Restriction from './Restriction'; import ReleaseProfile from './ReleaseProfile';
import styles from './Restrictions.css'; import styles from './ReleaseProfiles.css';
class Restrictions extends Component { class ReleaseProfiles extends Component {
// //
// Lifecycle // Lifecycle
@ -19,19 +19,19 @@ class Restrictions extends Component {
super(props, context); super(props, context);
this.state = { this.state = {
isAddRestrictionModalOpen: false isAddReleaseProfileModalOpen: false
}; };
} }
// //
// Listeners // Listeners
onAddRestrictionPress = () => { onAddReleaseProfilePress = () => {
this.setState({ isAddRestrictionModalOpen: true }); this.setState({ isAddReleaseProfileModalOpen: true });
}; };
onAddRestrictionModalClose = () => { onAddReleaseProfileModalClose = () => {
this.setState({ isAddRestrictionModalOpen: false }); this.setState({ isAddReleaseProfileModalOpen: false });
}; };
// //
@ -41,20 +41,21 @@ class Restrictions extends Component {
const { const {
items, items,
tagList, tagList,
onConfirmDeleteRestriction, indexerList,
onConfirmDeleteReleaseProfile,
...otherProps ...otherProps
} = this.props; } = this.props;
return ( return (
<FieldSet legend={translate('Restrictions')}> <FieldSet legend={translate('Release Profiles')}>
<PageSectionContent <PageSectionContent
errorMessage={translate('UnableToLoadRestrictions')} errorMessage={translate('Unable to load ReleaseProfiles')}
{...otherProps} {...otherProps}
> >
<div className={styles.restrictions}> <div className={styles.releaseProfiles}>
<Card <Card
className={styles.addRestriction} className={styles.addReleaseProfile}
onPress={this.onAddRestrictionPress} onPress={this.onAddReleaseProfilePress}
> >
<div className={styles.center}> <div className={styles.center}>
<Icon <Icon
@ -67,20 +68,21 @@ class Restrictions extends Component {
{ {
items.map((item) => { items.map((item) => {
return ( return (
<Restriction <ReleaseProfile
key={item.id} key={item.id}
tagList={tagList} tagList={tagList}
indexerList={indexerList}
{...item} {...item}
onConfirmDeleteRestriction={onConfirmDeleteRestriction} onConfirmDeleteReleaseProfile={onConfirmDeleteReleaseProfile}
/> />
); );
}) })
} }
</div> </div>
<EditRestrictionModalConnector <EditReleaseProfileModalConnector
isOpen={this.state.isAddRestrictionModalOpen} isOpen={this.state.isAddReleaseProfileModalOpen}
onModalClose={this.onAddRestrictionModalClose} onModalClose={this.onAddReleaseProfileModalClose}
/> />
</PageSectionContent> </PageSectionContent>
</FieldSet> </FieldSet>
@ -88,12 +90,13 @@ class Restrictions extends Component {
} }
} }
Restrictions.propTypes = { ReleaseProfiles.propTypes = {
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired, tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteRestriction: PropTypes.func.isRequired indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteReleaseProfile: PropTypes.func.isRequired
}; };
export default Restrictions; export default ReleaseProfiles;

View File

@ -0,0 +1,74 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteReleaseProfile, fetchIndexers, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import ReleaseProfiles from './ReleaseProfiles';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.releaseProfiles,
(state) => state.settings.indexers,
createTagsSelector(),
(releaseProfiles, indexers, tagList) => {
return {
...releaseProfiles,
tagList,
isIndexersPopulated: indexers.isPopulated,
indexerList: indexers.items
};
}
);
}
const mapDispatchToProps = {
fetchIndexers,
fetchReleaseProfiles,
deleteReleaseProfile
};
class ReleaseProfilesConnector extends Component {
//
// Lifecycle
componentDidMount() {
if (!this.props.isPopulated) {
this.props.fetchReleaseProfiles();
}
if (!this.props.isIndexersPopulated) {
this.props.fetchIndexers();
}
}
//
// Listeners
onConfirmDeleteReleaseProfile = (id) => {
this.props.deleteReleaseProfile({ id });
};
//
// Render
render() {
return (
<ReleaseProfiles
{...this.props}
onConfirmDeleteReleaseProfile={this.onConfirmDeleteReleaseProfile}
/>
);
}
}
ReleaseProfilesConnector.propTypes = {
isPopulated: PropTypes.bool.isRequired,
isIndexersPopulated: PropTypes.bool.isRequired,
fetchReleaseProfiles: PropTypes.func.isRequired,
deleteReleaseProfile: PropTypes.func.isRequired,
fetchIndexers: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(ReleaseProfilesConnector);

View File

@ -8,7 +8,6 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import split from 'Utilities/String/split';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import TagDetailsDelayProfile from './TagDetailsDelayProfile'; import TagDetailsDelayProfile from './TagDetailsDelayProfile';
import styles from './TagDetailsModalContent.css'; import styles from './TagDetailsModalContent.css';
@ -19,9 +18,9 @@ function TagDetailsModalContent(props) {
isTagUsed, isTagUsed,
movies, movies,
delayProfiles, delayProfiles,
notifications,
restrictions,
importLists, importLists,
notifications,
releaseProfiles,
indexers, indexers,
downloadClients, downloadClients,
autoTags, autoTags,
@ -106,10 +105,10 @@ function TagDetailsModalContent(props) {
} }
{ {
restrictions.length ? releaseProfiles.length ?
<FieldSet legend={translate('Restrictions')}> <FieldSet legend={translate('ReleaseProfiles')}>
{ {
restrictions.map((item) => { releaseProfiles.map((item) => {
return ( return (
<div <div
key={item.id} key={item.id}
@ -117,7 +116,7 @@ function TagDetailsModalContent(props) {
> >
<div> <div>
{ {
split(item.required).map((r) => { item.required.map((r) => {
return ( return (
<Label <Label
key={r} key={r}
@ -132,7 +131,7 @@ function TagDetailsModalContent(props) {
<div> <div>
{ {
split(item.ignored).map((i) => { item.ignored.map((i) => {
return ( return (
<Label <Label
key={i} key={i}
@ -245,9 +244,9 @@ TagDetailsModalContent.propTypes = {
isTagUsed: PropTypes.bool.isRequired, isTagUsed: PropTypes.bool.isRequired,
movies: PropTypes.arrayOf(PropTypes.object).isRequired, movies: PropTypes.arrayOf(PropTypes.object).isRequired,
delayProfiles: PropTypes.arrayOf(PropTypes.object).isRequired, delayProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
notifications: PropTypes.arrayOf(PropTypes.object).isRequired,
restrictions: PropTypes.arrayOf(PropTypes.object).isRequired,
importLists: PropTypes.arrayOf(PropTypes.object).isRequired, importLists: PropTypes.arrayOf(PropTypes.object).isRequired,
notifications: PropTypes.arrayOf(PropTypes.object).isRequired,
releaseProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
indexers: PropTypes.arrayOf(PropTypes.object).isRequired, indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
downloadClients: PropTypes.arrayOf(PropTypes.object).isRequired, downloadClients: PropTypes.arrayOf(PropTypes.object).isRequired,
autoTags: PropTypes.arrayOf(PropTypes.object).isRequired, autoTags: PropTypes.arrayOf(PropTypes.object).isRequired,

View File

@ -53,10 +53,10 @@ function createMatchingNotificationsSelector() {
); );
} }
function createMatchingRestrictionsSelector() { function createMatchingReleaseProfilesSelector() {
return createSelector( return createSelector(
(state, { restrictionIds }) => restrictionIds, (state, { releaseProfileIds }) => releaseProfileIds,
(state) => state.settings.restrictions.items, (state) => state.settings.releaseProfiles.items,
findMatchingItems findMatchingItems
); );
} }
@ -98,17 +98,17 @@ function createMapStateToProps() {
createMatchingMoviesSelector(), createMatchingMoviesSelector(),
createMatchingDelayProfilesSelector(), createMatchingDelayProfilesSelector(),
createMatchingNotificationsSelector(), createMatchingNotificationsSelector(),
createMatchingRestrictionsSelector(), createMatchingReleaseProfilesSelector(),
createMatchingImportListsSelector(), createMatchingImportListsSelector(),
createMatchingIndexersSelector(), createMatchingIndexersSelector(),
createMatchingDownloadClientsSelector(), createMatchingDownloadClientsSelector(),
createMatchingAutoTagsSelector(), createMatchingAutoTagsSelector(),
(movies, delayProfiles, notifications, restrictions, importLists, indexers, downloadClients, autoTags) => { (movies, delayProfiles, notifications, releaseProfiles, importLists, indexers, downloadClients, autoTags) => {
return { return {
movies, movies,
delayProfiles, delayProfiles,
notifications, notifications,
restrictions, releaseProfiles,
importLists, importLists,
indexers, indexers,
downloadClients, downloadClients,

View File

@ -9,7 +9,6 @@ import TagInUse from './TagInUse';
import styles from './Tag.css'; import styles from './Tag.css';
class Tag extends Component { class Tag extends Component {
// //
// Lifecycle // Lifecycle
@ -40,7 +39,7 @@ class Tag extends Component {
}); });
}; };
onDeleteTagModalClose= () => { onDeleteTagModalClose = () => {
this.setState({ isDeleteTagModalOpen: false }); this.setState({ isDeleteTagModalOpen: false });
}; };
@ -57,23 +56,20 @@ class Tag extends Component {
delayProfileIds, delayProfileIds,
importListIds, importListIds,
notificationIds, notificationIds,
restrictionIds, releaseProfileIds,
indexerIds, indexerIds,
downloadClientIds, downloadClientIds,
autoTagIds, autoTagIds,
movieIds movieIds
} = this.props; } = this.props;
const { const { isDetailsModalOpen, isDeleteTagModalOpen } = this.state;
isDetailsModalOpen,
isDeleteTagModalOpen
} = this.state;
const isTagUsed = !!( const isTagUsed = !!(
delayProfileIds.length || delayProfileIds.length ||
importListIds.length || importListIds.length ||
notificationIds.length || notificationIds.length ||
restrictionIds.length || releaseProfileIds.length ||
indexerIds.length || indexerIds.length ||
downloadClientIds.length || downloadClientIds.length ||
autoTagIds.length || autoTagIds.length ||
@ -86,9 +82,7 @@ class Tag extends Component {
overlayContent={true} overlayContent={true}
onPress={this.onShowDetailsPress} onPress={this.onShowDetailsPress}
> >
<div className={styles.label}> <div className={styles.label}>{label}</div>
{label}
</div>
{ {
isTagUsed ? isTagUsed ?
@ -115,7 +109,7 @@ class Tag extends Component {
<TagInUse <TagInUse
label="release profile" label="release profile"
count={restrictionIds.length} count={releaseProfileIds.length}
/> />
<TagInUse <TagInUse
@ -137,12 +131,7 @@ class Tag extends Component {
null null
} }
{ {!isTagUsed && <div>{translate('NoLinks')}</div>}
!isTagUsed &&
<div>
{translate('NoLinks')}
</div>
}
<TagDetailsModal <TagDetailsModal
label={label} label={label}
@ -151,7 +140,7 @@ class Tag extends Component {
delayProfileIds={delayProfileIds} delayProfileIds={delayProfileIds}
importListIds={importListIds} importListIds={importListIds}
notificationIds={notificationIds} notificationIds={notificationIds}
restrictionIds={restrictionIds} releaseProfileIds={releaseProfileIds}
indexerIds={indexerIds} indexerIds={indexerIds}
downloadClientIds={downloadClientIds} downloadClientIds={downloadClientIds}
autoTagIds={autoTagIds} autoTagIds={autoTagIds}
@ -180,7 +169,7 @@ Tag.propTypes = {
delayProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired, delayProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
importListIds: PropTypes.arrayOf(PropTypes.number).isRequired, importListIds: PropTypes.arrayOf(PropTypes.number).isRequired,
notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired, notificationIds: PropTypes.arrayOf(PropTypes.number).isRequired,
restrictionIds: PropTypes.arrayOf(PropTypes.number).isRequired, releaseProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired,
indexerIds: PropTypes.arrayOf(PropTypes.number).isRequired, indexerIds: PropTypes.arrayOf(PropTypes.number).isRequired,
downloadClientIds: PropTypes.arrayOf(PropTypes.number).isRequired, downloadClientIds: PropTypes.arrayOf(PropTypes.number).isRequired,
autoTagIds: PropTypes.arrayOf(PropTypes.number).isRequired, autoTagIds: PropTypes.arrayOf(PropTypes.number).isRequired,
@ -192,7 +181,7 @@ Tag.defaultProps = {
delayProfileIds: [], delayProfileIds: [],
importListIds: [], importListIds: [],
notificationIds: [], notificationIds: [],
restrictionIds: [], releaseProfileIds: [],
indexerIds: [], indexerIds: [],
downloadClientIds: [], downloadClientIds: [],
autoTagIds: [], autoTagIds: [],

View File

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchRestrictions } from 'Store/Actions/settingsActions'; import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
import { fetchTagDetails } from 'Store/Actions/tagActions'; import { fetchTagDetails } from 'Store/Actions/tagActions';
import Tags from './Tags'; import Tags from './Tags';
@ -28,7 +28,7 @@ const mapDispatchToProps = {
dispatchFetchTagDetails: fetchTagDetails, dispatchFetchTagDetails: fetchTagDetails,
dispatchFetchDelayProfiles: fetchDelayProfiles, dispatchFetchDelayProfiles: fetchDelayProfiles,
dispatchFetchNotifications: fetchNotifications, dispatchFetchNotifications: fetchNotifications,
dispatchFetchRestrictions: fetchRestrictions, dispatchFetchReleaseProfiles: fetchReleaseProfiles,
dispatchFetchImportLists: fetchImportLists, dispatchFetchImportLists: fetchImportLists,
dispatchFetchIndexers: fetchIndexers, dispatchFetchIndexers: fetchIndexers,
dispatchFetchDownloadClients: fetchDownloadClients dispatchFetchDownloadClients: fetchDownloadClients
@ -44,7 +44,7 @@ class MetadatasConnector extends Component {
dispatchFetchTagDetails, dispatchFetchTagDetails,
dispatchFetchDelayProfiles, dispatchFetchDelayProfiles,
dispatchFetchNotifications, dispatchFetchNotifications,
dispatchFetchRestrictions, dispatchFetchReleaseProfiles,
dispatchFetchImportLists, dispatchFetchImportLists,
dispatchFetchIndexers, dispatchFetchIndexers,
dispatchFetchDownloadClients dispatchFetchDownloadClients
@ -53,7 +53,7 @@ class MetadatasConnector extends Component {
dispatchFetchTagDetails(); dispatchFetchTagDetails();
dispatchFetchDelayProfiles(); dispatchFetchDelayProfiles();
dispatchFetchNotifications(); dispatchFetchNotifications();
dispatchFetchRestrictions(); dispatchFetchReleaseProfiles();
dispatchFetchImportLists(); dispatchFetchImportLists();
dispatchFetchIndexers(); dispatchFetchIndexers();
dispatchFetchDownloadClients(); dispatchFetchDownloadClients();
@ -75,7 +75,7 @@ MetadatasConnector.propTypes = {
dispatchFetchTagDetails: PropTypes.func.isRequired, dispatchFetchTagDetails: PropTypes.func.isRequired,
dispatchFetchDelayProfiles: PropTypes.func.isRequired, dispatchFetchDelayProfiles: PropTypes.func.isRequired,
dispatchFetchNotifications: PropTypes.func.isRequired, dispatchFetchNotifications: PropTypes.func.isRequired,
dispatchFetchRestrictions: PropTypes.func.isRequired, dispatchFetchReleaseProfiles: PropTypes.func.isRequired,
dispatchFetchImportLists: PropTypes.func.isRequired, dispatchFetchImportLists: PropTypes.func.isRequired,
dispatchFetchIndexers: PropTypes.func.isRequired, dispatchFetchIndexers: PropTypes.func.isRequired,
dispatchFetchDownloadClients: PropTypes.func.isRequired dispatchFetchDownloadClients: PropTypes.func.isRequired

View File

@ -0,0 +1,71 @@
import { createAction } from 'redux-actions';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
//
// Variables
const section = 'settings.releaseProfiles';
//
// Actions Types
export const FETCH_RELEASE_PROFILES = 'settings/releaseProfiles/fetchReleaseProfiles';
export const SAVE_RELEASE_PROFILE = 'settings/releaseProfiles/saveReleaseProfile';
export const DELETE_RELEASE_PROFILE = 'settings/releaseProfiles/deleteReleaseProfile';
export const SET_RELEASE_PROFILE_VALUE = 'settings/releaseProfiles/setReleaseProfileValue';
//
// Action Creators
export const fetchReleaseProfiles = createThunk(FETCH_RELEASE_PROFILES);
export const saveReleaseProfile = createThunk(SAVE_RELEASE_PROFILE);
export const deleteReleaseProfile = createThunk(DELETE_RELEASE_PROFILE);
export const setReleaseProfileValue = createAction(SET_RELEASE_PROFILE_VALUE, (payload) => {
return {
section,
...payload
};
});
//
// Details
export default {
//
// State
defaultState: {
isFetching: false,
isPopulated: false,
error: null,
isSaving: false,
saveError: null,
items: [],
pendingChanges: {}
},
//
// Action Handlers
actionHandlers: {
[FETCH_RELEASE_PROFILES]: createFetchHandler(section, '/releaseprofile'),
[SAVE_RELEASE_PROFILE]: createSaveProviderHandler(section, '/releaseprofile'),
[DELETE_RELEASE_PROFILE]: createRemoveItemHandler(section, '/releaseprofile')
},
//
// Reducers
reducers: {
[SET_RELEASE_PROFILE_VALUE]: createSetSettingValueReducer(section)
}
};

View File

@ -1,71 +0,0 @@
import { createAction } from 'redux-actions';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
//
// Variables
const section = 'settings.restrictions';
//
// Actions Types
export const FETCH_RESTRICTIONS = 'settings/restrictions/fetchRestrictions';
export const SAVE_RESTRICTION = 'settings/restrictions/saveRestriction';
export const DELETE_RESTRICTION = 'settings/restrictions/deleteRestriction';
export const SET_RESTRICTION_VALUE = 'settings/restrictions/setRestrictionValue';
//
// Action Creators
export const fetchRestrictions = createThunk(FETCH_RESTRICTIONS);
export const saveRestriction = createThunk(SAVE_RESTRICTION);
export const deleteRestriction = createThunk(DELETE_RESTRICTION);
export const setRestrictionValue = createAction(SET_RESTRICTION_VALUE, (payload) => {
return {
section,
...payload
};
});
//
// Details
export default {
//
// State
defaultState: {
isFetching: false,
isPopulated: false,
error: null,
isSaving: false,
saveError: null,
items: [],
pendingChanges: {}
},
//
// Action Handlers
actionHandlers: {
[FETCH_RESTRICTIONS]: createFetchHandler(section, '/restriction'),
[SAVE_RESTRICTION]: createSaveProviderHandler(section, '/restriction'),
[DELETE_RESTRICTION]: createRemoveItemHandler(section, '/restriction')
},
//
// Reducers
reducers: {
[SET_RESTRICTION_VALUE]: createSetSettingValueReducer(section)
}
};

View File

@ -24,8 +24,8 @@ import namingExamples from './Settings/namingExamples';
import notifications from './Settings/notifications'; import notifications from './Settings/notifications';
import qualityDefinitions from './Settings/qualityDefinitions'; import qualityDefinitions from './Settings/qualityDefinitions';
import qualityProfiles from './Settings/qualityProfiles'; import qualityProfiles from './Settings/qualityProfiles';
import releaseProfiles from './Settings/releaseProfiles';
import remotePathMappings from './Settings/remotePathMappings'; import remotePathMappings from './Settings/remotePathMappings';
import restrictions from './Settings/restrictions';
import ui from './Settings/ui'; import ui from './Settings/ui';
export * from './Settings/autoTaggingSpecifications'; export * from './Settings/autoTaggingSpecifications';
@ -52,7 +52,7 @@ export * from './Settings/notifications';
export * from './Settings/qualityDefinitions'; export * from './Settings/qualityDefinitions';
export * from './Settings/qualityProfiles'; export * from './Settings/qualityProfiles';
export * from './Settings/remotePathMappings'; export * from './Settings/remotePathMappings';
export * from './Settings/restrictions'; export * from './Settings/releaseProfiles';
export * from './Settings/ui'; export * from './Settings/ui';
// //
@ -89,7 +89,7 @@ export const defaultState = {
qualityDefinitions: qualityDefinitions.defaultState, qualityDefinitions: qualityDefinitions.defaultState,
qualityProfiles: qualityProfiles.defaultState, qualityProfiles: qualityProfiles.defaultState,
remotePathMappings: remotePathMappings.defaultState, remotePathMappings: remotePathMappings.defaultState,
restrictions: restrictions.defaultState, releaseProfiles: releaseProfiles.defaultState,
ui: ui.defaultState ui: ui.defaultState
}; };
@ -135,7 +135,7 @@ export const actionHandlers = handleThunks({
...qualityDefinitions.actionHandlers, ...qualityDefinitions.actionHandlers,
...qualityProfiles.actionHandlers, ...qualityProfiles.actionHandlers,
...remotePathMappings.actionHandlers, ...remotePathMappings.actionHandlers,
...restrictions.actionHandlers, ...releaseProfiles.actionHandlers,
...ui.actionHandlers ...ui.actionHandlers
}); });
@ -172,7 +172,7 @@ export const reducers = createHandleActions({
...qualityDefinitions.reducers, ...qualityDefinitions.reducers,
...qualityProfiles.reducers, ...qualityProfiles.reducers,
...remotePathMappings.reducers, ...remotePathMappings.reducers,
...restrictions.reducers, ...releaseProfiles.reducers,
...ui.reducers ...ui.reducers
}, defaultState, section); }, defaultState, section);

View File

@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.Linq;
using FluentAssertions;
using NUnit.Framework;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration;
using NzbDrone.Core.Profiles.Releases;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.Datastore.Migration
{
[TestFixture]
public class update_restrictions_to_release_profilesFixture : MigrationTest<update_restrictions_to_release_profiles>
{
[Test]
public void should_migrate_required_ignored_columns_to_json_arrays()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("Restrictions").Row(new
{
Required = "x265,1080p",
Ignored = "xvid,720p,480p",
Tags = new HashSet<int> { }.ToJson()
});
});
var items = db.Query<ReleaseProfile>("SELECT \"Required\", \"Ignored\" FROM \"ReleaseProfiles\"");
items.Should().HaveCount(1);
items.First().Required.Should().BeEquivalentTo(new[] { "x265", "1080p" });
items.First().Ignored.Should().BeEquivalentTo(new[] { "xvid", "720p", "480p" });
}
[Test]
public void should_delete_rows_with_empty_required_ignored_columns()
{
var db = WithMigrationTestDb(c =>
{
c.Insert.IntoTable("Restrictions").Row(new
{
Required = "",
Ignored = "",
Tags = new HashSet<int> { }.ToJson()
});
});
var items = db.Query<ReleaseProfile>("SELECT \"Required\", \"Ignored\" FROM \"ReleaseProfiles\"");
items.Should().HaveCount(0);
}
}
}

View File

@ -1,11 +1,12 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using FluentAssertions; using FluentAssertions;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Movies; using NzbDrone.Core.Movies;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Restrictions; using NzbDrone.Core.Profiles.Releases;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.DecisionEngineTests namespace NzbDrone.Core.Test.DecisionEngineTests
@ -30,16 +31,16 @@ public void Setup()
} }
}; };
Mocker.SetConstant<ITermMatcher>(Mocker.Resolve<TermMatcher>()); Mocker.SetConstant<ITermMatcherService>(Mocker.Resolve<TermMatcherService>());
} }
private void GivenRestictions(string required, string ignored) private void GivenRestictions(List<string> required, List<string> ignored)
{ {
Mocker.GetMock<IRestrictionService>() Mocker.GetMock<IReleaseProfileService>()
.Setup(s => s.AllForTags(It.IsAny<HashSet<int>>())) .Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
.Returns(new List<Restriction> .Returns(new List<ReleaseProfile>
{ {
new Restriction new ReleaseProfile()
{ {
Required = required, Required = required,
Ignored = ignored Ignored = ignored
@ -50,9 +51,9 @@ private void GivenRestictions(string required, string ignored)
[Test] [Test]
public void should_be_true_when_restrictions_are_empty() public void should_be_true_when_restrictions_are_empty()
{ {
Mocker.GetMock<IRestrictionService>() Mocker.GetMock<IReleaseProfileService>()
.Setup(s => s.AllForTags(It.IsAny<HashSet<int>>())) .Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
.Returns(new List<Restriction>()); .Returns(new List<ReleaseProfile>());
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue(); Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
} }
@ -60,7 +61,7 @@ public void should_be_true_when_restrictions_are_empty()
[Test] [Test]
public void should_be_true_when_title_contains_one_required_term() public void should_be_true_when_title_contains_one_required_term()
{ {
GivenRestictions("WEBRip", null); GivenRestictions(new List<string> { "WEBRip" }, new List<string>());
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue(); Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
} }
@ -68,7 +69,7 @@ public void should_be_true_when_title_contains_one_required_term()
[Test] [Test]
public void should_be_false_when_title_does_not_contain_any_required_terms() public void should_be_false_when_title_does_not_contain_any_required_terms()
{ {
GivenRestictions("doesnt,exist", null); GivenRestictions(new List<string> { "doesnt", "exist" }, new List<string>());
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse(); Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
} }
@ -76,7 +77,7 @@ public void should_be_false_when_title_does_not_contain_any_required_terms()
[Test] [Test]
public void should_be_true_when_title_does_not_contain_any_ignored_terms() public void should_be_true_when_title_does_not_contain_any_ignored_terms()
{ {
GivenRestictions(null, "ignored"); GivenRestictions(new List<string>(), new List<string> { "ignored" });
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue(); Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
} }
@ -84,7 +85,7 @@ public void should_be_true_when_title_does_not_contain_any_ignored_terms()
[Test] [Test]
public void should_be_false_when_title_contains_one_anded_ignored_terms() public void should_be_false_when_title_contains_one_anded_ignored_terms()
{ {
GivenRestictions(null, "edited"); GivenRestictions(new List<string>(), new List<string> { "edited" });
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse(); Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
} }
@ -95,7 +96,7 @@ public void should_be_false_when_title_contains_one_anded_ignored_terms()
[TestCase("X264,NOTTHERE")] [TestCase("X264,NOTTHERE")]
public void should_ignore_case_when_matching_required(string required) public void should_ignore_case_when_matching_required(string required)
{ {
GivenRestictions(required, null); GivenRestictions(required.Split(',').ToList(), new List<string>());
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue(); Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
} }
@ -106,7 +107,7 @@ public void should_ignore_case_when_matching_required(string required)
[TestCase("X264,NOTTHERE")] [TestCase("X264,NOTTHERE")]
public void should_ignore_case_when_matching_ignored(string ignored) public void should_ignore_case_when_matching_ignored(string ignored)
{ {
GivenRestictions(null, ignored); GivenRestictions(new List<string>(), ignored.Split(',').ToList());
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse(); Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
} }
@ -116,11 +117,15 @@ public void should_be_false_when_release_contains_one_restricted_word_and_one_re
{ {
_remoteMovie.Release.Title = "[ www.Speed.cd ] -Whose.Line.is.it.Anyway.US.S10E24.720p.HDTV.x264-BAJSKORV"; _remoteMovie.Release.Title = "[ www.Speed.cd ] -Whose.Line.is.it.Anyway.US.S10E24.720p.HDTV.x264-BAJSKORV";
Mocker.GetMock<IRestrictionService>() Mocker.GetMock<IReleaseProfileService>()
.Setup(s => s.AllForTags(It.IsAny<HashSet<int>>())) .Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
.Returns(new List<Restriction> .Returns(new List<ReleaseProfile>
{ {
new Restriction { Required = "x264", Ignored = "www.Speed.cd" } new ReleaseProfile
{
Required = new List<string> { "x264" },
Ignored = new List<string> { "www.Speed.cd" }
}
}); });
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse(); Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
@ -132,7 +137,7 @@ public void should_be_false_when_release_contains_one_restricted_word_and_one_re
[TestCase(@"/\.WEB/", true)] [TestCase(@"/\.WEB/", true)]
public void should_match_perl_regex(string pattern, bool expected) public void should_match_perl_regex(string pattern, bool expected)
{ {
GivenRestictions(pattern, null); GivenRestictions(pattern.Split(',').ToList(), new List<string>());
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().Be(expected); Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().Be(expected);
} }

View File

@ -2,7 +2,7 @@
using FluentAssertions; using FluentAssertions;
using NUnit.Framework; using NUnit.Framework;
using NzbDrone.Core.Housekeeping.Housekeepers; using NzbDrone.Core.Housekeeping.Housekeepers;
using NzbDrone.Core.Restrictions; using NzbDrone.Core.Profiles.Releases;
using NzbDrone.Core.Tags; using NzbDrone.Core.Tags;
using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Test.Framework;
@ -33,7 +33,7 @@ public void should_not_delete_used_tags()
.BuildList(); .BuildList();
Db.InsertMany(tags); Db.InsertMany(tags);
var restrictions = Builder<Restriction>.CreateListOfSize(2) var restrictions = Builder<ReleaseProfile>.CreateListOfSize(2)
.All() .All()
.With(v => v.Id = 0) .With(v => v.Id = 0)
.With(v => v.Tags.Add(tags[0].Id)) .With(v => v.Tags.Add(tags[0].Id))

View File

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using Dapper;
using FluentMigrator;
using NzbDrone.Common.Serializer;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(229)]
public class update_restrictions_to_release_profiles : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Rename.Table("Restrictions").To("ReleaseProfiles");
Alter.Table("ReleaseProfiles").AddColumn("Name").AsString().Nullable().WithDefaultValue(null);
Alter.Table("ReleaseProfiles").AddColumn("Enabled").AsBoolean().WithDefaultValue(true);
Alter.Table("ReleaseProfiles").AddColumn("IndexerId").AsInt32().WithDefaultValue(0);
Delete.Column("Preferred").FromTable("ReleaseProfiles");
Execute.WithConnection(ChangeRequiredIgnoredTypes);
Delete.FromTable("ReleaseProfiles").Row(new { Required = "[]", Ignored = "[]" });
}
// Update the Required and Ignored columns to be JSON arrays instead of comma separated strings
private void ChangeRequiredIgnoredTypes(IDbConnection conn, IDbTransaction tran)
{
var updatedReleaseProfiles = new List<object>();
using (var getEmailCmd = conn.CreateCommand())
{
getEmailCmd.Transaction = tran;
getEmailCmd.CommandText = "SELECT \"Id\", \"Required\", \"Ignored\" FROM \"ReleaseProfiles\"";
using var reader = getEmailCmd.ExecuteReader();
while (reader.Read())
{
var id = reader.GetInt32(0);
var requiredObj = reader.GetValue(1);
var ignoredObj = reader.GetValue(2);
var required = requiredObj == DBNull.Value
? Enumerable.Empty<string>()
: requiredObj.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
var ignored = ignoredObj == DBNull.Value
? Enumerable.Empty<string>()
: ignoredObj.ToString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
updatedReleaseProfiles.Add(new
{
Id = id,
Required = required.ToJson(),
Ignored = ignored.ToJson()
});
}
}
var updateReleaseProfilesSql = "UPDATE \"ReleaseProfiles\" SET \"Required\" = @Required, \"Ignored\" = @Ignored WHERE \"Id\" = @Id";
conn.Execute(updateReleaseProfilesSql, updatedReleaseProfiles, transaction: tran);
}
}
}

View File

@ -37,9 +37,9 @@
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles; using NzbDrone.Core.Profiles;
using NzbDrone.Core.Profiles.Delay; using NzbDrone.Core.Profiles.Delay;
using NzbDrone.Core.Profiles.Releases;
using NzbDrone.Core.Qualities; using NzbDrone.Core.Qualities;
using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Restrictions;
using NzbDrone.Core.RootFolders; using NzbDrone.Core.RootFolders;
using NzbDrone.Core.Tags; using NzbDrone.Core.Tags;
using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.ThingiProvider;
@ -154,7 +154,7 @@ public static void Map()
Mapper.Entity<RemotePathMapping>("RemotePathMappings").RegisterModel(); Mapper.Entity<RemotePathMapping>("RemotePathMappings").RegisterModel();
Mapper.Entity<Tag>("Tags").RegisterModel(); Mapper.Entity<Tag>("Tags").RegisterModel();
Mapper.Entity<Restriction>("Restrictions").RegisterModel(); Mapper.Entity<ReleaseProfile>("ReleaseProfiles").RegisterModel();
Mapper.Entity<DelayProfile>("DelayProfiles").RegisterModel(); Mapper.Entity<DelayProfile>("DelayProfiles").RegisterModel();
Mapper.Entity<User>("Users").RegisterModel(); Mapper.Entity<User>("Users").RegisterModel();

View File

@ -173,8 +173,19 @@ private IEnumerable<DownloadDecision> GetDecisions(List<ReleaseInfo> reports, bo
private DownloadDecision GetDecisionForReport(RemoteMovie remoteMovie, SearchCriteriaBase searchCriteria = null) private DownloadDecision GetDecisionForReport(RemoteMovie remoteMovie, SearchCriteriaBase searchCriteria = null)
{ {
var reasons = _specifications.Select(c => EvaluateSpec(c, remoteMovie, searchCriteria)) var reasons = Array.Empty<Rejection>();
.Where(c => c != null);
foreach (var specifications in _specifications.GroupBy(v => v.Priority).OrderBy(v => v.Key))
{
reasons = specifications.Select(c => EvaluateSpec(c, remoteMovie, searchCriteria))
.Where(c => c != null)
.ToArray();
if (reasons.Any())
{
break;
}
}
return new DownloadDecision(remoteMovie, reasons.ToArray()); return new DownloadDecision(remoteMovie, reasons.ToArray());
} }

View File

@ -1,25 +1,24 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Restrictions; using NzbDrone.Core.Profiles.Releases;
namespace NzbDrone.Core.DecisionEngine.Specifications namespace NzbDrone.Core.DecisionEngine.Specifications
{ {
public class ReleaseRestrictionsSpecification : IDecisionEngineSpecification public class ReleaseRestrictionsSpecification : IDecisionEngineSpecification
{ {
private readonly Logger _logger; private readonly Logger _logger;
private readonly IRestrictionService _restrictionService; private readonly IReleaseProfileService _releaseProfileService;
private readonly ITermMatcher _termMatcher; private readonly ITermMatcherService _termMatcherService;
public ReleaseRestrictionsSpecification(ITermMatcher termMatcher, IRestrictionService restrictionService, Logger logger) public ReleaseRestrictionsSpecification(ITermMatcherService termMatcherService, IReleaseProfileService releaseProfileService, Logger logger)
{ {
_logger = logger; _logger = logger;
_restrictionService = restrictionService; _releaseProfileService = releaseProfileService;
_termMatcher = termMatcher; _termMatcherService = termMatcherService;
} }
public SpecificationPriority Priority => SpecificationPriority.Default; public SpecificationPriority Priority => SpecificationPriority.Default;
@ -30,14 +29,14 @@ public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase se
_logger.Debug("Checking if release meets restrictions: {0}", subject); _logger.Debug("Checking if release meets restrictions: {0}", subject);
var title = subject.Release.Title; var title = subject.Release.Title;
var restrictions = _restrictionService.AllForTags(subject.Movie.Tags); var releaseProfiles = _releaseProfileService.EnabledForTags(subject.Movie.Tags, subject.Release.IndexerId);
var required = restrictions.Where(r => r.Required.IsNotNullOrWhiteSpace()); var required = releaseProfiles.Where(r => r.Required.Any());
var ignored = restrictions.Where(r => r.Ignored.IsNotNullOrWhiteSpace()); var ignored = releaseProfiles.Where(r => r.Ignored.Any());
foreach (var r in required) foreach (var r in required)
{ {
var requiredTerms = r.Required.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); var requiredTerms = r.Required;
var foundTerms = ContainsAny(requiredTerms, title); var foundTerms = ContainsAny(requiredTerms, title);
if (foundTerms.Empty()) if (foundTerms.Empty())
@ -50,7 +49,7 @@ public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase se
foreach (var r in ignored) foreach (var r in ignored)
{ {
var ignoredTerms = r.Ignored.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); var ignoredTerms = r.Ignored;
var foundTerms = ContainsAny(ignoredTerms, title); var foundTerms = ContainsAny(ignoredTerms, title);
if (foundTerms.Any()) if (foundTerms.Any())
@ -67,7 +66,7 @@ public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase se
private List<string> ContainsAny(List<string> terms, string title) private List<string> ContainsAny(List<string> terms, string title)
{ {
return terms.Where(t => _termMatcher.IsMatch(t, title)).ToList(); return terms.Where(t => _termMatcherService.IsMatch(t, title)).ToList();
} }
} }
} }

View File

@ -19,7 +19,7 @@ public CleanupUnusedTags(IMainDatabase database)
public void Clean() public void Clean()
{ {
using var mapper = _database.OpenConnection(); using var mapper = _database.OpenConnection();
var usedTags = new[] { "Movies", "Notifications", "DelayProfiles", "Restrictions", "ImportLists", "Indexers", "AutoTagging", "DownloadClients" } var usedTags = new[] { "Movies", "Notifications", "DelayProfiles", "ReleaseProfiles", "ImportLists", "Indexers", "AutoTagging", "DownloadClients" }
.SelectMany(v => GetUsedTags(v, mapper)) .SelectMany(v => GetUsedTags(v, mapper))
.Distinct() .Distinct()
.ToList(); .ToList();

View File

@ -789,7 +789,7 @@
"PrioritySettings": "Priority: {priority}", "PrioritySettings": "Priority: {priority}",
"ProcessingFolders": "Processing Folders", "ProcessingFolders": "Processing Folders",
"Profiles": "Profiles", "Profiles": "Profiles",
"ProfilesSettingsSummary": "Quality, Language and Delay profiles", "ProfilesSettingsSummary": "Quality, Language, Delay and Release profiles",
"Progress": "Progress", "Progress": "Progress",
"Proper": "Proper", "Proper": "Proper",
"Protocol": "Protocol", "Protocol": "Protocol",

View File

@ -1,7 +1,7 @@
using System; using System;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace NzbDrone.Core.Restrictions namespace NzbDrone.Core.Profiles.Releases
{ {
public static class PerlRegexFactory public static class PerlRegexFactory
{ {

View File

@ -0,0 +1,32 @@
using System.Collections.Generic;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Profiles.Releases
{
public class ReleaseProfile : ModelBase
{
public string Name { get; set; }
public bool Enabled { get; set; }
public List<string> Required { get; set; }
public List<string> Ignored { get; set; }
public int IndexerId { get; set; }
public HashSet<int> Tags { get; set; }
public ReleaseProfile()
{
Enabled = true;
Required = new List<string>();
Ignored = new List<string>();
Tags = new HashSet<int>();
IndexerId = 0;
}
}
public class ReleaseProfilePreferredComparer : IComparer<KeyValuePair<string, int>>
{
public int Compare(KeyValuePair<string, int> x, KeyValuePair<string, int> y)
{
return y.Value.CompareTo(x.Value);
}
}
}

View File

@ -0,0 +1,17 @@
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Profiles.Releases
{
public interface IReleaseProfileRepository : IBasicRepository<ReleaseProfile>
{
}
public class ReleaseProfileRepository : BasicRepository<ReleaseProfile>, IReleaseProfileRepository
{
public ReleaseProfileRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
}
}

View File

@ -0,0 +1,75 @@
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Core.Profiles.Releases
{
public interface IReleaseProfileService
{
List<ReleaseProfile> All();
List<ReleaseProfile> AllForTag(int tagId);
List<ReleaseProfile> AllForTags(HashSet<int> tagIds);
List<ReleaseProfile> EnabledForTags(HashSet<int> tagIds, int indexerId);
ReleaseProfile Get(int id);
void Delete(int id);
ReleaseProfile Add(ReleaseProfile restriction);
ReleaseProfile Update(ReleaseProfile restriction);
}
public class ReleaseProfileService : IReleaseProfileService
{
private readonly IReleaseProfileRepository _repo;
private readonly Logger _logger;
public ReleaseProfileService(IReleaseProfileRepository repo, Logger logger)
{
_repo = repo;
_logger = logger;
}
public List<ReleaseProfile> All()
{
var all = _repo.All().ToList();
return all;
}
public List<ReleaseProfile> AllForTag(int tagId)
{
return _repo.All().Where(r => r.Tags.Contains(tagId)).ToList();
}
public List<ReleaseProfile> AllForTags(HashSet<int> tagIds)
{
return _repo.All().Where(r => r.Tags.Intersect(tagIds).Any() || r.Tags.Empty()).ToList();
}
public List<ReleaseProfile> EnabledForTags(HashSet<int> tagIds, int indexerId)
{
return AllForTags(tagIds)
.Where(r => r.Enabled)
.Where(r => r.IndexerId == indexerId || r.IndexerId == 0).ToList();
}
public ReleaseProfile Get(int id)
{
return _repo.Get(id);
}
public void Delete(int id)
{
_repo.Delete(id);
}
public ReleaseProfile Add(ReleaseProfile restriction)
{
return _repo.Insert(restriction);
}
public ReleaseProfile Update(ReleaseProfile restriction)
{
return _repo.Update(restriction);
}
}
}

View File

@ -0,0 +1,49 @@
using System;
using NzbDrone.Common.Cache;
using NzbDrone.Core.Profiles.Releases.TermMatchers;
namespace NzbDrone.Core.Profiles.Releases
{
public interface ITermMatcherService
{
bool IsMatch(string term, string value);
string MatchingTerm(string term, string value);
}
public class TermMatcherService : ITermMatcherService
{
private ICached<ITermMatcher> _matcherCache;
public TermMatcherService(ICacheManager cacheManager)
{
_matcherCache = cacheManager.GetCache<ITermMatcher>(GetType());
}
public bool IsMatch(string term, string value)
{
return GetMatcher(term).IsMatch(value);
}
public string MatchingTerm(string term, string value)
{
return GetMatcher(term).MatchingTerm(value);
}
public ITermMatcher GetMatcher(string term)
{
return _matcherCache.Get(term, () => CreateMatcherInternal(term), TimeSpan.FromHours(24));
}
private ITermMatcher CreateMatcherInternal(string term)
{
if (PerlRegexFactory.TryCreateRegex(term, out var regex))
{
return new RegexTermMatcher(regex);
}
else
{
return new CaseInsensitiveTermMatcher(term);
}
}
}
}

View File

@ -0,0 +1,29 @@
namespace NzbDrone.Core.Profiles.Releases.TermMatchers
{
public sealed class CaseInsensitiveTermMatcher : ITermMatcher
{
private readonly string _originalTerm;
private readonly string _term;
public CaseInsensitiveTermMatcher(string term)
{
_originalTerm = term;
_term = term.ToLowerInvariant();
}
public bool IsMatch(string value)
{
return value.ToLowerInvariant().Contains(_term);
}
public string MatchingTerm(string value)
{
if (value.ToLowerInvariant().Contains(_term))
{
return _originalTerm;
}
return null;
}
}
}

View File

@ -0,0 +1,8 @@
namespace NzbDrone.Core.Profiles.Releases.TermMatchers
{
public interface ITermMatcher
{
bool IsMatch(string value);
string MatchingTerm(string value);
}
}

View File

@ -0,0 +1,24 @@
using System.Text.RegularExpressions;
namespace NzbDrone.Core.Profiles.Releases.TermMatchers
{
public class RegexTermMatcher : ITermMatcher
{
private readonly Regex _regex;
public RegexTermMatcher(Regex regex)
{
_regex = regex;
}
public bool IsMatch(string value)
{
return _regex.IsMatch(value);
}
public string MatchingTerm(string value)
{
return _regex.Match(value).Value;
}
}
}

View File

@ -1,18 +0,0 @@
using System.Collections.Generic;
using NzbDrone.Core.Datastore;
namespace NzbDrone.Core.Restrictions
{
public class Restriction : ModelBase
{
public string Required { get; set; }
public string Preferred { get; set; }
public string Ignored { get; set; }
public HashSet<int> Tags { get; set; }
public Restriction()
{
Tags = new HashSet<int>();
}
}
}

View File

@ -1,17 +0,0 @@
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Restrictions
{
public interface IRestrictionRepository : IBasicRepository<Restriction>
{
}
public class RestrictionRepository : BasicRepository<Restriction>, IRestrictionRepository
{
public RestrictionRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
}
}

View File

@ -1,65 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using NLog;
using NzbDrone.Common.Extensions;
namespace NzbDrone.Core.Restrictions
{
public interface IRestrictionService
{
List<Restriction> All();
List<Restriction> AllForTag(int tagId);
List<Restriction> AllForTags(HashSet<int> tagIds);
Restriction Get(int id);
void Delete(int id);
Restriction Add(Restriction restriction);
Restriction Update(Restriction restriction);
}
public class RestrictionService : IRestrictionService
{
private readonly IRestrictionRepository _repo;
private readonly Logger _logger;
public RestrictionService(IRestrictionRepository repo, Logger logger)
{
_repo = repo;
_logger = logger;
}
public List<Restriction> All()
{
return _repo.All().ToList();
}
public List<Restriction> AllForTag(int tagId)
{
return _repo.All().Where(r => r.Tags.Contains(tagId)).ToList();
}
public List<Restriction> AllForTags(HashSet<int> tagIds)
{
return _repo.All().Where(r => r.Tags.Intersect(tagIds).Any() || r.Tags.Empty()).ToList();
}
public Restriction Get(int id)
{
return _repo.Get(id);
}
public void Delete(int id)
{
_repo.Delete(id);
}
public Restriction Add(Restriction restriction)
{
return _repo.Insert(restriction);
}
public Restriction Update(Restriction restriction)
{
return _repo.Update(restriction);
}
}
}

View File

@ -1,57 +0,0 @@
using System;
using NzbDrone.Common.Cache;
namespace NzbDrone.Core.Restrictions
{
public interface ITermMatcher
{
bool IsMatch(string term, string value);
}
public class TermMatcher : ITermMatcher
{
private ICached<Predicate<string>> _matcherCache;
public TermMatcher(ICacheManager cacheManager)
{
_matcherCache = cacheManager.GetCache<Predicate<string>>(GetType());
}
public bool IsMatch(string term, string value)
{
return GetMatcher(term)(value);
}
public Predicate<string> GetMatcher(string term)
{
return _matcherCache.Get(term, () => CreateMatcherInternal(term), TimeSpan.FromHours(24));
}
private Predicate<string> CreateMatcherInternal(string term)
{
if (PerlRegexFactory.TryCreateRegex(term, out var regex))
{
return regex.IsMatch;
}
else
{
return new CaseInsensitiveTermMatcher(term).IsMatch;
}
}
private sealed class CaseInsensitiveTermMatcher
{
private readonly string _term;
public CaseInsensitiveTermMatcher(string term)
{
_term = term.ToLowerInvariant();
}
public bool IsMatch(string value)
{
return value.ToLowerInvariant().Contains(_term);
}
}
}
}

View File

@ -9,7 +9,7 @@ public class TagDetails : ModelBase
public string Label { get; set; } public string Label { get; set; }
public List<int> MovieIds { get; set; } public List<int> MovieIds { get; set; }
public List<int> NotificationIds { get; set; } public List<int> NotificationIds { get; set; }
public List<int> RestrictionIds { get; set; } public List<int> ReleaseProfileIds { get; set; }
public List<int> DelayProfileIds { get; set; } public List<int> DelayProfileIds { get; set; }
public List<int> ImportListIds { get; set; } public List<int> ImportListIds { get; set; }
public List<int> IndexerIds { get; set; } public List<int> IndexerIds { get; set; }
@ -18,7 +18,7 @@ public class TagDetails : ModelBase
public bool InUse => MovieIds.Any() || public bool InUse => MovieIds.Any() ||
NotificationIds.Any() || NotificationIds.Any() ||
RestrictionIds.Any() || ReleaseProfileIds.Any() ||
DelayProfileIds.Any() || DelayProfileIds.Any() ||
ImportListIds.Any() || ImportListIds.Any() ||
IndexerIds.Any() || IndexerIds.Any() ||

View File

@ -9,7 +9,7 @@
using NzbDrone.Core.Movies; using NzbDrone.Core.Movies;
using NzbDrone.Core.Notifications; using NzbDrone.Core.Notifications;
using NzbDrone.Core.Profiles.Delay; using NzbDrone.Core.Profiles.Delay;
using NzbDrone.Core.Restrictions; using NzbDrone.Core.Profiles.Releases;
namespace NzbDrone.Core.Tags namespace NzbDrone.Core.Tags
{ {
@ -33,7 +33,7 @@ public class TagService : ITagService
private readonly IDelayProfileService _delayProfileService; private readonly IDelayProfileService _delayProfileService;
private readonly IImportListFactory _importListFactory; private readonly IImportListFactory _importListFactory;
private readonly INotificationFactory _notificationFactory; private readonly INotificationFactory _notificationFactory;
private readonly IRestrictionService _restrictionService; private readonly IReleaseProfileService _releaseProfileService;
private readonly IMovieService _movieService; private readonly IMovieService _movieService;
private readonly IIndexerFactory _indexerService; private readonly IIndexerFactory _indexerService;
private readonly IAutoTaggingService _autoTaggingService; private readonly IAutoTaggingService _autoTaggingService;
@ -44,7 +44,7 @@ public TagService(ITagRepository repo,
IDelayProfileService delayProfileService, IDelayProfileService delayProfileService,
IImportListFactory importListFactory, IImportListFactory importListFactory,
INotificationFactory notificationFactory, INotificationFactory notificationFactory,
IRestrictionService restrictionService, IReleaseProfileService releaseProfileService,
IMovieService movieService, IMovieService movieService,
IIndexerFactory indexerService, IIndexerFactory indexerService,
IAutoTaggingService autoTaggingService, IAutoTaggingService autoTaggingService,
@ -55,7 +55,7 @@ public TagService(ITagRepository repo,
_delayProfileService = delayProfileService; _delayProfileService = delayProfileService;
_importListFactory = importListFactory; _importListFactory = importListFactory;
_notificationFactory = notificationFactory; _notificationFactory = notificationFactory;
_restrictionService = restrictionService; _releaseProfileService = releaseProfileService;
_movieService = movieService; _movieService = movieService;
_indexerService = indexerService; _indexerService = indexerService;
_autoTaggingService = autoTaggingService; _autoTaggingService = autoTaggingService;
@ -90,7 +90,7 @@ public TagDetails Details(int tagId)
var delayProfiles = _delayProfileService.AllForTag(tagId); var delayProfiles = _delayProfileService.AllForTag(tagId);
var importLists = _importListFactory.AllForTag(tagId); var importLists = _importListFactory.AllForTag(tagId);
var notifications = _notificationFactory.AllForTag(tagId); var notifications = _notificationFactory.AllForTag(tagId);
var restrictions = _restrictionService.AllForTag(tagId); var releaseProfiles = _releaseProfileService.AllForTag(tagId);
var movies = _movieService.AllMovieTags().Where(x => x.Value.Contains(tagId)).Select(x => x.Key).ToList(); var movies = _movieService.AllMovieTags().Where(x => x.Value.Contains(tagId)).Select(x => x.Key).ToList();
var indexers = _indexerService.AllForTag(tagId); var indexers = _indexerService.AllForTag(tagId);
var autoTags = _autoTaggingService.AllForTag(tagId); var autoTags = _autoTaggingService.AllForTag(tagId);
@ -103,7 +103,7 @@ public TagDetails Details(int tagId)
DelayProfileIds = delayProfiles.Select(c => c.Id).ToList(), DelayProfileIds = delayProfiles.Select(c => c.Id).ToList(),
ImportListIds = importLists.Select(c => c.Id).ToList(), ImportListIds = importLists.Select(c => c.Id).ToList(),
NotificationIds = notifications.Select(c => c.Id).ToList(), NotificationIds = notifications.Select(c => c.Id).ToList(),
RestrictionIds = restrictions.Select(c => c.Id).ToList(), ReleaseProfileIds = releaseProfiles.Select(c => c.Id).ToList(),
MovieIds = movies, MovieIds = movies,
IndexerIds = indexers.Select(c => c.Id).ToList(), IndexerIds = indexers.Select(c => c.Id).ToList(),
AutoTagIds = autoTags.Select(c => c.Id).ToList(), AutoTagIds = autoTags.Select(c => c.Id).ToList(),
@ -117,7 +117,7 @@ public List<TagDetails> Details()
var delayProfiles = _delayProfileService.All(); var delayProfiles = _delayProfileService.All();
var importLists = _importListFactory.All(); var importLists = _importListFactory.All();
var notifications = _notificationFactory.All(); var notifications = _notificationFactory.All();
var restrictions = _restrictionService.All(); var releaseProfiles = _releaseProfileService.All();
var movies = _movieService.AllMovieTags(); var movies = _movieService.AllMovieTags();
var indexers = _indexerService.All(); var indexers = _indexerService.All();
var autotags = _autoTaggingService.All(); var autotags = _autoTaggingService.All();
@ -134,7 +134,7 @@ public List<TagDetails> Details()
DelayProfileIds = delayProfiles.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), DelayProfileIds = delayProfiles.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
ImportListIds = importLists.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), ImportListIds = importLists.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
NotificationIds = notifications.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), NotificationIds = notifications.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
RestrictionIds = restrictions.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), ReleaseProfileIds = releaseProfiles.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
MovieIds = movies.Where(c => c.Value.Contains(tag.Id)).Select(c => c.Key).ToList(), MovieIds = movies.Where(c => c.Value.Contains(tag.Id)).Select(c => c.Key).ToList(),
IndexerIds = indexers.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), IndexerIds = indexers.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
AutoTagIds = autotags.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(), AutoTagIds = autotags.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),

View File

@ -0,0 +1,73 @@
using System.Collections.Generic;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Profiles.Releases;
using Radarr.Http;
using Radarr.Http.REST;
using Radarr.Http.REST.Attributes;
namespace Radarr.Api.V3.Profiles.Release
{
[V3ApiController]
public class ReleaseProfileController : RestController<ReleaseProfileResource>
{
private readonly IReleaseProfileService _profileService;
private readonly IIndexerFactory _indexerFactory;
public ReleaseProfileController(IReleaseProfileService profileService, IIndexerFactory indexerFactory)
{
_profileService = profileService;
_indexerFactory = indexerFactory;
SharedValidator.RuleFor(d => d).Custom((restriction, context) =>
{
if (restriction.MapIgnored().Empty() && restriction.MapRequired().Empty())
{
context.AddFailure("'Must contain' or 'Must not contain' is required");
}
if (restriction.Enabled && restriction.IndexerId != 0 && !_indexerFactory.Exists(restriction.IndexerId))
{
context.AddFailure(nameof(ReleaseProfile.IndexerId), "Indexer does not exist");
}
});
}
[RestPostById]
public ActionResult<ReleaseProfileResource> Create(ReleaseProfileResource resource)
{
var model = resource.ToModel();
model = _profileService.Add(model);
return Created(model.Id);
}
[RestDeleteById]
public void DeleteProfile(int id)
{
_profileService.Delete(id);
}
[RestPutById]
public ActionResult<ReleaseProfileResource> Update(ReleaseProfileResource resource)
{
var model = resource.ToModel();
_profileService.Update(model);
return Accepted(model.Id);
}
protected override ReleaseProfileResource GetResourceById(int id)
{
return _profileService.Get(id).ToResource();
}
[HttpGet]
public List<ReleaseProfileResource> GetAll()
{
return _profileService.All().ToResource();
}
}
}

View File

@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using NzbDrone.Core.Profiles.Releases;
using Radarr.Http.REST;
namespace Radarr.Api.V3.Profiles.Release
{
public class ReleaseProfileResource : RestResource
{
public string Name { get; set; }
public bool Enabled { get; set; }
// Is List<string>, string or JArray, we accept 'string' with POST and PUT for backwards compatibility
public object Required { get; set; }
public object Ignored { get; set; }
public int IndexerId { get; set; }
public HashSet<int> Tags { get; set; }
public ReleaseProfileResource()
{
Tags = new HashSet<int>();
}
}
public static class RestrictionResourceMapper
{
public static ReleaseProfileResource ToResource(this ReleaseProfile model)
{
if (model == null)
{
return null;
}
return new ReleaseProfileResource
{
Id = model.Id,
Name = model.Name,
Enabled = model.Enabled,
Required = model.Required ?? new List<string>(),
Ignored = model.Ignored ?? new List<string>(),
IndexerId = model.IndexerId,
Tags = new HashSet<int>(model.Tags)
};
}
public static ReleaseProfile ToModel(this ReleaseProfileResource resource)
{
if (resource == null)
{
return null;
}
return new ReleaseProfile
{
Id = resource.Id,
Name = resource.Name,
Enabled = resource.Enabled,
Required = resource.MapRequired(),
Ignored = resource.MapIgnored(),
IndexerId = resource.IndexerId,
Tags = new HashSet<int>(resource.Tags)
};
}
public static List<ReleaseProfileResource> ToResource(this IEnumerable<ReleaseProfile> models)
{
return models.Select(ToResource).ToList();
}
public static List<string> MapRequired(this ReleaseProfileResource profile) => ParseArray(profile.Required, "required");
public static List<string> MapIgnored(this ReleaseProfileResource profile) => ParseArray(profile.Ignored, "ignored");
private static List<string> ParseArray(object resource, string title)
{
if (resource == null)
{
return new List<string>();
}
if (resource is List<string> list)
{
return list;
}
if (resource is JsonElement array)
{
if (array.ValueKind == JsonValueKind.String)
{
return array.GetString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
}
if (array.ValueKind == JsonValueKind.Array)
{
return JsonSerializer.Deserialize<List<string>>(array);
}
}
if (resource is string str)
{
return str.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
}
throw new BadRequestException($"Invalid field {title}, should be string or string array");
}
}
}

View File

@ -1,60 +0,0 @@
using System.Collections.Generic;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Restrictions;
using Radarr.Http;
using Radarr.Http.REST;
using Radarr.Http.REST.Attributes;
namespace Radarr.Api.V3.Restrictions
{
[V3ApiController]
public class RestrictionController : RestController<RestrictionResource>
{
private readonly IRestrictionService _restrictionService;
public RestrictionController(IRestrictionService restrictionService)
{
_restrictionService = restrictionService;
SharedValidator.RuleFor(d => d).Custom((restriction, context) =>
{
if (restriction.Ignored.IsNullOrWhiteSpace() && restriction.Required.IsNullOrWhiteSpace())
{
context.AddFailure("Either 'Must contain' or 'Must not contain' is required");
}
});
}
protected override RestrictionResource GetResourceById(int id)
{
return _restrictionService.Get(id).ToResource();
}
[HttpGet]
public List<RestrictionResource> GetAll()
{
return _restrictionService.All().ToResource();
}
[RestPostById]
public ActionResult<RestrictionResource> Create(RestrictionResource resource)
{
return Created(_restrictionService.Add(resource.ToModel()).Id);
}
[RestPutById]
public ActionResult<RestrictionResource> Update(RestrictionResource resource)
{
_restrictionService.Update(resource.ToModel());
return Accepted(resource.Id);
}
[RestDeleteById]
public void DeleteRestriction(int id)
{
_restrictionService.Delete(id);
}
}
}

View File

@ -1,64 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.Restrictions;
using Radarr.Http.REST;
namespace Radarr.Api.V3.Restrictions
{
public class RestrictionResource : RestResource
{
public string Required { get; set; }
public string Preferred { get; set; }
public string Ignored { get; set; }
public HashSet<int> Tags { get; set; }
public RestrictionResource()
{
Tags = new HashSet<int>();
}
}
public static class RestrictionResourceMapper
{
public static RestrictionResource ToResource(this Restriction model)
{
if (model == null)
{
return null;
}
return new RestrictionResource
{
Id = model.Id,
Required = model.Required,
Preferred = model.Preferred,
Ignored = model.Ignored,
Tags = new HashSet<int>(model.Tags)
};
}
public static Restriction ToModel(this RestrictionResource resource)
{
if (resource == null)
{
return null;
}
return new Restriction
{
Id = resource.Id,
Required = resource.Required,
Preferred = resource.Preferred,
Ignored = resource.Ignored,
Tags = new HashSet<int>(resource.Tags)
};
}
public static List<RestrictionResource> ToResource(this IEnumerable<Restriction> models)
{
return models.Select(ToResource).ToList();
}
}
}

View File

@ -11,7 +11,7 @@ public class TagDetailsResource : RestResource
public List<int> DelayProfileIds { get; set; } public List<int> DelayProfileIds { get; set; }
public List<int> ImportListIds { get; set; } public List<int> ImportListIds { get; set; }
public List<int> NotificationIds { get; set; } public List<int> NotificationIds { get; set; }
public List<int> RestrictionIds { get; set; } public List<int> ReleaseProfileIds { get; set; }
public List<int> IndexerIds { get; set; } public List<int> IndexerIds { get; set; }
public List<int> DownloadClientIds { get; set; } public List<int> DownloadClientIds { get; set; }
public List<int> AutoTagIds { get; set; } public List<int> AutoTagIds { get; set; }
@ -34,7 +34,7 @@ public static TagDetailsResource ToResource(this TagDetails model)
DelayProfileIds = model.DelayProfileIds, DelayProfileIds = model.DelayProfileIds,
ImportListIds = model.ImportListIds, ImportListIds = model.ImportListIds,
NotificationIds = model.NotificationIds, NotificationIds = model.NotificationIds,
RestrictionIds = model.RestrictionIds, ReleaseProfileIds = model.ReleaseProfileIds,
IndexerIds = model.IndexerIds, IndexerIds = model.IndexerIds,
DownloadClientIds = model.DownloadClientIds, DownloadClientIds = model.DownloadClientIds,
AutoTagIds = model.AutoTagIds, AutoTagIds = model.AutoTagIds,