New: Convert restrictions to release profiles

pull/9009/head
ricci2511 1 year ago committed by Qstick
parent ca93a72d63
commit 99ff6aa9c4

@ -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;

@ -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);

@ -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
}); });
}; };

@ -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,

@ -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}

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

@ -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;

@ -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);

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

@ -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;

@ -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>

@ -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;

@ -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);

@ -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%;
}

@ -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;

@ -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;

@ -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);

@ -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';

@ -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;

@ -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;

@ -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);

@ -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;

@ -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;

@ -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);

@ -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,

@ -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,

@ -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: [],

@ -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

@ -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)
}
};

@ -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)
}
};

@ -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);

@ -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);
}
}
}

@ -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 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
} }
}; };
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 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[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 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[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 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[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 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[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 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[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 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[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 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[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 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{ {
_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 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[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);
} }

@ -2,7 +2,7 @@ using FizzWare.NBuilder;
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 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers
.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))

@ -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);
}
}
}

@ -37,9 +37,9 @@ using NzbDrone.Core.Organizer;
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 @@ namespace NzbDrone.Core.Datastore
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();

@ -173,8 +173,19 @@ namespace NzbDrone.Core.DecisionEngine
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());
} }

@ -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 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
_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 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
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 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
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();
} }
} }
} }

@ -19,7 +19,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers
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();

@ -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",

@ -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
{ {

@ -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);
}
}
}

@ -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)
{
}
}
}

@ -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);
}
}
}

@ -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);
}
}
}
}

@ -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;
}
}
}

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

@ -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;
}
}
}

@ -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>();
}
}
}

@ -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)
{
}
}
}

@ -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);
}
}
}

@ -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);
}
}
}
}

@ -9,7 +9,7 @@ namespace NzbDrone.Core.Tags
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 @@ namespace NzbDrone.Core.Tags
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() ||

@ -9,7 +9,7 @@ using NzbDrone.Core.Messaging.Events;
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 @@ namespace NzbDrone.Core.Tags
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 @@ namespace NzbDrone.Core.Tags
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 @@ namespace NzbDrone.Core.Tags
_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 @@ namespace NzbDrone.Core.Tags
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 @@ namespace NzbDrone.Core.Tags
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 @@ namespace NzbDrone.Core.Tags
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 @@ namespace NzbDrone.Core.Tags
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(),

@ -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();
}
}
}

@ -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");
}
}
}

@ -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);
}
}
}

@ -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();
}
}
}

@ -11,7 +11,7 @@ namespace Radarr.Api.V3.Tags
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 @@ namespace Radarr.Api.V3.Tags
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,

Loading…
Cancel
Save