parent
ca93a72d63
commit
99ff6aa9c4
@ -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);
|
@ -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;
|
@ -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%;
|
||||
}
|
||||
|
@ -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;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.addRestriction {
|
||||
composes: restriction from '~./Restriction.css';
|
||||
.addReleaseProfile {
|
||||
composes: releaseProfile from '~./ReleaseProfile.css';
|
||||
|
||||
background-color: var(--cardAlternateBackgroundColor);
|
||||
color: var(--gray);
|
@ -1,9 +1,9 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'addRestriction': string;
|
||||
'addReleaseProfile': string;
|
||||
'center': string;
|
||||
'restrictions': string;
|
||||
'releaseProfiles': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
@ -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);
|
@ -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)
|
||||
}
|
||||
|
||||
};
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Profiles.Releases
|
||||
{
|
||||
public class ReleaseProfile : ModelBase
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public List<string> Required { get; set; }
|
||||
public List<string> Ignored { get; set; }
|
||||
public int IndexerId { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
|
||||
public ReleaseProfile()
|
||||
{
|
||||
Enabled = true;
|
||||
Required = new List<string>();
|
||||
Ignored = new List<string>();
|
||||
Tags = new HashSet<int>();
|
||||
IndexerId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public class ReleaseProfilePreferredComparer : IComparer<KeyValuePair<string, int>>
|
||||
{
|
||||
public int Compare(KeyValuePair<string, int> x, KeyValuePair<string, int> y)
|
||||
{
|
||||
return y.Value.CompareTo(x.Value);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Profiles.Releases
|
||||
{
|
||||
public interface IReleaseProfileRepository : IBasicRepository<ReleaseProfile>
|
||||
{
|
||||
}
|
||||
|
||||
public class ReleaseProfileRepository : BasicRepository<ReleaseProfile>, IReleaseProfileRepository
|
||||
{
|
||||
public ReleaseProfileRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace NzbDrone.Core.Profiles.Releases
|
||||
{
|
||||
public interface IReleaseProfileService
|
||||
{
|
||||
List<ReleaseProfile> All();
|
||||
List<ReleaseProfile> AllForTag(int tagId);
|
||||
List<ReleaseProfile> AllForTags(HashSet<int> tagIds);
|
||||
List<ReleaseProfile> EnabledForTags(HashSet<int> tagIds, int indexerId);
|
||||
ReleaseProfile Get(int id);
|
||||
void Delete(int id);
|
||||
ReleaseProfile Add(ReleaseProfile restriction);
|
||||
ReleaseProfile Update(ReleaseProfile restriction);
|
||||
}
|
||||
|
||||
public class ReleaseProfileService : IReleaseProfileService
|
||||
{
|
||||
private readonly IReleaseProfileRepository _repo;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ReleaseProfileService(IReleaseProfileRepository repo, Logger logger)
|
||||
{
|
||||
_repo = repo;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public List<ReleaseProfile> All()
|
||||
{
|
||||
var all = _repo.All().ToList();
|
||||
|
||||
return all;
|
||||
}
|
||||
|
||||
public List<ReleaseProfile> AllForTag(int tagId)
|
||||
{
|
||||
return _repo.All().Where(r => r.Tags.Contains(tagId)).ToList();
|
||||
}
|
||||
|
||||
public List<ReleaseProfile> AllForTags(HashSet<int> tagIds)
|
||||
{
|
||||
return _repo.All().Where(r => r.Tags.Intersect(tagIds).Any() || r.Tags.Empty()).ToList();
|
||||
}
|
||||
|
||||
public List<ReleaseProfile> EnabledForTags(HashSet<int> tagIds, int indexerId)
|
||||
{
|
||||
return AllForTags(tagIds)
|
||||
.Where(r => r.Enabled)
|
||||
.Where(r => r.IndexerId == indexerId || r.IndexerId == 0).ToList();
|
||||
}
|
||||
|
||||
public ReleaseProfile Get(int id)
|
||||
{
|
||||
return _repo.Get(id);
|
||||
}
|
||||
|
||||
public void Delete(int id)
|
||||
{
|
||||
_repo.Delete(id);
|
||||
}
|
||||
|
||||
public ReleaseProfile Add(ReleaseProfile restriction)
|
||||
{
|
||||
return _repo.Insert(restriction);
|
||||
}
|
||||
|
||||
public ReleaseProfile Update(ReleaseProfile restriction)
|
||||
{
|
||||
return _repo.Update(restriction);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Core.Profiles.Releases.TermMatchers;
|
||||
|
||||
namespace NzbDrone.Core.Profiles.Releases
|
||||
{
|
||||
public interface ITermMatcherService
|
||||
{
|
||||
bool IsMatch(string term, string value);
|
||||
string MatchingTerm(string term, string value);
|
||||
}
|
||||
|
||||
public class TermMatcherService : ITermMatcherService
|
||||
{
|
||||
private ICached<ITermMatcher> _matcherCache;
|
||||
|
||||
public TermMatcherService(ICacheManager cacheManager)
|
||||
{
|
||||
_matcherCache = cacheManager.GetCache<ITermMatcher>(GetType());
|
||||
}
|
||||
|
||||
public bool IsMatch(string term, string value)
|
||||
{
|
||||
return GetMatcher(term).IsMatch(value);
|
||||
}
|
||||
|
||||
public string MatchingTerm(string term, string value)
|
||||
{
|
||||
return GetMatcher(term).MatchingTerm(value);
|
||||
}
|
||||
|
||||
public ITermMatcher GetMatcher(string term)
|
||||
{
|
||||
return _matcherCache.Get(term, () => CreateMatcherInternal(term), TimeSpan.FromHours(24));
|
||||
}
|
||||
|
||||
private ITermMatcher CreateMatcherInternal(string term)
|
||||
{
|
||||
if (PerlRegexFactory.TryCreateRegex(term, out var regex))
|
||||
{
|
||||
return new RegexTermMatcher(regex);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new CaseInsensitiveTermMatcher(term);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Profiles.Releases;
|
||||
using Radarr.Http;
|
||||
using Radarr.Http.REST;
|
||||
using Radarr.Http.REST.Attributes;
|
||||
|
||||
namespace Radarr.Api.V3.Profiles.Release
|
||||
{
|
||||
[V3ApiController]
|
||||
public class ReleaseProfileController : RestController<ReleaseProfileResource>
|
||||
{
|
||||
private readonly IReleaseProfileService _profileService;
|
||||
private readonly IIndexerFactory _indexerFactory;
|
||||
|
||||
public ReleaseProfileController(IReleaseProfileService profileService, IIndexerFactory indexerFactory)
|
||||
{
|
||||
_profileService = profileService;
|
||||
_indexerFactory = indexerFactory;
|
||||
|
||||
SharedValidator.RuleFor(d => d).Custom((restriction, context) =>
|
||||
{
|
||||
if (restriction.MapIgnored().Empty() && restriction.MapRequired().Empty())
|
||||
{
|
||||
context.AddFailure("'Must contain' or 'Must not contain' is required");
|
||||
}
|
||||
|
||||
if (restriction.Enabled && restriction.IndexerId != 0 && !_indexerFactory.Exists(restriction.IndexerId))
|
||||
{
|
||||
context.AddFailure(nameof(ReleaseProfile.IndexerId), "Indexer does not exist");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[RestPostById]
|
||||
public ActionResult<ReleaseProfileResource> Create(ReleaseProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
model = _profileService.Add(model);
|
||||
return Created(model.Id);
|
||||
}
|
||||
|
||||
[RestDeleteById]
|
||||
public void DeleteProfile(int id)
|
||||
{
|
||||
_profileService.Delete(id);
|
||||
}
|
||||
|
||||
[RestPutById]
|
||||
public ActionResult<ReleaseProfileResource> Update(ReleaseProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
|
||||
_profileService.Update(model);
|
||||
|
||||
return Accepted(model.Id);
|
||||
}
|
||||
|
||||
protected override ReleaseProfileResource GetResourceById(int id)
|
||||
{
|
||||
return _profileService.Get(id).ToResource();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public List<ReleaseProfileResource> GetAll()
|
||||
{
|
||||
return _profileService.All().ToResource();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using NzbDrone.Core.Profiles.Releases;
|
||||
using Radarr.Http.REST;
|
||||
|
||||
namespace Radarr.Api.V3.Profiles.Release
|
||||
{
|
||||
public class ReleaseProfileResource : RestResource
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
// Is List<string>, string or JArray, we accept 'string' with POST and PUT for backwards compatibility
|
||||
public object Required { get; set; }
|
||||
public object Ignored { get; set; }
|
||||
public int IndexerId { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
|
||||
public ReleaseProfileResource()
|
||||
{
|
||||
Tags = new HashSet<int>();
|
||||
}
|
||||
}
|
||||
|
||||
public static class RestrictionResourceMapper
|
||||
{
|
||||
public static ReleaseProfileResource ToResource(this ReleaseProfile model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ReleaseProfileResource
|
||||
{
|
||||
Id = model.Id,
|
||||
Name = model.Name,
|
||||
Enabled = model.Enabled,
|
||||
Required = model.Required ?? new List<string>(),
|
||||
Ignored = model.Ignored ?? new List<string>(),
|
||||
IndexerId = model.IndexerId,
|
||||
Tags = new HashSet<int>(model.Tags)
|
||||
};
|
||||
}
|
||||
|
||||
public static ReleaseProfile ToModel(this ReleaseProfileResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ReleaseProfile
|
||||
{
|
||||
Id = resource.Id,
|
||||
Name = resource.Name,
|
||||
Enabled = resource.Enabled,
|
||||
Required = resource.MapRequired(),
|
||||
Ignored = resource.MapIgnored(),
|
||||
IndexerId = resource.IndexerId,
|
||||
Tags = new HashSet<int>(resource.Tags)
|
||||
};
|
||||
}
|
||||
|
||||
public static List<ReleaseProfileResource> ToResource(this IEnumerable<ReleaseProfile> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
|
||||
public static List<string> MapRequired(this ReleaseProfileResource profile) => ParseArray(profile.Required, "required");
|
||||
public static List<string> MapIgnored(this ReleaseProfileResource profile) => ParseArray(profile.Ignored, "ignored");
|
||||
|
||||
private static List<string> ParseArray(object resource, string title)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
if (resource is List<string> list)
|
||||
{
|
||||
return list;
|
||||
}
|
||||
|
||||
if (resource is JsonElement array)
|
||||
{
|
||||
if (array.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return array.GetString().Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
}
|
||||
|
||||
if (array.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
return JsonSerializer.Deserialize<List<string>>(array);
|
||||
}
|
||||
}
|
||||
|
||||
if (resource is string str)
|
||||
{
|
||||
return str.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
}
|
||||
|
||||
throw new BadRequestException($"Invalid field {title}, should be string or string array");
|
||||
}
|
||||
}
|
||||
}
|
@ -1,60 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Restrictions;
|
||||
using Radarr.Http;
|
||||
using Radarr.Http.REST;
|
||||
using Radarr.Http.REST.Attributes;
|
||||
|
||||
namespace Radarr.Api.V3.Restrictions
|
||||
{
|
||||
[V3ApiController]
|
||||
public class RestrictionController : RestController<RestrictionResource>
|
||||
{
|
||||
private readonly IRestrictionService _restrictionService;
|
||||
|
||||
public RestrictionController(IRestrictionService restrictionService)
|
||||
{
|
||||
_restrictionService = restrictionService;
|
||||
|
||||
SharedValidator.RuleFor(d => d).Custom((restriction, context) =>
|
||||
{
|
||||
if (restriction.Ignored.IsNullOrWhiteSpace() && restriction.Required.IsNullOrWhiteSpace())
|
||||
{
|
||||
context.AddFailure("Either 'Must contain' or 'Must not contain' is required");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override RestrictionResource GetResourceById(int id)
|
||||
{
|
||||
return _restrictionService.Get(id).ToResource();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public List<RestrictionResource> GetAll()
|
||||
{
|
||||
return _restrictionService.All().ToResource();
|
||||
}
|
||||
|
||||
[RestPostById]
|
||||
public ActionResult<RestrictionResource> Create(RestrictionResource resource)
|
||||
{
|
||||
return Created(_restrictionService.Add(resource.ToModel()).Id);
|
||||
}
|
||||
|
||||
[RestPutById]
|
||||
public ActionResult<RestrictionResource> Update(RestrictionResource resource)
|
||||
{
|
||||
_restrictionService.Update(resource.ToModel());
|
||||
return Accepted(resource.Id);
|
||||
}
|
||||
|
||||
[RestDeleteById]
|
||||
public void DeleteRestriction(int id)
|
||||
{
|
||||
_restrictionService.Delete(id);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.Restrictions;
|
||||
using Radarr.Http.REST;
|
||||
|
||||
namespace Radarr.Api.V3.Restrictions
|
||||
{
|
||||
public class RestrictionResource : RestResource
|
||||
{
|
||||
public string Required { get; set; }
|
||||
public string Preferred { get; set; }
|
||||
public string Ignored { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
|
||||
public RestrictionResource()
|
||||
{
|
||||
Tags = new HashSet<int>();
|
||||
}
|
||||
}
|
||||
|
||||
public static class RestrictionResourceMapper
|
||||
{
|
||||
public static RestrictionResource ToResource(this Restriction model)
|
||||
{
|
||||
if (model == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new RestrictionResource
|
||||
{
|
||||
Id = model.Id,
|
||||
|
||||
Required = model.Required,
|
||||
Preferred = model.Preferred,
|
||||
Ignored = model.Ignored,
|
||||
Tags = new HashSet<int>(model.Tags)
|
||||
};
|
||||
}
|
||||
|
||||
public static Restriction ToModel(this RestrictionResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Restriction
|
||||
{
|
||||
Id = resource.Id,
|
||||
|
||||
Required = resource.Required,
|
||||
Preferred = resource.Preferred,
|
||||
Ignored = resource.Ignored,
|
||||
Tags = new HashSet<int>(resource.Tags)
|
||||
};
|
||||
}
|
||||
|
||||
public static List<RestrictionResource> ToResource(this IEnumerable<Restriction> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue