mirror of
https://github.com/Radarr/Radarr.git
synced 2024-10-05 15:47:20 +02:00
New: Convert restrictions to release profiles
This commit is contained in:
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;
|
||||||
|
|
||||||
|
93
frontend/src/Components/Form/IndexerSelectInputConnector.js
Normal file
93
frontend/src/Components/Form/IndexerSelectInputConnector.js
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
||||||
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.indexers,
|
||||||
|
(state, { includeAny }) => includeAny,
|
||||||
|
(indexers, includeAny) => {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items
|
||||||
|
} = indexers;
|
||||||
|
|
||||||
|
const values = items.sort(sortByName).map((indexer) => ({
|
||||||
|
key: indexer.id,
|
||||||
|
value: indexer.name
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (includeAny) {
|
||||||
|
values.unshift({
|
||||||
|
key: 0,
|
||||||
|
value: '(Any)'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
values
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchFetchIndexers: fetchIndexers
|
||||||
|
};
|
||||||
|
|
||||||
|
class IndexerSelectInputConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (!this.props.isPopulated) {
|
||||||
|
this.props.dispatchFetchIndexers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onChange = ({ name, value }) => {
|
||||||
|
this.props.onChange({ name, value: parseInt(value) });
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<EnhancedSelectInput
|
||||||
|
{...this.props}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexerSelectInputConnector.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||||
|
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
includeAny: PropTypes.bool.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchIndexers: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
IndexerSelectInputConnector.defaultProps = {
|
||||||
|
includeAny: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerSelectInputConnector);
|
@ -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;
|
196
frontend/src/Settings/Profiles/Release/ReleaseProfile.js
Normal file
196
frontend/src/Settings/Profiles/Release/ReleaseProfile.js
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Card from 'Components/Card';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import TagList from 'Components/TagList';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import EditReleaseProfileModalConnector from './EditReleaseProfileModalConnector';
|
||||||
|
import styles from './ReleaseProfile.css';
|
||||||
|
|
||||||
|
class ReleaseProfile extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isEditReleaseProfileModalOpen: false,
|
||||||
|
isDeleteReleaseProfileModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onEditReleaseProfilePress = () => {
|
||||||
|
this.setState({ isEditReleaseProfileModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onEditReleaseProfileModalClose = () => {
|
||||||
|
this.setState({ isEditReleaseProfileModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onDeleteReleaseProfilePress = () => {
|
||||||
|
this.setState({
|
||||||
|
isEditReleaseProfileModalOpen: false,
|
||||||
|
isDeleteReleaseProfileModalOpen: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onDeleteReleaseProfileModalClose= () => {
|
||||||
|
this.setState({ isDeleteReleaseProfileModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onConfirmDeleteReleaseProfile = () => {
|
||||||
|
this.props.onConfirmDeleteReleaseProfile(this.props.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
enabled,
|
||||||
|
required,
|
||||||
|
ignored,
|
||||||
|
tags,
|
||||||
|
indexerId,
|
||||||
|
tagList,
|
||||||
|
indexerList
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isEditReleaseProfileModalOpen,
|
||||||
|
isDeleteReleaseProfileModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const indexer = indexerList.find((i) => i.id === indexerId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={styles.releaseProfile}
|
||||||
|
overlayContent={true}
|
||||||
|
onPress={this.onEditReleaseProfilePress}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
name ?
|
||||||
|
<div className={styles.name}>
|
||||||
|
{name}
|
||||||
|
</div> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
required.map((item) => {
|
||||||
|
if (!item) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
className={styles.label}
|
||||||
|
key={item}
|
||||||
|
kind={kinds.SUCCESS}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
ignored.map((item) => {
|
||||||
|
if (!item) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
className={styles.label}
|
||||||
|
key={item}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TagList
|
||||||
|
tags={tags}
|
||||||
|
tagList={tagList}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
!enabled &&
|
||||||
|
<Label
|
||||||
|
kind={kinds.DISABLED}
|
||||||
|
outline={true}
|
||||||
|
>
|
||||||
|
Disabled
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
indexer &&
|
||||||
|
<Label
|
||||||
|
kind={kinds.INFO}
|
||||||
|
outline={true}
|
||||||
|
>
|
||||||
|
{indexer.name}
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<EditReleaseProfileModalConnector
|
||||||
|
id={id}
|
||||||
|
isOpen={isEditReleaseProfileModalOpen}
|
||||||
|
onModalClose={this.onEditReleaseProfileModalClose}
|
||||||
|
onDeleteReleaseProfilePress={this.onDeleteReleaseProfilePress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isDeleteReleaseProfileModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title="Delete ReleaseProfile"
|
||||||
|
message={'Are you sure you want to delete this releaseProfile?'}
|
||||||
|
confirmLabel="Delete"
|
||||||
|
onConfirm={this.onConfirmDeleteReleaseProfile}
|
||||||
|
onCancel={this.onDeleteReleaseProfileModalClose}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReleaseProfile.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
name: PropTypes.string,
|
||||||
|
enabled: PropTypes.bool.isRequired,
|
||||||
|
required: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
|
ignored: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
|
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
|
indexerId: PropTypes.number.isRequired,
|
||||||
|
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
indexerList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onConfirmDeleteReleaseProfile: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
ReleaseProfile.defaultProps = {
|
||||||
|
enabled: true,
|
||||||
|
required: [],
|
||||||
|
ignored: [],
|
||||||
|
indexerId: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReleaseProfile;
|
@ -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
|
||||||
|
71
frontend/src/Store/Actions/Settings/releaseProfiles.js
Normal file
71
frontend/src/Store/Actions/Settings/releaseProfiles.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||||
|
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
||||||
|
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||||
|
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||||
|
import { createThunk } from 'Store/thunks';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
const section = 'settings.releaseProfiles';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const FETCH_RELEASE_PROFILES = 'settings/releaseProfiles/fetchReleaseProfiles';
|
||||||
|
export const SAVE_RELEASE_PROFILE = 'settings/releaseProfiles/saveReleaseProfile';
|
||||||
|
export const DELETE_RELEASE_PROFILE = 'settings/releaseProfiles/deleteReleaseProfile';
|
||||||
|
export const SET_RELEASE_PROFILE_VALUE = 'settings/releaseProfiles/setReleaseProfileValue';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetchReleaseProfiles = createThunk(FETCH_RELEASE_PROFILES);
|
||||||
|
export const saveReleaseProfile = createThunk(SAVE_RELEASE_PROFILE);
|
||||||
|
export const deleteReleaseProfile = createThunk(DELETE_RELEASE_PROFILE);
|
||||||
|
|
||||||
|
export const setReleaseProfileValue = createAction(SET_RELEASE_PROFILE_VALUE, (payload) => {
|
||||||
|
return {
|
||||||
|
section,
|
||||||
|
...payload
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// Details
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
defaultState: {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
isSaving: false,
|
||||||
|
saveError: null,
|
||||||
|
items: [],
|
||||||
|
pendingChanges: {}
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
actionHandlers: {
|
||||||
|
[FETCH_RELEASE_PROFILES]: createFetchHandler(section, '/releaseprofile'),
|
||||||
|
|
||||||
|
[SAVE_RELEASE_PROFILE]: createSaveProviderHandler(section, '/releaseprofile'),
|
||||||
|
|
||||||
|
[DELETE_RELEASE_PROFILE]: createRemoveItemHandler(section, '/releaseprofile')
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
reducers: {
|
||||||
|
[SET_RELEASE_PROFILE_VALUE]: createSetSettingValueReducer(section)
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
@ -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 @@ public void Setup()
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Mocker.SetConstant<ITermMatcher>(Mocker.Resolve<TermMatcher>());
|
Mocker.SetConstant<ITermMatcherService>(Mocker.Resolve<TermMatcherService>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GivenRestictions(string required, string ignored)
|
private void GivenRestictions(List<string> required, List<string> ignored)
|
||||||
{
|
{
|
||||||
Mocker.GetMock<IRestrictionService>()
|
Mocker.GetMock<IReleaseProfileService>()
|
||||||
.Setup(s => s.AllForTags(It.IsAny<HashSet<int>>()))
|
.Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
|
||||||
.Returns(new List<Restriction>
|
.Returns(new List<ReleaseProfile>
|
||||||
{
|
{
|
||||||
new Restriction
|
new ReleaseProfile()
|
||||||
{
|
{
|
||||||
Required = required,
|
Required = required,
|
||||||
Ignored = ignored
|
Ignored = ignored
|
||||||
@ -50,9 +51,9 @@ private void GivenRestictions(string required, string ignored)
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_be_true_when_restrictions_are_empty()
|
public void should_be_true_when_restrictions_are_empty()
|
||||||
{
|
{
|
||||||
Mocker.GetMock<IRestrictionService>()
|
Mocker.GetMock<IReleaseProfileService>()
|
||||||
.Setup(s => s.AllForTags(It.IsAny<HashSet<int>>()))
|
.Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
|
||||||
.Returns(new List<Restriction>());
|
.Returns(new List<ReleaseProfile>());
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
|
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
|
||||||
}
|
}
|
||||||
@ -60,7 +61,7 @@ public void should_be_true_when_restrictions_are_empty()
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_be_true_when_title_contains_one_required_term()
|
public void should_be_true_when_title_contains_one_required_term()
|
||||||
{
|
{
|
||||||
GivenRestictions("WEBRip", null);
|
GivenRestictions(new List<string> { "WEBRip" }, new List<string>());
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
|
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
|
||||||
}
|
}
|
||||||
@ -68,7 +69,7 @@ public void should_be_true_when_title_contains_one_required_term()
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_be_false_when_title_does_not_contain_any_required_terms()
|
public void should_be_false_when_title_does_not_contain_any_required_terms()
|
||||||
{
|
{
|
||||||
GivenRestictions("doesnt,exist", null);
|
GivenRestictions(new List<string> { "doesnt", "exist" }, new List<string>());
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
|
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
|
||||||
}
|
}
|
||||||
@ -76,7 +77,7 @@ public void should_be_false_when_title_does_not_contain_any_required_terms()
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_be_true_when_title_does_not_contain_any_ignored_terms()
|
public void should_be_true_when_title_does_not_contain_any_ignored_terms()
|
||||||
{
|
{
|
||||||
GivenRestictions(null, "ignored");
|
GivenRestictions(new List<string>(), new List<string> { "ignored" });
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
|
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
|
||||||
}
|
}
|
||||||
@ -84,7 +85,7 @@ public void should_be_true_when_title_does_not_contain_any_ignored_terms()
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_be_false_when_title_contains_one_anded_ignored_terms()
|
public void should_be_false_when_title_contains_one_anded_ignored_terms()
|
||||||
{
|
{
|
||||||
GivenRestictions(null, "edited");
|
GivenRestictions(new List<string>(), new List<string> { "edited" });
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
|
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
|
||||||
}
|
}
|
||||||
@ -95,7 +96,7 @@ public void should_be_false_when_title_contains_one_anded_ignored_terms()
|
|||||||
[TestCase("X264,NOTTHERE")]
|
[TestCase("X264,NOTTHERE")]
|
||||||
public void should_ignore_case_when_matching_required(string required)
|
public void should_ignore_case_when_matching_required(string required)
|
||||||
{
|
{
|
||||||
GivenRestictions(required, null);
|
GivenRestictions(required.Split(',').ToList(), new List<string>());
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
|
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
|
||||||
}
|
}
|
||||||
@ -106,7 +107,7 @@ public void should_ignore_case_when_matching_required(string required)
|
|||||||
[TestCase("X264,NOTTHERE")]
|
[TestCase("X264,NOTTHERE")]
|
||||||
public void should_ignore_case_when_matching_ignored(string ignored)
|
public void should_ignore_case_when_matching_ignored(string ignored)
|
||||||
{
|
{
|
||||||
GivenRestictions(null, ignored);
|
GivenRestictions(new List<string>(), ignored.Split(',').ToList());
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
|
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
|
||||||
}
|
}
|
||||||
@ -116,11 +117,15 @@ public void should_be_false_when_release_contains_one_restricted_word_and_one_re
|
|||||||
{
|
{
|
||||||
_remoteMovie.Release.Title = "[ www.Speed.cd ] -Whose.Line.is.it.Anyway.US.S10E24.720p.HDTV.x264-BAJSKORV";
|
_remoteMovie.Release.Title = "[ www.Speed.cd ] -Whose.Line.is.it.Anyway.US.S10E24.720p.HDTV.x264-BAJSKORV";
|
||||||
|
|
||||||
Mocker.GetMock<IRestrictionService>()
|
Mocker.GetMock<IReleaseProfileService>()
|
||||||
.Setup(s => s.AllForTags(It.IsAny<HashSet<int>>()))
|
.Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
|
||||||
.Returns(new List<Restriction>
|
.Returns(new List<ReleaseProfile>
|
||||||
{
|
{
|
||||||
new Restriction { Required = "x264", Ignored = "www.Speed.cd" }
|
new ReleaseProfile
|
||||||
|
{
|
||||||
|
Required = new List<string> { "x264" },
|
||||||
|
Ignored = new List<string> { "www.Speed.cd" }
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
|
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
|
||||||
@ -132,7 +137,7 @@ public void should_be_false_when_release_contains_one_restricted_word_and_one_re
|
|||||||
[TestCase(@"/\.WEB/", true)]
|
[TestCase(@"/\.WEB/", true)]
|
||||||
public void should_match_perl_regex(string pattern, bool expected)
|
public void should_match_perl_regex(string pattern, bool expected)
|
||||||
{
|
{
|
||||||
GivenRestictions(pattern, null);
|
GivenRestictions(pattern.Split(',').ToList(), new List<string>());
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().Be(expected);
|
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().Be(expected);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Core.Housekeeping.Housekeepers;
|
using NzbDrone.Core.Housekeeping.Housekeepers;
|
||||||
using NzbDrone.Core.Restrictions;
|
using NzbDrone.Core.Profiles.Releases;
|
||||||
using NzbDrone.Core.Tags;
|
using NzbDrone.Core.Tags;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ public void should_not_delete_used_tags()
|
|||||||
.BuildList();
|
.BuildList();
|
||||||
Db.InsertMany(tags);
|
Db.InsertMany(tags);
|
||||||
|
|
||||||
var restrictions = Builder<Restriction>.CreateListOfSize(2)
|
var restrictions = Builder<ReleaseProfile>.CreateListOfSize(2)
|
||||||
.All()
|
.All()
|
||||||
.With(v => v.Id = 0)
|
.With(v => v.Id = 0)
|
||||||
.With(v => v.Tags.Add(tags[0].Id))
|
.With(v => v.Tags.Add(tags[0].Id))
|
||||||
|
@ -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.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.Profiles;
|
using NzbDrone.Core.Profiles;
|
||||||
using NzbDrone.Core.Profiles.Delay;
|
using NzbDrone.Core.Profiles.Delay;
|
||||||
|
using NzbDrone.Core.Profiles.Releases;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
using NzbDrone.Core.Restrictions;
|
|
||||||
using NzbDrone.Core.RootFolders;
|
using NzbDrone.Core.RootFolders;
|
||||||
using NzbDrone.Core.Tags;
|
using NzbDrone.Core.Tags;
|
||||||
using NzbDrone.Core.ThingiProvider;
|
using NzbDrone.Core.ThingiProvider;
|
||||||
@ -154,7 +154,7 @@ public static void Map()
|
|||||||
|
|
||||||
Mapper.Entity<RemotePathMapping>("RemotePathMappings").RegisterModel();
|
Mapper.Entity<RemotePathMapping>("RemotePathMappings").RegisterModel();
|
||||||
Mapper.Entity<Tag>("Tags").RegisterModel();
|
Mapper.Entity<Tag>("Tags").RegisterModel();
|
||||||
Mapper.Entity<Restriction>("Restrictions").RegisterModel();
|
Mapper.Entity<ReleaseProfile>("ReleaseProfiles").RegisterModel();
|
||||||
|
|
||||||
Mapper.Entity<DelayProfile>("DelayProfiles").RegisterModel();
|
Mapper.Entity<DelayProfile>("DelayProfiles").RegisterModel();
|
||||||
Mapper.Entity<User>("Users").RegisterModel();
|
Mapper.Entity<User>("Users").RegisterModel();
|
||||||
|
@ -173,8 +173,19 @@ private IEnumerable<DownloadDecision> GetDecisions(List<ReleaseInfo> reports, bo
|
|||||||
|
|
||||||
private DownloadDecision GetDecisionForReport(RemoteMovie remoteMovie, SearchCriteriaBase searchCriteria = null)
|
private DownloadDecision GetDecisionForReport(RemoteMovie remoteMovie, SearchCriteriaBase searchCriteria = null)
|
||||||
{
|
{
|
||||||
var reasons = _specifications.Select(c => EvaluateSpec(c, remoteMovie, searchCriteria))
|
var reasons = Array.Empty<Rejection>();
|
||||||
.Where(c => c != null);
|
|
||||||
|
foreach (var specifications in _specifications.GroupBy(v => v.Priority).OrderBy(v => v.Key))
|
||||||
|
{
|
||||||
|
reasons = specifications.Select(c => EvaluateSpec(c, remoteMovie, searchCriteria))
|
||||||
|
.Where(c => c != null)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
if (reasons.Any())
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new DownloadDecision(remoteMovie, reasons.ToArray());
|
return new DownloadDecision(remoteMovie, reasons.ToArray());
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,24 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.Restrictions;
|
using NzbDrone.Core.Profiles.Releases;
|
||||||
|
|
||||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||||
{
|
{
|
||||||
public class ReleaseRestrictionsSpecification : IDecisionEngineSpecification
|
public class ReleaseRestrictionsSpecification : IDecisionEngineSpecification
|
||||||
{
|
{
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
private readonly IRestrictionService _restrictionService;
|
private readonly IReleaseProfileService _releaseProfileService;
|
||||||
private readonly ITermMatcher _termMatcher;
|
private readonly ITermMatcherService _termMatcherService;
|
||||||
|
|
||||||
public ReleaseRestrictionsSpecification(ITermMatcher termMatcher, IRestrictionService restrictionService, Logger logger)
|
public ReleaseRestrictionsSpecification(ITermMatcherService termMatcherService, IReleaseProfileService releaseProfileService, Logger logger)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_restrictionService = restrictionService;
|
_releaseProfileService = releaseProfileService;
|
||||||
_termMatcher = termMatcher;
|
_termMatcherService = termMatcherService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||||
@ -30,14 +29,14 @@ public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase se
|
|||||||
_logger.Debug("Checking if release meets restrictions: {0}", subject);
|
_logger.Debug("Checking if release meets restrictions: {0}", subject);
|
||||||
|
|
||||||
var title = subject.Release.Title;
|
var title = subject.Release.Title;
|
||||||
var restrictions = _restrictionService.AllForTags(subject.Movie.Tags);
|
var releaseProfiles = _releaseProfileService.EnabledForTags(subject.Movie.Tags, subject.Release.IndexerId);
|
||||||
|
|
||||||
var required = restrictions.Where(r => r.Required.IsNotNullOrWhiteSpace());
|
var required = releaseProfiles.Where(r => r.Required.Any());
|
||||||
var ignored = restrictions.Where(r => r.Ignored.IsNotNullOrWhiteSpace());
|
var ignored = releaseProfiles.Where(r => r.Ignored.Any());
|
||||||
|
|
||||||
foreach (var r in required)
|
foreach (var r in required)
|
||||||
{
|
{
|
||||||
var requiredTerms = r.Required.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
|
var requiredTerms = r.Required;
|
||||||
|
|
||||||
var foundTerms = ContainsAny(requiredTerms, title);
|
var foundTerms = ContainsAny(requiredTerms, title);
|
||||||
if (foundTerms.Empty())
|
if (foundTerms.Empty())
|
||||||
@ -50,7 +49,7 @@ public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase se
|
|||||||
|
|
||||||
foreach (var r in ignored)
|
foreach (var r in ignored)
|
||||||
{
|
{
|
||||||
var ignoredTerms = r.Ignored.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
|
var ignoredTerms = r.Ignored;
|
||||||
|
|
||||||
var foundTerms = ContainsAny(ignoredTerms, title);
|
var foundTerms = ContainsAny(ignoredTerms, title);
|
||||||
if (foundTerms.Any())
|
if (foundTerms.Any())
|
||||||
@ -67,7 +66,7 @@ public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase se
|
|||||||
|
|
||||||
private List<string> ContainsAny(List<string> terms, string title)
|
private List<string> ContainsAny(List<string> terms, string title)
|
||||||
{
|
{
|
||||||
return terms.Where(t => _termMatcher.IsMatch(t, title)).ToList();
|
return terms.Where(t => _termMatcherService.IsMatch(t, title)).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ public CleanupUnusedTags(IMainDatabase database)
|
|||||||
public void Clean()
|
public void Clean()
|
||||||
{
|
{
|
||||||
using var mapper = _database.OpenConnection();
|
using var mapper = _database.OpenConnection();
|
||||||
var usedTags = new[] { "Movies", "Notifications", "DelayProfiles", "Restrictions", "ImportLists", "Indexers", "AutoTagging", "DownloadClients" }
|
var usedTags = new[] { "Movies", "Notifications", "DelayProfiles", "ReleaseProfiles", "ImportLists", "Indexers", "AutoTagging", "DownloadClients" }
|
||||||
.SelectMany(v => GetUsedTags(v, mapper))
|
.SelectMany(v => GetUsedTags(v, mapper))
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToList();
|
.ToList();
|
||||||
|
@ -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
|
||||||
{
|
{
|
32
src/NzbDrone.Core/Profiles/Releases/ReleaseProfile.cs
Normal file
32
src/NzbDrone.Core/Profiles/Releases/ReleaseProfile.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Profiles.Releases
|
||||||
|
{
|
||||||
|
public class ReleaseProfile : ModelBase
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
public List<string> Required { get; set; }
|
||||||
|
public List<string> Ignored { get; set; }
|
||||||
|
public int IndexerId { get; set; }
|
||||||
|
public HashSet<int> Tags { get; set; }
|
||||||
|
|
||||||
|
public ReleaseProfile()
|
||||||
|
{
|
||||||
|
Enabled = true;
|
||||||
|
Required = new List<string>();
|
||||||
|
Ignored = new List<string>();
|
||||||
|
Tags = new HashSet<int>();
|
||||||
|
IndexerId = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ReleaseProfilePreferredComparer : IComparer<KeyValuePair<string, int>>
|
||||||
|
{
|
||||||
|
public int Compare(KeyValuePair<string, int> x, KeyValuePair<string, int> y)
|
||||||
|
{
|
||||||
|
return y.Value.CompareTo(x.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
75
src/NzbDrone.Core/Profiles/Releases/ReleaseProfileService.cs
Normal file
75
src/NzbDrone.Core/Profiles/Releases/ReleaseProfileService.cs
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Profiles.Releases
|
||||||
|
{
|
||||||
|
public interface IReleaseProfileService
|
||||||
|
{
|
||||||
|
List<ReleaseProfile> All();
|
||||||
|
List<ReleaseProfile> AllForTag(int tagId);
|
||||||
|
List<ReleaseProfile> AllForTags(HashSet<int> tagIds);
|
||||||
|
List<ReleaseProfile> EnabledForTags(HashSet<int> tagIds, int indexerId);
|
||||||
|
ReleaseProfile Get(int id);
|
||||||
|
void Delete(int id);
|
||||||
|
ReleaseProfile Add(ReleaseProfile restriction);
|
||||||
|
ReleaseProfile Update(ReleaseProfile restriction);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ReleaseProfileService : IReleaseProfileService
|
||||||
|
{
|
||||||
|
private readonly IReleaseProfileRepository _repo;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
public ReleaseProfileService(IReleaseProfileRepository repo, Logger logger)
|
||||||
|
{
|
||||||
|
_repo = repo;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ReleaseProfile> All()
|
||||||
|
{
|
||||||
|
var all = _repo.All().ToList();
|
||||||
|
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ReleaseProfile> AllForTag(int tagId)
|
||||||
|
{
|
||||||
|
return _repo.All().Where(r => r.Tags.Contains(tagId)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ReleaseProfile> AllForTags(HashSet<int> tagIds)
|
||||||
|
{
|
||||||
|
return _repo.All().Where(r => r.Tags.Intersect(tagIds).Any() || r.Tags.Empty()).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ReleaseProfile> EnabledForTags(HashSet<int> tagIds, int indexerId)
|
||||||
|
{
|
||||||
|
return AllForTags(tagIds)
|
||||||
|
.Where(r => r.Enabled)
|
||||||
|
.Where(r => r.IndexerId == indexerId || r.IndexerId == 0).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReleaseProfile Get(int id)
|
||||||
|
{
|
||||||
|
return _repo.Get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Delete(int id)
|
||||||
|
{
|
||||||
|
_repo.Delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReleaseProfile Add(ReleaseProfile restriction)
|
||||||
|
{
|
||||||
|
return _repo.Insert(restriction);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReleaseProfile Update(ReleaseProfile restriction)
|
||||||
|
{
|
||||||
|
return _repo.Update(restriction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
49
src/NzbDrone.Core/Profiles/Releases/TermMatcherService.cs
Normal file
49
src/NzbDrone.Core/Profiles/Releases/TermMatcherService.cs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using NzbDrone.Common.Cache;
|
||||||
|
using NzbDrone.Core.Profiles.Releases.TermMatchers;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Profiles.Releases
|
||||||
|
{
|
||||||
|
public interface ITermMatcherService
|
||||||
|
{
|
||||||
|
bool IsMatch(string term, string value);
|
||||||
|
string MatchingTerm(string term, string value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TermMatcherService : ITermMatcherService
|
||||||
|
{
|
||||||
|
private ICached<ITermMatcher> _matcherCache;
|
||||||
|
|
||||||
|
public TermMatcherService(ICacheManager cacheManager)
|
||||||
|
{
|
||||||
|
_matcherCache = cacheManager.GetCache<ITermMatcher>(GetType());
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsMatch(string term, string value)
|
||||||
|
{
|
||||||
|
return GetMatcher(term).IsMatch(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string MatchingTerm(string term, string value)
|
||||||
|
{
|
||||||
|
return GetMatcher(term).MatchingTerm(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ITermMatcher GetMatcher(string term)
|
||||||
|
{
|
||||||
|
return _matcherCache.Get(term, () => CreateMatcherInternal(term), TimeSpan.FromHours(24));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ITermMatcher CreateMatcherInternal(string term)
|
||||||
|
{
|
||||||
|
if (PerlRegexFactory.TryCreateRegex(term, out var regex))
|
||||||
|
{
|
||||||
|
return new RegexTermMatcher(regex);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new CaseInsensitiveTermMatcher(term);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 @@ public class TagDetails : ModelBase
|
|||||||
public string Label { get; set; }
|
public string Label { get; set; }
|
||||||
public List<int> MovieIds { get; set; }
|
public List<int> MovieIds { get; set; }
|
||||||
public List<int> NotificationIds { get; set; }
|
public List<int> NotificationIds { get; set; }
|
||||||
public List<int> RestrictionIds { get; set; }
|
public List<int> ReleaseProfileIds { get; set; }
|
||||||
public List<int> DelayProfileIds { get; set; }
|
public List<int> DelayProfileIds { get; set; }
|
||||||
public List<int> ImportListIds { get; set; }
|
public List<int> ImportListIds { get; set; }
|
||||||
public List<int> IndexerIds { get; set; }
|
public List<int> IndexerIds { get; set; }
|
||||||
@ -18,7 +18,7 @@ public class TagDetails : ModelBase
|
|||||||
|
|
||||||
public bool InUse => MovieIds.Any() ||
|
public bool InUse => MovieIds.Any() ||
|
||||||
NotificationIds.Any() ||
|
NotificationIds.Any() ||
|
||||||
RestrictionIds.Any() ||
|
ReleaseProfileIds.Any() ||
|
||||||
DelayProfileIds.Any() ||
|
DelayProfileIds.Any() ||
|
||||||
ImportListIds.Any() ||
|
ImportListIds.Any() ||
|
||||||
IndexerIds.Any() ||
|
IndexerIds.Any() ||
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
using NzbDrone.Core.Movies;
|
using NzbDrone.Core.Movies;
|
||||||
using NzbDrone.Core.Notifications;
|
using NzbDrone.Core.Notifications;
|
||||||
using NzbDrone.Core.Profiles.Delay;
|
using NzbDrone.Core.Profiles.Delay;
|
||||||
using NzbDrone.Core.Restrictions;
|
using NzbDrone.Core.Profiles.Releases;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Tags
|
namespace NzbDrone.Core.Tags
|
||||||
{
|
{
|
||||||
@ -33,7 +33,7 @@ public class TagService : ITagService
|
|||||||
private readonly IDelayProfileService _delayProfileService;
|
private readonly IDelayProfileService _delayProfileService;
|
||||||
private readonly IImportListFactory _importListFactory;
|
private readonly IImportListFactory _importListFactory;
|
||||||
private readonly INotificationFactory _notificationFactory;
|
private readonly INotificationFactory _notificationFactory;
|
||||||
private readonly IRestrictionService _restrictionService;
|
private readonly IReleaseProfileService _releaseProfileService;
|
||||||
private readonly IMovieService _movieService;
|
private readonly IMovieService _movieService;
|
||||||
private readonly IIndexerFactory _indexerService;
|
private readonly IIndexerFactory _indexerService;
|
||||||
private readonly IAutoTaggingService _autoTaggingService;
|
private readonly IAutoTaggingService _autoTaggingService;
|
||||||
@ -44,7 +44,7 @@ public TagService(ITagRepository repo,
|
|||||||
IDelayProfileService delayProfileService,
|
IDelayProfileService delayProfileService,
|
||||||
IImportListFactory importListFactory,
|
IImportListFactory importListFactory,
|
||||||
INotificationFactory notificationFactory,
|
INotificationFactory notificationFactory,
|
||||||
IRestrictionService restrictionService,
|
IReleaseProfileService releaseProfileService,
|
||||||
IMovieService movieService,
|
IMovieService movieService,
|
||||||
IIndexerFactory indexerService,
|
IIndexerFactory indexerService,
|
||||||
IAutoTaggingService autoTaggingService,
|
IAutoTaggingService autoTaggingService,
|
||||||
@ -55,7 +55,7 @@ public TagService(ITagRepository repo,
|
|||||||
_delayProfileService = delayProfileService;
|
_delayProfileService = delayProfileService;
|
||||||
_importListFactory = importListFactory;
|
_importListFactory = importListFactory;
|
||||||
_notificationFactory = notificationFactory;
|
_notificationFactory = notificationFactory;
|
||||||
_restrictionService = restrictionService;
|
_releaseProfileService = releaseProfileService;
|
||||||
_movieService = movieService;
|
_movieService = movieService;
|
||||||
_indexerService = indexerService;
|
_indexerService = indexerService;
|
||||||
_autoTaggingService = autoTaggingService;
|
_autoTaggingService = autoTaggingService;
|
||||||
@ -90,7 +90,7 @@ public TagDetails Details(int tagId)
|
|||||||
var delayProfiles = _delayProfileService.AllForTag(tagId);
|
var delayProfiles = _delayProfileService.AllForTag(tagId);
|
||||||
var importLists = _importListFactory.AllForTag(tagId);
|
var importLists = _importListFactory.AllForTag(tagId);
|
||||||
var notifications = _notificationFactory.AllForTag(tagId);
|
var notifications = _notificationFactory.AllForTag(tagId);
|
||||||
var restrictions = _restrictionService.AllForTag(tagId);
|
var releaseProfiles = _releaseProfileService.AllForTag(tagId);
|
||||||
var movies = _movieService.AllMovieTags().Where(x => x.Value.Contains(tagId)).Select(x => x.Key).ToList();
|
var movies = _movieService.AllMovieTags().Where(x => x.Value.Contains(tagId)).Select(x => x.Key).ToList();
|
||||||
var indexers = _indexerService.AllForTag(tagId);
|
var indexers = _indexerService.AllForTag(tagId);
|
||||||
var autoTags = _autoTaggingService.AllForTag(tagId);
|
var autoTags = _autoTaggingService.AllForTag(tagId);
|
||||||
@ -103,7 +103,7 @@ public TagDetails Details(int tagId)
|
|||||||
DelayProfileIds = delayProfiles.Select(c => c.Id).ToList(),
|
DelayProfileIds = delayProfiles.Select(c => c.Id).ToList(),
|
||||||
ImportListIds = importLists.Select(c => c.Id).ToList(),
|
ImportListIds = importLists.Select(c => c.Id).ToList(),
|
||||||
NotificationIds = notifications.Select(c => c.Id).ToList(),
|
NotificationIds = notifications.Select(c => c.Id).ToList(),
|
||||||
RestrictionIds = restrictions.Select(c => c.Id).ToList(),
|
ReleaseProfileIds = releaseProfiles.Select(c => c.Id).ToList(),
|
||||||
MovieIds = movies,
|
MovieIds = movies,
|
||||||
IndexerIds = indexers.Select(c => c.Id).ToList(),
|
IndexerIds = indexers.Select(c => c.Id).ToList(),
|
||||||
AutoTagIds = autoTags.Select(c => c.Id).ToList(),
|
AutoTagIds = autoTags.Select(c => c.Id).ToList(),
|
||||||
@ -117,7 +117,7 @@ public List<TagDetails> Details()
|
|||||||
var delayProfiles = _delayProfileService.All();
|
var delayProfiles = _delayProfileService.All();
|
||||||
var importLists = _importListFactory.All();
|
var importLists = _importListFactory.All();
|
||||||
var notifications = _notificationFactory.All();
|
var notifications = _notificationFactory.All();
|
||||||
var restrictions = _restrictionService.All();
|
var releaseProfiles = _releaseProfileService.All();
|
||||||
var movies = _movieService.AllMovieTags();
|
var movies = _movieService.AllMovieTags();
|
||||||
var indexers = _indexerService.All();
|
var indexers = _indexerService.All();
|
||||||
var autotags = _autoTaggingService.All();
|
var autotags = _autoTaggingService.All();
|
||||||
@ -134,7 +134,7 @@ public List<TagDetails> Details()
|
|||||||
DelayProfileIds = delayProfiles.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
DelayProfileIds = delayProfiles.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
||||||
ImportListIds = importLists.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
ImportListIds = importLists.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
||||||
NotificationIds = notifications.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
NotificationIds = notifications.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
||||||
RestrictionIds = restrictions.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
ReleaseProfileIds = releaseProfiles.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
||||||
MovieIds = movies.Where(c => c.Value.Contains(tag.Id)).Select(c => c.Key).ToList(),
|
MovieIds = movies.Where(c => c.Value.Contains(tag.Id)).Select(c => c.Key).ToList(),
|
||||||
IndexerIds = indexers.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
IndexerIds = indexers.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
||||||
AutoTagIds = autotags.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
AutoTagIds = autotags.Where(c => c.Tags.Contains(tag.Id)).Select(c => c.Id).ToList(),
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
108
src/Radarr.Api.V3/Profiles/Release/ReleaseProfileResource.cs
Normal file
108
src/Radarr.Api.V3/Profiles/Release/ReleaseProfileResource.cs
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using NzbDrone.Core.Profiles.Releases;
|
||||||
|
using Radarr.Http.REST;
|
||||||
|
|
||||||
|
namespace Radarr.Api.V3.Profiles.Release
|
||||||
|
{
|
||||||
|
public class ReleaseProfileResource : RestResource
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
|
||||||
|
// Is List<string>, string or JArray, we accept 'string' with POST and PUT for backwards compatibility
|
||||||
|
public object Required { get; set; }
|
||||||
|
public object Ignored { get; set; }
|
||||||
|
public int IndexerId { get; set; }
|
||||||
|
public HashSet<int> Tags { get; set; }
|
||||||
|
|
||||||
|
public ReleaseProfileResource()
|
||||||
|
{
|
||||||
|
Tags = new HashSet<int>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class RestrictionResourceMapper
|
||||||
|
{
|
||||||
|
public static ReleaseProfileResource ToResource(this ReleaseProfile model)
|
||||||
|
{
|
||||||
|
if (model == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ReleaseProfileResource
|
||||||
|
{
|
||||||
|
Id = model.Id,
|
||||||
|
Name = model.Name,
|
||||||
|
Enabled = model.Enabled,
|
||||||
|
Required = model.Required ?? new List<string>(),
|
||||||
|
Ignored = model.Ignored ?? new List<string>(),
|
||||||
|
IndexerId = model.IndexerId,
|
||||||
|
Tags = new HashSet<int>(model.Tags)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ReleaseProfile ToModel(this ReleaseProfileResource resource)
|
||||||
|
{
|
||||||
|
if (resource == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ReleaseProfile
|
||||||
|
{
|
||||||
|
Id = resource.Id,
|
||||||
|
Name = resource.Name,
|
||||||
|
Enabled = resource.Enabled,
|
||||||
|
Required = resource.MapRequired(),
|
||||||
|
Ignored = resource.MapIgnored(),
|
||||||
|
IndexerId = resource.IndexerId,
|
||||||
|
Tags = new HashSet<int>(resource.Tags)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ReleaseProfileResource> ToResource(this IEnumerable<ReleaseProfile> models)
|
||||||
|
{
|
||||||
|
return models.Select(ToResource).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<string> MapRequired(this ReleaseProfileResource profile) => ParseArray(profile.Required, "required");
|
||||||
|
public static List<string> MapIgnored(this ReleaseProfileResource profile) => ParseArray(profile.Ignored, "ignored");
|
||||||
|
|
||||||
|
private static List<string> ParseArray(object resource, string title)
|
||||||
|
{
|
||||||
|
if (resource == null)
|
||||||
|
{
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resource is List<string> list)
|
||||||
|
{
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resource is JsonElement array)
|
||||||
|
{
|
||||||
|
if (array.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return array.GetString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<List<string>>(array);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resource is string str)
|
||||||
|
{
|
||||||
|
return str.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestException($"Invalid field {title}, should be string or string array");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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 @@ public class TagDetailsResource : RestResource
|
|||||||
public List<int> DelayProfileIds { get; set; }
|
public List<int> DelayProfileIds { get; set; }
|
||||||
public List<int> ImportListIds { get; set; }
|
public List<int> ImportListIds { get; set; }
|
||||||
public List<int> NotificationIds { get; set; }
|
public List<int> NotificationIds { get; set; }
|
||||||
public List<int> RestrictionIds { get; set; }
|
public List<int> ReleaseProfileIds { get; set; }
|
||||||
public List<int> IndexerIds { get; set; }
|
public List<int> IndexerIds { get; set; }
|
||||||
public List<int> DownloadClientIds { get; set; }
|
public List<int> DownloadClientIds { get; set; }
|
||||||
public List<int> AutoTagIds { get; set; }
|
public List<int> AutoTagIds { get; set; }
|
||||||
@ -34,7 +34,7 @@ public static TagDetailsResource ToResource(this TagDetails model)
|
|||||||
DelayProfileIds = model.DelayProfileIds,
|
DelayProfileIds = model.DelayProfileIds,
|
||||||
ImportListIds = model.ImportListIds,
|
ImportListIds = model.ImportListIds,
|
||||||
NotificationIds = model.NotificationIds,
|
NotificationIds = model.NotificationIds,
|
||||||
RestrictionIds = model.RestrictionIds,
|
ReleaseProfileIds = model.ReleaseProfileIds,
|
||||||
IndexerIds = model.IndexerIds,
|
IndexerIds = model.IndexerIds,
|
||||||
DownloadClientIds = model.DownloadClientIds,
|
DownloadClientIds = model.DownloadClientIds,
|
||||||
AutoTagIds = model.AutoTagIds,
|
AutoTagIds = model.AutoTagIds,
|
||||||
|
Loading…
Reference in New Issue
Block a user