mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-04-24 13:57:11 -04:00
Fixed: Quality Groups and Profiles
This commit is contained in:
parent
6275737ced
commit
16ff1176f7
55 changed files with 1229 additions and 216 deletions
|
@ -17,7 +17,7 @@ import Settings from 'Settings/Settings';
|
|||
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
|
||||
import Profiles from 'Settings/Profiles/Profiles';
|
||||
import Quality from 'Settings/Quality/Quality';
|
||||
import CustomFormatsConnector from 'Settings/CustomFormats/CustomFormatsConnector';
|
||||
import CustomFormatSettingsConnector from 'Settings/CustomFormats/CustomFormatSettingsConnector';
|
||||
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
|
||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||
import NetImportSettingsConnector from 'Settings/NetImport/NetImportSettingsConnector';
|
||||
|
@ -142,7 +142,7 @@ function AppRoutes(props) {
|
|||
|
||||
<Route
|
||||
path="/settings/customformats"
|
||||
component={CustomFormatsConnector}
|
||||
component={CustomFormatSettingsConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import React, { Component } from 'react';
|
||||
import { DragDropContext } from 'react-dnd';
|
||||
import HTML5Backend from 'react-dnd-html5-backend';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
|
||||
class CustomFormatSettingsConnector extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PageContent title="Custom Formats Settings">
|
||||
<SettingsToolbarConnector
|
||||
showSave={false}
|
||||
/>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<CustomFormatsConnector />
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DragDropContext(HTML5Backend)(CustomFormatSettingsConnector);
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
.customFormat {
|
||||
composes: card from '~Components/Card.css';
|
||||
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.nameContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.name {
|
||||
@add-mixin truncate;
|
||||
|
||||
margin-bottom: 20px;
|
||||
font-weight: 300;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.cloneButton {
|
||||
composes: button from '~Components/Link/IconButton.css';
|
||||
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.formats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 5px;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.tooltipLabel {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
// import EditCustomFormatModalConnector from './EditCustomFormatModalConnector';
|
||||
import styles from './CustomFormat.css';
|
||||
|
||||
class CustomFormat extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isEditCustomFormatModalOpen: false,
|
||||
isDeleteCustomFormatModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditCustomFormatPress = () => {
|
||||
this.setState({ isEditCustomFormatModalOpen: true });
|
||||
}
|
||||
|
||||
onEditCustomFormatModalClose = () => {
|
||||
this.setState({ isEditCustomFormatModalOpen: false });
|
||||
}
|
||||
|
||||
onDeleteCustomFormatPress = () => {
|
||||
this.setState({
|
||||
isEditCustomFormatModalOpen: false,
|
||||
isDeleteCustomFormatModalOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
onDeleteCustomFormatModalClose = () => {
|
||||
this.setState({ isDeleteCustomFormatModalOpen: false });
|
||||
}
|
||||
|
||||
onConfirmDeleteCustomFormat = () => {
|
||||
this.props.onConfirmDeleteCustomFormat(this.props.id);
|
||||
}
|
||||
|
||||
onCloneCustomFormatPress = () => {
|
||||
const {
|
||||
id,
|
||||
onCloneCustomFormatPress
|
||||
} = this.props;
|
||||
|
||||
onCloneCustomFormatPress(id);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
// id,
|
||||
name,
|
||||
items,
|
||||
isDeleting
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.CustomFormat}
|
||||
overlayContent={true}
|
||||
onPress={this.onEditCustomFormatPress}
|
||||
>
|
||||
<div className={styles.nameContainer}>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
|
||||
<IconButton
|
||||
className={styles.cloneButton}
|
||||
title="Clone Profile"
|
||||
name={icons.CLONE}
|
||||
onPress={this.onCloneCustomFormatPress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formats}>
|
||||
{
|
||||
items.map((item) => {
|
||||
if (!item.allowed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Label
|
||||
key={item.quality.id}
|
||||
kind={kinds.default}
|
||||
title={null}
|
||||
>
|
||||
{item.quality.name}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* <EditCustomFormatModalConnector
|
||||
id={id}
|
||||
isOpen={this.state.isEditCustomFormatModalOpen}
|
||||
onModalClose={this.onEditCustomFormatModalClose}
|
||||
onDeleteCustomFormatPress={this.onDeleteCustomFormatPress}
|
||||
/> */}
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isDeleteCustomFormatModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title="Delete Custom Format"
|
||||
message={`Are you sure you want to delete the custom format '${name}'?`}
|
||||
confirmLabel="Delete"
|
||||
isSpinning={isDeleting}
|
||||
onConfirm={this.onConfirmDeleteCustomFormat}
|
||||
onCancel={this.onDeleteCustomFormatModalClose}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CustomFormat.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
onConfirmDeleteCustomFormat: PropTypes.func.isRequired,
|
||||
onCloneCustomFormatPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CustomFormat;
|
|
@ -0,0 +1,21 @@
|
|||
.customFormats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.addCustomFormat {
|
||||
composes: customFormat from '~./CustomFormat.css';
|
||||
|
||||
background-color: $cardAlternateBackgroundColor;
|
||||
color: $gray;
|
||||
text-align: center;
|
||||
font-size: 45px;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: inline-block;
|
||||
padding: 5px 20px 0;
|
||||
border: 1px solid $borderColor;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Card from 'Components/Card';
|
||||
import Icon from 'Components/Icon';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import CustomFormat from './CustomFormat';
|
||||
// import EditCustomFormatModalConnector from './EditCustomFormatModalConnector';
|
||||
import styles from './CustomFormats.css';
|
||||
|
||||
class CustomFormats extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isCustomFormatModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onCloneCustomFormatPress = (id) => {
|
||||
this.props.onCloneCustomFormatPress(id);
|
||||
this.setState({ isCustomFormatModalOpen: true });
|
||||
}
|
||||
|
||||
onEditCustomFormatPress = () => {
|
||||
this.setState({ isCustomFormatModalOpen: true });
|
||||
}
|
||||
|
||||
onModalClose = () => {
|
||||
this.setState({ isCustomFormatModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
isDeleting,
|
||||
onConfirmDeleteCustomFormat,
|
||||
onCloneCustomFormatPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<FieldSet legend="Custom Formats">
|
||||
<PageSectionContent
|
||||
errorMessage="Unable to load Custom Formats"
|
||||
{...otherProps}c={true}
|
||||
>
|
||||
<div className={styles.CustomFormats}>
|
||||
{
|
||||
items.sort(sortByName).map((item) => {
|
||||
return (
|
||||
<CustomFormat
|
||||
key={item.id}
|
||||
{...item}
|
||||
isDeleting={isDeleting}
|
||||
onConfirmDeleteCustomFormat={onConfirmDeleteCustomFormat}
|
||||
onCloneCustomFormatPress={this.onCloneCustomFormatPress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<Card
|
||||
className={styles.addCustomFormat}
|
||||
onPress={this.onEditCustomFormatPress}
|
||||
>
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.ADD}
|
||||
size={45}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/*
|
||||
<EditCustomFormatModalConnector
|
||||
isOpen={this.state.isCustomFormatModalOpen}
|
||||
onModalClose={this.onModalClose}
|
||||
/> */}
|
||||
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CustomFormats.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onConfirmDeleteCustomFormat: PropTypes.func.isRequired,
|
||||
onCloneCustomFormatPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CustomFormats;
|
|
@ -0,0 +1,65 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchCustomFormats, deleteCustomFormat, cloneCustomFormat } from 'Store/Actions/settingsActions';
|
||||
import CustomFormats from './CustomFormats';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.customFormats,
|
||||
(customFormats) => {
|
||||
return {
|
||||
...customFormats
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchCustomFormats: fetchCustomFormats,
|
||||
dispatchDeleteCustomFormat: deleteCustomFormat,
|
||||
dispatchCloneCustomFormat: cloneCustomFormat
|
||||
};
|
||||
|
||||
class CustomFormatsConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchCustomFormats();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onConfirmDeleteCustomFormat = (id) => {
|
||||
this.props.dispatchDeleteCustomFormat({ id });
|
||||
}
|
||||
|
||||
onCloneCustomFormatPress = (id) => {
|
||||
this.props.dispatchCloneCustomFormat({ id });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CustomFormats
|
||||
onConfirmDeleteCustomFormat={this.onConfirmDeleteCustomFormat}
|
||||
onCloneCustomFormatPress={this.onCloneCustomFormatPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CustomFormatsConnector.propTypes = {
|
||||
dispatchFetchCustomFormats: PropTypes.func.isRequired,
|
||||
dispatchDeleteCustomFormat: PropTypes.func.isRequired,
|
||||
dispatchCloneCustomFormat: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(CustomFormatsConnector);
|
|
@ -1,17 +0,0 @@
|
|||
import React from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
|
||||
function CustomFormatsConnector() {
|
||||
return (
|
||||
<PageContent title="Custom Formats Settings">
|
||||
<SettingsToolbarConnector
|
||||
showSave={false}
|
||||
/>
|
||||
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomFormatsConnector;
|
||||
|
|
@ -121,7 +121,7 @@ class EditQualityProfileModalContentConnector extends Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
return i.id === cutoff.id || (i.quality && i.quality.id === cutoff.id);
|
||||
return i.id === cutoff || (i.quality && i.quality.id === cutoff);
|
||||
});
|
||||
|
||||
// If the cutoff isn't allowed anymore or there isn't a cutoff set one
|
||||
|
|
|
@ -97,20 +97,20 @@ class QualityProfile extends Component {
|
|||
}
|
||||
|
||||
if (item.quality) {
|
||||
const isCutoff = item.quality.id === cutoff.id;
|
||||
const isCutoff = item.quality.id === cutoff;
|
||||
|
||||
return (
|
||||
<Label
|
||||
key={item.quality.id}
|
||||
kind={isCutoff ? kinds.INFO : kinds.default}
|
||||
title={isCutoff ? 'Cutoff' : null}
|
||||
title={isCutoff ? 'Upgrade until this quality is met or exceeded' : null}
|
||||
>
|
||||
{item.quality.name}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
const isCutoff = item.id === cutoff.id;
|
||||
const isCutoff = item.id === cutoff;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
|
@ -174,7 +174,7 @@ class QualityProfile extends Component {
|
|||
QualityProfile.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
cutoff: PropTypes.object.isRequired,
|
||||
cutoff: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
onConfirmDeleteQualityProfile: PropTypes.func.isRequired,
|
||||
|
|
97
frontend/src/Store/Actions/Settings/customFormats.js
Normal file
97
frontend/src/Store/Actions/Settings/customFormats.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
import { createAction } from 'redux-actions';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||
import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler';
|
||||
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
const section = 'settings.customFormats';
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_CUSTOM_FORMATS = 'settings/customFormats/fetchCustomFormats';
|
||||
export const FETCH_CUSTOM_FORMAT_SCHEMA = 'settings/customFormats/fetchCustomFormatSchema';
|
||||
export const SAVE_CUSTOM_FORMAT = 'settings/customFormats/saveCustomFormat';
|
||||
export const DELETE_CUSTOM_FORMAT = 'settings/customFormats/deleteCustomFormat';
|
||||
export const SET_CUSTOM_FORMAT_VALUE = 'settings/customFormats/setCustomFormatValue';
|
||||
export const CLONE_CUSTOM_FORMAT = 'settings/customFormats/cloneCustomFormat';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchCustomFormats = createThunk(FETCH_CUSTOM_FORMATS);
|
||||
export const fetchCustomFormatSchema = createThunk(FETCH_CUSTOM_FORMAT_SCHEMA);
|
||||
export const saveCustomFormat = createThunk(SAVE_CUSTOM_FORMAT);
|
||||
export const deleteCustomFormat = createThunk(DELETE_CUSTOM_FORMAT);
|
||||
|
||||
export const setCustomFormatValue = createAction(SET_CUSTOM_FORMAT_VALUE, (payload) => {
|
||||
return {
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
});
|
||||
|
||||
export const cloneCustomFormat = createAction(CLONE_CUSTOM_FORMAT);
|
||||
|
||||
//
|
||||
// Details
|
||||
|
||||
export default {
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
defaultState: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
isDeleting: false,
|
||||
deleteError: null,
|
||||
isSchemaFetching: false,
|
||||
isSchemaPopulated: false,
|
||||
schemaError: null,
|
||||
schema: {},
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
items: [],
|
||||
pendingChanges: {}
|
||||
},
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
actionHandlers: {
|
||||
[FETCH_CUSTOM_FORMATS]: createFetchHandler(section, '/customformat'),
|
||||
[FETCH_CUSTOM_FORMAT_SCHEMA]: createFetchSchemaHandler(section, '/customformat/schema'),
|
||||
[SAVE_CUSTOM_FORMAT]: createSaveProviderHandler(section, '/customformat'),
|
||||
[DELETE_CUSTOM_FORMAT]: createRemoveItemHandler(section, '/customformat')
|
||||
},
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
reducers: {
|
||||
[SET_CUSTOM_FORMAT_VALUE]: createSetSettingValueReducer(section),
|
||||
|
||||
[CLONE_CUSTOM_FORMAT]: function(state, { payload }) {
|
||||
const id = payload.id;
|
||||
const newState = getSectionState(state, section);
|
||||
const item = newState.items.find((i) => i.id === id);
|
||||
const pendingChanges = { ...item, id: 0 };
|
||||
delete pendingChanges.id;
|
||||
|
||||
pendingChanges.name = `${pendingChanges.name} - Copy`;
|
||||
newState.pendingChanges = pendingChanges;
|
||||
|
||||
return updateSectionState(state, section, newState);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
|
@ -1,6 +1,7 @@
|
|||
import { createAction } from 'redux-actions';
|
||||
import { handleThunks } from 'Store/thunks';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import customFormats from './Settings/customFormats';
|
||||
import delayProfiles from './Settings/delayProfiles';
|
||||
import downloadClients from './Settings/downloadClients';
|
||||
import downloadClientOptions from './Settings/downloadClientOptions';
|
||||
|
@ -20,6 +21,7 @@ import remotePathMappings from './Settings/remotePathMappings';
|
|||
import restrictions from './Settings/restrictions';
|
||||
import ui from './Settings/ui';
|
||||
|
||||
export * from './Settings/customFormats';
|
||||
export * from './Settings/delayProfiles';
|
||||
export * from './Settings/downloadClients';
|
||||
export * from './Settings/downloadClientOptions';
|
||||
|
@ -50,6 +52,7 @@ export const section = 'settings';
|
|||
export const defaultState = {
|
||||
advancedSettings: false,
|
||||
|
||||
customFormats: customFormats.defaultState,
|
||||
delayProfiles: delayProfiles.defaultState,
|
||||
downloadClients: downloadClients.defaultState,
|
||||
downloadClientOptions: downloadClientOptions.defaultState,
|
||||
|
@ -88,6 +91,7 @@ export const toggleAdvancedSettings = createAction(TOGGLE_ADVANCED_SETTINGS);
|
|||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
...customFormats.actionHandlers,
|
||||
...delayProfiles.actionHandlers,
|
||||
...downloadClients.actionHandlers,
|
||||
...downloadClientOptions.actionHandlers,
|
||||
|
@ -117,6 +121,7 @@ export const reducers = createHandleActions({
|
|||
return Object.assign({}, state, { advancedSettings: !state.advancedSettings });
|
||||
},
|
||||
|
||||
...customFormats.reducers,
|
||||
...delayProfiles.reducers,
|
||||
...downloadClients.reducers,
|
||||
...downloadClientOptions.reducers,
|
||||
|
|
|
@ -6,7 +6,6 @@ using NzbDrone.Core.CustomFormats;
|
|||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Validation;
|
||||
using Radarr.Http;
|
||||
using Radarr.Http.Mapping;
|
||||
|
||||
namespace NzbDrone.Api.Profiles
|
||||
{
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Api.Qualities;
|
||||
using Radarr.Http.REST;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
@ -37,14 +38,42 @@ namespace NzbDrone.Api.Profiles
|
|||
{
|
||||
if (model == null) return null;
|
||||
|
||||
var cutoffItem = model.Items.First(q =>
|
||||
{
|
||||
if (q.Id == model.Cutoff) return true;
|
||||
|
||||
if (q.Quality == null) return false;
|
||||
|
||||
return q.Quality.Id == model.Cutoff;
|
||||
});
|
||||
|
||||
var cutoff = cutoffItem.Items == null || cutoffItem.Items.Empty()
|
||||
? cutoffItem.Quality
|
||||
: cutoffItem.Items.First().Quality;
|
||||
|
||||
return new ProfileResource
|
||||
{
|
||||
Id = model.Id,
|
||||
|
||||
Name = model.Name,
|
||||
Cutoff = model.Cutoff,
|
||||
PreferredTags = model.PreferredTags != null ? string.Join(",", model.PreferredTags) : "",
|
||||
Items = model.Items.ConvertAll(ToResource),
|
||||
Cutoff = cutoff,
|
||||
|
||||
// Flatten groups so things don't explode
|
||||
Items = model.Items.SelectMany(i =>
|
||||
{
|
||||
if (i == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (i.Items.Any())
|
||||
{
|
||||
return i.Items.ConvertAll(ToResource);
|
||||
}
|
||||
|
||||
return new List<ProfileQualityItemResource> { ToResource(i) };
|
||||
}).ToList(),
|
||||
FormatCutoff = model.FormatCutoff.ToResource(),
|
||||
FormatItems = model.FormatItems.ConvertAll(ToResource),
|
||||
Language = model.Language
|
||||
|
@ -80,7 +109,7 @@ namespace NzbDrone.Api.Profiles
|
|||
Id = resource.Id,
|
||||
|
||||
Name = resource.Name,
|
||||
Cutoff = (Quality)resource.Cutoff.Id,
|
||||
Cutoff = resource.Cutoff.Id,
|
||||
PreferredTags = resource.PreferredTags.Split(',').ToList(),
|
||||
Items = resource.Items.ConvertAll(ToModel),
|
||||
FormatCutoff = resource.FormatCutoff.ToModel(),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
@ -42,7 +42,7 @@ namespace NzbDrone.Api.Profiles
|
|||
});
|
||||
|
||||
var profile = new Profile();
|
||||
profile.Cutoff = Quality.Unknown;
|
||||
profile.Cutoff = Quality.Unknown.Id;
|
||||
profile.Items = items;
|
||||
profile.FormatCutoff = CustomFormat.None;
|
||||
profile.FormatItems = formatItems;
|
||||
|
|
|
@ -19,7 +19,7 @@ namespace NzbDrone.Core.Test.Datastore
|
|||
var profile = new Profile
|
||||
{
|
||||
Name = "Test",
|
||||
Cutoff = Quality.WEBDL720p,
|
||||
Cutoff = Quality.WEBDL720p.Id,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities()
|
||||
};
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
|
||||
|
||||
var fakeSeries = Builder<Movie>.CreateNew()
|
||||
.With(c => c.Profile = (LazyLoaded<Profile>)new Profile { Cutoff = Quality.Bluray1080p })
|
||||
.With(c => c.Profile = (LazyLoaded<Profile>)new Profile { Cutoff = Quality.Bluray1080p.Id })
|
||||
.Build();
|
||||
|
||||
remoteMovie = new RemoteMovie
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Profiles;
|
||||
|
@ -32,28 +32,28 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
[Test]
|
||||
public void should_return_true_if_current_episode_is_less_than_cutoff()
|
||||
{
|
||||
Subject.CutoffNotMet(new Profile { Cutoff = Quality.Bluray1080p, Items = Qualities.QualityFixture.GetDefaultQualities() },
|
||||
Subject.CutoffNotMet(new Profile { Cutoff = Quality.Bluray1080p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() },
|
||||
new QualityModel(Quality.DVD, new Revision(version: 2))).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_false_if_current_episode_is_equal_to_cutoff()
|
||||
{
|
||||
Subject.CutoffNotMet(new Profile { Cutoff = Quality.HDTV720p, Items = Qualities.QualityFixture.GetDefaultQualities() },
|
||||
Subject.CutoffNotMet(new Profile { Cutoff = Quality.HDTV720p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() },
|
||||
new QualityModel(Quality.HDTV720p, new Revision(version: 2))).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_false_if_current_episode_is_greater_than_cutoff()
|
||||
{
|
||||
Subject.CutoffNotMet(new Profile { Cutoff = Quality.HDTV720p, Items = Qualities.QualityFixture.GetDefaultQualities() },
|
||||
Subject.CutoffNotMet(new Profile { Cutoff = Quality.HDTV720p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() },
|
||||
new QualityModel(Quality.Bluray1080p, new Revision(version: 2))).Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_when_new_episode_is_proper_but_existing_is_not()
|
||||
{
|
||||
Subject.CutoffNotMet(new Profile { Cutoff = Quality.HDTV720p, Items = Qualities.QualityFixture.GetDefaultQualities() },
|
||||
Subject.CutoffNotMet(new Profile { Cutoff = Quality.HDTV720p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() },
|
||||
new QualityModel(Quality.HDTV720p, new Revision(version: 1)),
|
||||
new QualityModel(Quality.HDTV720p, new Revision(version: 2))).Should().BeTrue();
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
[Test]
|
||||
public void should_return_false_if_cutoff_is_met_and_quality_is_higher()
|
||||
{
|
||||
Subject.CutoffNotMet(new Profile { Cutoff = Quality.HDTV720p, Items = Qualities.QualityFixture.GetDefaultQualities() },
|
||||
Subject.CutoffNotMet(new Profile { Cutoff = Quality.HDTV720p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() },
|
||||
new QualityModel(Quality.HDTV720p, new Revision(version: 2)),
|
||||
new QualityModel(Quality.Bluray1080p, new Revision(version: 2))).Should().BeFalse();
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
Subject.CutoffNotMet(
|
||||
new Profile
|
||||
{
|
||||
Cutoff = Quality.HDTV720p,
|
||||
Cutoff = Quality.HDTV720p.Id,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
FormatCutoff = CustomFormats.CustomFormat.None,
|
||||
FormatItems = CustomFormatsFixture.GetSampleFormatItems("None", "My Format")
|
||||
|
|
|
@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
_upgradeHistory = Mocker.Resolve<HistorySpecification>();
|
||||
|
||||
_fakeMovie = Builder<Movie>.CreateNew()
|
||||
.With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p, Items = Qualities.QualityFixture.GetDefaultQualities() })
|
||||
.With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() })
|
||||
.Build();
|
||||
|
||||
_parseResultSingle = new RemoteMovie
|
||||
|
@ -144,7 +144,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
[Test]
|
||||
public void should_not_be_upgradable_if_episode_is_of_same_quality_as_existing()
|
||||
{
|
||||
_fakeMovie.Profile = new Profile { Cutoff = Quality.Bluray1080p, Items = Qualities.QualityFixture.GetDefaultQualities() };
|
||||
_fakeMovie.Profile = new Profile { Cutoff = Quality.Bluray1080p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() };
|
||||
_parseResultSingle.ParsedMovieInfo.Quality = new QualityModel(Quality.WEBDL1080p, new Revision(version: 1));
|
||||
_upgradableQuality = new QualityModel(Quality.WEBDL1080p, new Revision(version: 1));
|
||||
|
||||
|
@ -156,7 +156,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
[Test]
|
||||
public void should_not_be_upgradable_if_cutoff_already_met()
|
||||
{
|
||||
_fakeMovie.Profile = new Profile { Cutoff = Quality.WEBDL1080p, Items = Qualities.QualityFixture.GetDefaultQualities() };
|
||||
_fakeMovie.Profile = new Profile { Cutoff = Quality.WEBDL1080p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() };
|
||||
_parseResultSingle.ParsedMovieInfo.Quality = new QualityModel(Quality.WEBDL1080p, new Revision(version: 1));
|
||||
_upgradableQuality = new QualityModel(Quality.Bluray1080p, new Revision(version: 1));
|
||||
|
||||
|
@ -184,7 +184,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
public void should_return_false_if_cutoff_already_met_and_cdh_is_disabled()
|
||||
{
|
||||
GivenCdhDisabled();
|
||||
_fakeMovie.Profile = new Profile { Cutoff = Quality.WEBDL1080p, Items = Qualities.QualityFixture.GetDefaultQualities() };
|
||||
_fakeMovie.Profile = new Profile { Cutoff = Quality.WEBDL1080p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() };
|
||||
_parseResultSingle.ParsedMovieInfo.Quality = new QualityModel(Quality.Bluray1080p, new Revision(version: 1));
|
||||
_upgradableQuality = new QualityModel(Quality.WEBDL1080p, new Revision(version: 1));
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
public void Setup()
|
||||
{
|
||||
var fakeSeries = Builder<Movie>.CreateNew()
|
||||
.With(c => c.Profile = (LazyLoaded<Profile>)new Profile { Cutoff = Quality.Bluray1080p })
|
||||
.With(c => c.Profile = (LazyLoaded<Profile>)new Profile { Cutoff = Quality.Bluray1080p.Id })
|
||||
.Build();
|
||||
|
||||
remoteMovie = new RemoteMovie
|
||||
|
|
|
@ -81,7 +81,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
[Test]
|
||||
public void should_return_true_when_quality_in_queue_is_lower()
|
||||
{
|
||||
_movie.Profile.Value.Cutoff = Quality.Bluray1080p;
|
||||
_movie.Profile.Value.Cutoff = Quality.Bluray1080p.Id;
|
||||
|
||||
var remoteEpisode = Builder<RemoteMovie>.CreateNew()
|
||||
.With(r => r.Movie = _movie)
|
||||
|
@ -113,7 +113,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
[Test]
|
||||
public void should_return_false_when_quality_in_queue_is_better()
|
||||
{
|
||||
_movie.Profile.Value.Cutoff = Quality.Bluray1080p;
|
||||
_movie.Profile.Value.Cutoff = Quality.Bluray1080p.Id;
|
||||
|
||||
var remoteEpisode = Builder<RemoteMovie>.CreateNew()
|
||||
.With(r => r.Movie = _movie)
|
||||
|
@ -130,7 +130,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
[Test]
|
||||
public void should_return_false_if_quality_in_queue_meets_cutoff()
|
||||
{
|
||||
_movie.Profile.Value.Cutoff = _remoteMovie.ParsedMovieInfo.Quality.Quality;
|
||||
_movie.Profile.Value.Cutoff = _remoteMovie.ParsedMovieInfo.Quality.Quality.Id;
|
||||
|
||||
var remoteEpisode = Builder<RemoteMovie>.CreateNew()
|
||||
.With(r => r.Movie = _movie)
|
||||
|
|
|
@ -51,7 +51,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
_profile.Items.Add(new ProfileQualityItem { Allowed = true, Quality = Quality.WEBDL720p });
|
||||
_profile.Items.Add(new ProfileQualityItem { Allowed = true, Quality = Quality.Bluray720p });
|
||||
|
||||
_profile.Cutoff = Quality.WEBDL720p;
|
||||
_profile.Cutoff = Quality.WEBDL720p.Id;
|
||||
|
||||
_remoteEpisode.ParsedMovieInfo = new ParsedMovieInfo();
|
||||
_remoteEpisode.Release = new ReleaseInfo();
|
||||
|
|
|
@ -34,7 +34,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||
_secondFile = new MovieFile { Quality = new QualityModel(Quality.Bluray1080p, new Revision(version: 1)), DateAdded = DateTime.Now };
|
||||
|
||||
var fakeSeries = Builder<Movie>.CreateNew()
|
||||
.With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p })
|
||||
.With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p.Id })
|
||||
.With(c => c.MovieFile = _firstFile)
|
||||
.Build();
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
_firstFile = new MovieFile { Quality = new QualityModel(Quality.Bluray1080p, new Revision(version: 2)), DateAdded = DateTime.Now };
|
||||
|
||||
var fakeSeries = Builder<Movie>.CreateNew()
|
||||
.With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p, Items = Qualities.QualityFixture.GetDefaultQualities() })
|
||||
.With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p.Id, Items = Qualities.QualityFixture.GetDefaultQualities() })
|
||||
.With(e => e.MovieFile = _firstFile)
|
||||
.Build();
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
|
|||
_profile = new Profile
|
||||
{
|
||||
Name = "Test",
|
||||
Cutoff = Quality.HDTV720p,
|
||||
Cutoff = Quality.HDTV720p.Id,
|
||||
Items = new List<ProfileQualityItem>
|
||||
{
|
||||
new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p },
|
||||
|
|
|
@ -35,7 +35,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
|
|||
_profile = new Profile
|
||||
{
|
||||
Name = "Test",
|
||||
Cutoff = Quality.HDTV720p,
|
||||
Cutoff = Quality.HDTV720p.Id,
|
||||
Items = new List<ProfileQualityItem>
|
||||
{
|
||||
new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p },
|
||||
|
|
|
@ -38,7 +38,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
|
|||
_profile = new Profile
|
||||
{
|
||||
Name = "Test",
|
||||
Cutoff = Quality.HDTV720p,
|
||||
Cutoff = Quality.HDTV720p.Id,
|
||||
Items = new List<ProfileQualityItem>
|
||||
{
|
||||
new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p },
|
||||
|
|
|
@ -25,8 +25,8 @@ namespace NzbDrone.Core.Test.HistoryTests
|
|||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_profile = new Profile { Cutoff = Quality.WEBDL720p, Items = QualityFixture.GetDefaultQualities() };
|
||||
_profileCustom = new Profile { Cutoff = Quality.WEBDL720p, Items = QualityFixture.GetDefaultQualities(Quality.DVD) };
|
||||
_profile = new Profile { Cutoff = Quality.WEBDL720p.Id, Items = QualityFixture.GetDefaultQualities() };
|
||||
_profileCustom = new Profile { Cutoff = Quality.WEBDL720p.Id, Items = QualityFixture.GetDefaultQualities(Quality.DVD) };
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
|
@ -25,7 +25,7 @@ namespace NzbDrone.Core.Test.MovieTests.MovieRepositoryTests
|
|||
Items = Qualities.QualityFixture.GetDefaultQualities(Quality.Bluray1080p, Quality.DVD, Quality.HDTV720p),
|
||||
FormatItems = CustomFormat.CustomFormatsFixture.GetDefaultFormatItems(),
|
||||
FormatCutoff = CustomFormats.CustomFormat.None,
|
||||
Cutoff = Quality.Bluray1080p,
|
||||
Cutoff = Quality.Bluray1080p.Id,
|
||||
Name = "TestProfile"
|
||||
};
|
||||
|
||||
|
|
|
@ -305,6 +305,7 @@
|
|||
<Compile Include="ParserTests\ParsingServiceTests\AugmentersTests\AugmentWithReleaseInfoFixture.cs" />
|
||||
<Compile Include="ParserTests\ParsingServiceTests\ParseQualityDefinitionFixture.cs" />
|
||||
<Compile Include="ParserTests\RomanNumeralTests\RomanNumeralConversionFixture.cs" />
|
||||
<Compile Include="Profiles\Qualities\QualityIndexCompareToFixture.cs" />
|
||||
<Compile Include="Qualities\RevisionComparableFixture.cs" />
|
||||
<Compile Include="QueueTests\QueueServiceFixture.cs" />
|
||||
<Compile Include="RemotePathMappingsTests\RemotePathMappingServiceFixture.cs" />
|
||||
|
|
|
@ -22,7 +22,7 @@ namespace NzbDrone.Core.Test.Profiles
|
|||
Items = Qualities.QualityFixture.GetDefaultQualities(Quality.Bluray1080p, Quality.DVD, Quality.HDTV720p),
|
||||
FormatCutoff = CustomFormats.CustomFormat.None,
|
||||
FormatItems = CustomFormat.CustomFormatsFixture.GetDefaultFormatItems(),
|
||||
Cutoff = Quality.Bluray1080p,
|
||||
Cutoff = Quality.Bluray1080p.Id,
|
||||
Name = "TestProfile"
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
using System.Collections.Generic;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Qualities
|
||||
{
|
||||
[TestFixture]
|
||||
public class QualityIndexCompareToFixture : CoreTest
|
||||
{
|
||||
[TestCase(1, 0, 1, 0, 0)]
|
||||
[TestCase(1, 1, 1, 0, 1)]
|
||||
[TestCase(2, 0, 1, 0, 1)]
|
||||
[TestCase(1, 0, 1, 1, -1)]
|
||||
[TestCase(1, 0, 2, 0, -1)]
|
||||
public void should_match_expected_when_respect_group_order_is_true(int leftIndex, int leftGroupIndex, int rightIndex, int rightGroupIndex, int expected)
|
||||
{
|
||||
var left = new QualityIndex(leftIndex, leftGroupIndex);
|
||||
var right = new QualityIndex(rightIndex, rightGroupIndex);
|
||||
left.CompareTo(right, true).Should().Be(expected);
|
||||
}
|
||||
|
||||
[TestCase(1, 0, 1, 0, 0)]
|
||||
[TestCase(1, 1, 1, 0, 0)]
|
||||
[TestCase(2, 0, 1, 0, 1)]
|
||||
[TestCase(1, 0, 1, 1, 0)]
|
||||
[TestCase(1, 0, 2, 0, -1)]
|
||||
public void should_match_expected_when_respect_group_order_is_false(int leftIndex, int leftGroupIndex, int rightIndex, int rightGroupIndex, int expected)
|
||||
{
|
||||
var left = new QualityIndex(leftIndex, leftGroupIndex);
|
||||
var right = new QualityIndex(rightIndex, rightGroupIndex);
|
||||
left.CompareTo(right, false).Should().Be(expected);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Profiles;
|
||||
|
@ -31,6 +31,50 @@ namespace NzbDrone.Core.Test.Qualities
|
|||
Subject = new QualityModelComparer(new Profile { Items = QualityFixture.GetDefaultQualities(Quality.Bluray720p, Quality.DVD) });
|
||||
}
|
||||
|
||||
private void GivenGroupedProfile()
|
||||
{
|
||||
var profile = new Profile
|
||||
{
|
||||
Items = new List<ProfileQualityItem>
|
||||
{
|
||||
new ProfileQualityItem
|
||||
{
|
||||
Allowed = false,
|
||||
Quality = Quality.SDTV
|
||||
},
|
||||
new ProfileQualityItem
|
||||
{
|
||||
Allowed = false,
|
||||
Quality = Quality.DVD
|
||||
},
|
||||
new ProfileQualityItem
|
||||
{
|
||||
Allowed = true,
|
||||
Items = new List<ProfileQualityItem>
|
||||
{
|
||||
new ProfileQualityItem
|
||||
{
|
||||
Allowed = true,
|
||||
Quality = Quality.HDTV720p
|
||||
},
|
||||
new ProfileQualityItem
|
||||
{
|
||||
Allowed = true,
|
||||
Quality = Quality.WEBDL720p
|
||||
}
|
||||
}
|
||||
},
|
||||
new ProfileQualityItem
|
||||
{
|
||||
Allowed = true,
|
||||
Quality = Quality.Bluray720p
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Subject = new QualityModelComparer(profile);
|
||||
}
|
||||
|
||||
private void GivenDefaultProfileWithFormats()
|
||||
{
|
||||
_customFormat1 = new CustomFormats.CustomFormat("My Format 1", "L_ENGLISH"){Id=1};
|
||||
|
@ -118,5 +162,31 @@ namespace NzbDrone.Core.Test.Qualities
|
|||
|
||||
compare.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_ignore_group_order_by_default()
|
||||
{
|
||||
GivenGroupedProfile();
|
||||
|
||||
var first = new QualityModel(Quality.HDTV720p);
|
||||
var second = new QualityModel(Quality.WEBDL720p);
|
||||
|
||||
var compare = Subject.Compare(first, second);
|
||||
|
||||
compare.Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_respect_group_order()
|
||||
{
|
||||
GivenGroupedProfile();
|
||||
|
||||
var first = new QualityModel(Quality.HDTV720p);
|
||||
var second = new QualityModel(Quality.WEBDL720p);
|
||||
|
||||
var compare = Subject.Compare(first, second, true);
|
||||
|
||||
compare.Should().BeLessThan(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -112,7 +112,8 @@ namespace NzbDrone.Core.Datastore
|
|||
Mapper.Entity<ImportExclusion>().RegisterModel("ImportExclusions");
|
||||
|
||||
Mapper.Entity<QualityDefinition>().RegisterModel("QualityDefinitions")
|
||||
.Ignore(d => d.Weight)
|
||||
.Ignore(d => d.GroupName)
|
||||
.Ignore(d => d.Weight)
|
||||
.Relationship();
|
||||
|
||||
Mapper.Entity<CustomFormat>().RegisterModel("CustomFormats")
|
||||
|
|
|
@ -61,7 +61,7 @@ namespace NzbDrone.Core.DecisionEngine
|
|||
|
||||
private int CompareQuality(DownloadDecision x, DownloadDecision y)
|
||||
{
|
||||
return CompareAll(CompareBy(x.RemoteMovie, y.RemoteMovie, remoteMovie => remoteMovie.Movie.Profile.Value.Items.FindIndex(v => v.Quality == remoteMovie.ParsedMovieInfo.Quality.Quality)),
|
||||
return CompareAll(CompareBy(x.RemoteMovie, y.RemoteMovie, remoteMovie => remoteMovie.Movie.Profile.Value.GetIndex(remoteMovie.ParsedMovieInfo.Quality.Quality)),
|
||||
CompareCustomFormats(x, y),
|
||||
CompareBy(x.RemoteMovie, y.RemoteMovie, remoteMovie => remoteMovie.ParsedMovieInfo.Quality.Revision.Real),
|
||||
CompareBy(x.RemoteMovie, y.RemoteMovie, remoteMovie => remoteMovie.ParsedMovieInfo.Quality.Revision.Version));
|
||||
|
|
|
@ -43,7 +43,7 @@ namespace NzbDrone.Core.DecisionEngine
|
|||
public bool CutoffNotMet(Profile profile, QualityModel currentQuality, QualityModel newQuality = null)
|
||||
{
|
||||
var comparer = new QualityModelComparer(profile);
|
||||
var compare = comparer.Compare(currentQuality.Quality, profile.Cutoff);
|
||||
var compare = comparer.Compare(currentQuality.Quality.Id, profile.Cutoff);
|
||||
|
||||
if (compare < 0)
|
||||
{
|
||||
|
|
|
@ -21,11 +21,18 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
|
||||
public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var profile = subject.Movie.Profile.Value;
|
||||
|
||||
if (subject.Movie.MovieFile != null)
|
||||
{
|
||||
if (!_qualityUpgradableSpecification.CutoffNotMet(subject.Movie.Profile, subject.Movie.MovieFile.Quality, subject.ParsedMovieInfo.Quality))
|
||||
if (!_qualityUpgradableSpecification.CutoffNotMet(profile,
|
||||
subject.Movie.MovieFile.Quality,
|
||||
subject.ParsedMovieInfo.Quality))
|
||||
{
|
||||
return Decision.Reject("Existing file meets cutoff: {0}", subject.Movie.Profile.Value.Cutoff);
|
||||
var qualityCutoffIndex = profile.GetIndex(profile.Cutoff);
|
||||
var qualityCutoff = profile.Items[qualityCutoffIndex.Index];
|
||||
|
||||
return Decision.Reject("Existing file meets cutoff: {0} - {1}", qualityCutoff, subject.Movie.Profile.Value.Cutoff);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
|||
public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
_logger.Debug("Checking if report meets quality requirements. {0}", subject.ParsedMovieInfo.Quality);
|
||||
if (!subject.Movie.Profile.Value.Items.Exists(v => v.Allowed && v.Quality == subject.ParsedMovieInfo.Quality.Quality))
|
||||
var profile = subject.Movie.Profile.Value;
|
||||
var qualityIndex = profile.GetIndex(subject.ParsedMovieInfo.Quality.Quality);
|
||||
var qualityOrGroup = profile.Items[qualityIndex.Index];
|
||||
|
||||
if (!qualityOrGroup.Allowed)
|
||||
{
|
||||
_logger.Debug("Quality {0} rejected by Movie's quality profile", subject.ParsedMovieInfo.Quality);
|
||||
return Decision.Reject("{0} is not wanted in profile", subject.ParsedMovieInfo.Quality.Quality);
|
||||
|
|
|
@ -82,8 +82,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
|||
}
|
||||
|
||||
// If quality meets or exceeds the best allowed quality in the profile accept it immediately
|
||||
var bestQualityInProfile = new QualityModel(profile.LastAllowedQuality());
|
||||
var isBestInProfile = comparer.Compare(subject.ParsedMovieInfo.Quality, bestQualityInProfile) >= 0;
|
||||
var bestQualityInProfile = profile.LastAllowedQuality();
|
||||
var isBestInProfile = comparer.Compare(subject.ParsedMovieInfo.Quality.Quality, bestQualityInProfile) >= 0;
|
||||
|
||||
if (isBestInProfile && isPreferredProtocol && (preferredCount > 0 || preferredWords == null))
|
||||
{
|
||||
|
|
|
@ -21,8 +21,8 @@ namespace NzbDrone.Core.MediaFiles.MovieImport.Specifications
|
|||
var qualityComparer = new QualityModelComparer(localMovie.Movie.Profile);
|
||||
if (localMovie.Movie.MovieFile != null && qualityComparer.Compare(localMovie.Movie.MovieFile.Quality, localMovie.Quality) > 0)
|
||||
{
|
||||
_logger.Debug("This file isn't an upgrade for all episodes. Skipping {0}", localMovie.Path);
|
||||
return Decision.Reject("Not an upgrade for existing episode file(s)");
|
||||
_logger.Debug("This file isn't an upgrade for all movies. Skipping {0}", localMovie.Path);
|
||||
return Decision.Reject("Not an upgrade for existing movie file(s)");
|
||||
}
|
||||
|
||||
return Decision.Accept();
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
@ -16,13 +16,11 @@ namespace NzbDrone.Core.Movies
|
|||
{
|
||||
private readonly IMovieRepository _movieRepository;
|
||||
private readonly IProfileService _profileService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public MovieCutoffService(IMovieRepository movieRepository, IProfileService profileService, Logger logger)
|
||||
{
|
||||
_movieRepository = movieRepository;
|
||||
_profileService = profileService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public PagingSpec<Movie> MoviesWhereCutoffUnmet(PagingSpec<Movie> pagingSpec)
|
||||
|
@ -33,12 +31,12 @@ namespace NzbDrone.Core.Movies
|
|||
//Get all items less than the cutoff
|
||||
foreach (var profile in profiles)
|
||||
{
|
||||
var cutoffIndex = profile.Items.FindIndex(v => v.Quality.Id == profile.Cutoff.Id);
|
||||
var belowCutoff = profile.Items.Take(cutoffIndex).ToList();
|
||||
var cutoffIndex = profile.GetIndex(profile.Cutoff);
|
||||
var belowCutoff = profile.Items.Take(cutoffIndex.Index).ToList();
|
||||
|
||||
if (belowCutoff.Any())
|
||||
{
|
||||
qualitiesBelowCutoff.Add(new QualitiesBelowCutoff(profile.Id, belowCutoff.Select(i => i.Quality.Id)));
|
||||
qualitiesBelowCutoff.Add(new QualitiesBelowCutoff(profile.Id, belowCutoff.SelectMany(i => i.GetQualities().Select(q => q.Id))));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1030,6 +1030,7 @@
|
|||
<Compile Include="Parser\RomanNumerals\RomanNumeralParser.cs" />
|
||||
<Compile Include="Parser\RomanNumerals\SimpleArabicNumeral.cs" />
|
||||
<Compile Include="Parser\RomanNumerals\SimpleRomanNumeral.cs" />
|
||||
<Compile Include="Profiles\QualityIndex.cs" />
|
||||
<Compile Include="Profiles\Delay\DelayProfile.cs" />
|
||||
<Compile Include="Profiles\Delay\DelayProfileService.cs" />
|
||||
<Compile Include="Profiles\Delay\DelayProfileTagInUseValidator.cs" />
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
@ -15,7 +15,7 @@ namespace NzbDrone.Core.Profiles
|
|||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
public Quality Cutoff { get; set; }
|
||||
public int Cutoff { get; set; }
|
||||
public List<ProfileQualityItem> Items { get; set; }
|
||||
public CustomFormat FormatCutoff { get; set; }
|
||||
public List<ProfileFormatItem> FormatItems { get; set; }
|
||||
|
@ -24,7 +24,54 @@ namespace NzbDrone.Core.Profiles
|
|||
|
||||
public Quality LastAllowedQuality()
|
||||
{
|
||||
return Items.Last(q => q.Allowed).Quality;
|
||||
var lastAllowed = Items.Last(q => q.Allowed);
|
||||
|
||||
if (lastAllowed.Quality != null)
|
||||
{
|
||||
return lastAllowed.Quality;
|
||||
}
|
||||
|
||||
// Returning any item from the group will work,
|
||||
// returning the last because it's the true last quality.
|
||||
return lastAllowed.Items.Last().Quality;
|
||||
}
|
||||
|
||||
public QualityIndex GetIndex(Quality quality)
|
||||
{
|
||||
return GetIndex(quality.Id);
|
||||
}
|
||||
|
||||
public QualityIndex GetIndex(int id)
|
||||
{
|
||||
for (var i = 0; i < Items.Count; i++)
|
||||
{
|
||||
var item = Items[i];
|
||||
var quality = item.Quality;
|
||||
|
||||
// Quality matches by ID
|
||||
if (quality != null && quality.Id == id)
|
||||
{
|
||||
return new QualityIndex(i);
|
||||
}
|
||||
|
||||
// Group matches by ID
|
||||
if (item.Id > 0 && item.Id == id)
|
||||
{
|
||||
return new QualityIndex(i);
|
||||
}
|
||||
|
||||
for (var g = 0; g < item.Items.Count; g++)
|
||||
{
|
||||
var groupItem = item.Items[g];
|
||||
|
||||
if (groupItem.Quality.Id == id)
|
||||
{
|
||||
return new QualityIndex(i, g);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new QualityIndex();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,47 @@
|
|||
using NzbDrone.Core.Datastore;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace NzbDrone.Core.Profiles
|
||||
{
|
||||
public class ProfileQualityItem : IEmbeddedDocument
|
||||
{
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
public Quality Quality { get; set; }
|
||||
public List<ProfileQualityItem> Items { get; set; }
|
||||
public bool Allowed { get; set; }
|
||||
|
||||
public ProfileQualityItem()
|
||||
{
|
||||
Items = new List<ProfileQualityItem>();
|
||||
}
|
||||
|
||||
public List<Quality> GetQualities()
|
||||
{
|
||||
if (Quality == null)
|
||||
{
|
||||
return Items.Select(s => s.Quality).ToList();
|
||||
}
|
||||
|
||||
return new List<Quality> { Quality };
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var qualitiesString = string.Join(", ", GetQualities());
|
||||
|
||||
if (Name.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return $"{Name} ({qualitiesString})";
|
||||
}
|
||||
|
||||
return qualitiesString;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ namespace NzbDrone.Core.Profiles
|
|||
List<Profile> All();
|
||||
Profile Get(int id);
|
||||
bool Exists(int id);
|
||||
Profile GetDefaultProfile(string name, Quality cutoff = null, params Quality[] allowed);
|
||||
}
|
||||
|
||||
public class ProfileService : IProfileService, IHandle<ApplicationStartedEvent>
|
||||
|
@ -106,25 +107,6 @@ namespace NzbDrone.Core.Profiles
|
|||
return _profileRepository.Exists(id);
|
||||
}
|
||||
|
||||
private Profile AddDefaultProfile(string name, Quality cutoff, params Quality[] allowed)
|
||||
{
|
||||
var items = Quality.DefaultQualityDefinitions
|
||||
.OrderBy(v => v.Weight)
|
||||
.Select(v => new ProfileQualityItem { Quality = v.Quality, Allowed = allowed.Contains(v.Quality) })
|
||||
.ToList();
|
||||
|
||||
var profile = new Profile { Name = name, Cutoff = cutoff, Items = items, Language = Language.English, FormatCutoff = CustomFormat.None, FormatItems = new List<ProfileFormatItem>
|
||||
{
|
||||
new ProfileFormatItem
|
||||
{
|
||||
Allowed = true,
|
||||
Format = CustomFormat.None
|
||||
}
|
||||
}};
|
||||
|
||||
return Add(profile);
|
||||
}
|
||||
|
||||
public void Handle(ApplicationStartedEvent message)
|
||||
{
|
||||
// Hack to force custom formats to be loaded into memory, if you have a better solution please let me know.
|
||||
|
@ -200,5 +182,70 @@ namespace NzbDrone.Core.Profiles
|
|||
Quality.Remux2160p
|
||||
);
|
||||
}
|
||||
|
||||
public Profile GetDefaultProfile(string name, Quality cutoff = null, params Quality[] allowed)
|
||||
{
|
||||
var groupedQualites = Quality.DefaultQualityDefinitions.GroupBy(q => q.Weight);
|
||||
var items = new List<ProfileQualityItem>();
|
||||
var groupId = 1000;
|
||||
var profileCutoff = cutoff == null ? Quality.Unknown.Id : cutoff.Id;
|
||||
|
||||
foreach (var group in groupedQualites)
|
||||
{
|
||||
if (group.Count() == 1)
|
||||
{
|
||||
var quality = group.First().Quality;
|
||||
|
||||
items.Add(new ProfileQualityItem { Quality = group.First().Quality, Allowed = allowed.Contains(quality) });
|
||||
continue;
|
||||
}
|
||||
|
||||
var groupAllowed = group.Any(g => allowed.Contains(g.Quality));
|
||||
|
||||
items.Add(new ProfileQualityItem
|
||||
{
|
||||
Id = groupId,
|
||||
Name = group.First().GroupName,
|
||||
Items = group.Select(g => new ProfileQualityItem
|
||||
{
|
||||
Quality = g.Quality,
|
||||
Allowed = groupAllowed
|
||||
}).ToList(),
|
||||
Allowed = groupAllowed
|
||||
});
|
||||
|
||||
if (group.Any(g => g.Quality.Id == profileCutoff))
|
||||
{
|
||||
profileCutoff = groupId;
|
||||
}
|
||||
|
||||
groupId++;
|
||||
}
|
||||
|
||||
var qualityProfile = new Profile
|
||||
{
|
||||
Name = name,
|
||||
Cutoff = profileCutoff,
|
||||
Items = items,
|
||||
FormatCutoff = CustomFormat.None,
|
||||
FormatItems = new List<ProfileFormatItem>
|
||||
{
|
||||
new ProfileFormatItem
|
||||
{
|
||||
Allowed = true,
|
||||
Format = CustomFormat.None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return qualityProfile;
|
||||
}
|
||||
|
||||
private Profile AddDefaultProfile(string name, Quality cutoff, params Quality[] allowed)
|
||||
{
|
||||
var profile = GetDefaultProfile(name, cutoff, allowed);
|
||||
|
||||
return Add(profile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
55
src/NzbDrone.Core/Profiles/QualityIndex.cs
Normal file
55
src/NzbDrone.Core/Profiles/QualityIndex.cs
Normal file
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
|
||||
namespace NzbDrone.Core.Profiles
|
||||
{
|
||||
public class QualityIndex : IComparable, IComparable<QualityIndex>
|
||||
{
|
||||
public int Index { get; set; }
|
||||
public int GroupIndex { get; set; }
|
||||
|
||||
public QualityIndex()
|
||||
{
|
||||
Index = 0;
|
||||
GroupIndex = 0;
|
||||
}
|
||||
|
||||
public QualityIndex(int index)
|
||||
{
|
||||
Index = index;
|
||||
GroupIndex = 0;
|
||||
}
|
||||
|
||||
public QualityIndex(int index, int groupIndex)
|
||||
{
|
||||
Index = index;
|
||||
GroupIndex = groupIndex;
|
||||
}
|
||||
|
||||
public int CompareTo(object obj)
|
||||
{
|
||||
return CompareTo((QualityIndex)obj, true);
|
||||
}
|
||||
|
||||
public int CompareTo(QualityIndex other)
|
||||
{
|
||||
return CompareTo(other, true);
|
||||
}
|
||||
|
||||
public int CompareTo(QualityIndex right, bool respectGroupOrder)
|
||||
{
|
||||
if (right == null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var indexCompare = Index.CompareTo(right.Index);
|
||||
|
||||
if (respectGroupOrder && indexCompare == 0)
|
||||
{
|
||||
return GroupIndex.CompareTo(right.GroupIndex);
|
||||
}
|
||||
|
||||
return indexCompare; ;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Qualities
|
||||
{
|
||||
|
@ -7,7 +7,7 @@ namespace NzbDrone.Core.Qualities
|
|||
public Quality Quality { get; set; }
|
||||
|
||||
public string Title { get; set; }
|
||||
|
||||
public string GroupName { get; set; }
|
||||
public int Weight { get; set; }
|
||||
|
||||
public double? MinSize { get; set; }
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Datastore.Migration;
|
||||
using NzbDrone.Core.Instrumentation;
|
||||
using NzbDrone.Core.Profiles;
|
||||
|
||||
namespace NzbDrone.Core.Qualities
|
||||
|
@ -20,12 +18,47 @@ namespace NzbDrone.Core.Qualities
|
|||
_profile = profile;
|
||||
}
|
||||
|
||||
public int Compare(int left, int right, bool respectGroupOrder = false)
|
||||
{
|
||||
var leftIndex = _profile.GetIndex(left);
|
||||
var rightIndex = _profile.GetIndex(right);
|
||||
|
||||
return leftIndex.CompareTo(rightIndex, respectGroupOrder);
|
||||
}
|
||||
|
||||
public int Compare(Quality left, Quality right)
|
||||
{
|
||||
int leftIndex = _profile.Items.FindIndex(v => v.Quality == left);
|
||||
int rightIndex = _profile.Items.FindIndex(v => v.Quality == right);
|
||||
return Compare(left, right, false);
|
||||
}
|
||||
|
||||
return leftIndex.CompareTo(rightIndex);
|
||||
public int Compare(Quality left, Quality right, bool respectGroupOrder)
|
||||
{
|
||||
var leftIndex = _profile.GetIndex(left);
|
||||
var rightIndex = _profile.GetIndex(right);
|
||||
|
||||
return leftIndex.CompareTo(rightIndex, respectGroupOrder);
|
||||
}
|
||||
|
||||
public int Compare(QualityModel left, QualityModel right)
|
||||
{
|
||||
return Compare(left, right, false);
|
||||
}
|
||||
|
||||
public int Compare(QualityModel left, QualityModel right, bool respectGroupOrder)
|
||||
{
|
||||
int result = Compare(left.Quality, right.Quality, respectGroupOrder);
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
result = Compare(left.CustomFormats, right.CustomFormats);
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
result = left.Revision.CompareTo(right.Revision);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public int Compare(List<CustomFormat> left, List<CustomFormat> right)
|
||||
|
@ -61,22 +94,5 @@ namespace NzbDrone.Core.Qualities
|
|||
|
||||
return leftIndicies.Select(i => i.CompareTo(rightIndex)).Sum();
|
||||
}
|
||||
|
||||
public int Compare(QualityModel left, QualityModel right)
|
||||
{
|
||||
int result = Compare(left.Quality, right.Quality);
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
result = Compare(left.CustomFormats, right.CustomFormats);
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
result = left.Revision.CompareTo(right.Revision);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
39
src/Radarr.Api.V2/Profiles/Quality/QualityCutoffValidator.cs
Normal file
39
src/Radarr.Api.V2/Profiles/Quality/QualityCutoffValidator.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Validators;
|
||||
|
||||
namespace Radarr.Api.V2.Profiles.Quality
|
||||
{
|
||||
public static class QualityCutoffValidator
|
||||
{
|
||||
public static IRuleBuilderOptions<T, int> ValidCutoff<T>(this IRuleBuilder<T, int> ruleBuilder)
|
||||
{
|
||||
return ruleBuilder.SetValidator(new ValidCutoffValidator<T>());
|
||||
}
|
||||
}
|
||||
|
||||
public class ValidCutoffValidator<T> : PropertyValidator
|
||||
{
|
||||
public ValidCutoffValidator()
|
||||
: base("Cutoff must be an allowed quality or group")
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
var cutoff = (int)context.PropertyValue;
|
||||
dynamic instance = context.ParentContext.InstanceToValidate;
|
||||
var items = instance.Items as IList<QualityProfileQualityItemResource>;
|
||||
|
||||
var cutoffItem = items.SingleOrDefault(i => (i.Quality == null && i.Id == cutoff) || i.Quality?.Id == cutoff);
|
||||
|
||||
if (cutoffItem == null) return false;
|
||||
|
||||
if (!cutoffItem.Allowed) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
197
src/Radarr.Api.V2/Profiles/Quality/QualityItemsValidator.cs
Normal file
197
src/Radarr.Api.V2/Profiles/Quality/QualityItemsValidator.cs
Normal file
|
@ -0,0 +1,197 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Validators;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace Radarr.Api.V2.Profiles.Quality
|
||||
{
|
||||
public static class QualityItemsValidator
|
||||
{
|
||||
public static IRuleBuilderOptions<T, IList<QualityProfileQualityItemResource>> ValidItems<T>(this IRuleBuilder<T, IList<QualityProfileQualityItemResource>> ruleBuilder)
|
||||
{
|
||||
ruleBuilder.SetValidator(new NotEmptyValidator(null));
|
||||
ruleBuilder.SetValidator(new AllowedValidator<T>());
|
||||
ruleBuilder.SetValidator(new QualityNameValidator<T>());
|
||||
ruleBuilder.SetValidator(new EmptyItemGroupNameValidator<T>());
|
||||
ruleBuilder.SetValidator(new ItemGroupIdValidator<T>());
|
||||
ruleBuilder.SetValidator(new UniqueIdValidator<T>());
|
||||
ruleBuilder.SetValidator(new UniqueQualityIdValidator<T>());
|
||||
return ruleBuilder.SetValidator(new ItemGroupNameValidator<T>());
|
||||
}
|
||||
}
|
||||
|
||||
public class AllowedValidator<T> : PropertyValidator
|
||||
{
|
||||
public AllowedValidator()
|
||||
: base("Must contain at least one allowed quality")
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
var list = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||
|
||||
if (list == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!list.Any(c => c.Allowed))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class EmptyItemGroupNameValidator<T> : PropertyValidator
|
||||
{
|
||||
public EmptyItemGroupNameValidator()
|
||||
: base("Groups must not be empty")
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||
|
||||
if (items.Any(i => i.Name.IsNotNullOrWhiteSpace() && i.Items.Empty()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class QualityNameValidator<T> : PropertyValidator
|
||||
{
|
||||
public QualityNameValidator()
|
||||
: base("Individual qualities should not be named")
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||
|
||||
if (items.Any(i => i.Name.IsNotNullOrWhiteSpace() && i.Quality != null))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class ItemGroupNameValidator<T> : PropertyValidator
|
||||
{
|
||||
public ItemGroupNameValidator()
|
||||
: base("Groups must have a name")
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||
|
||||
if (items.Any(i => i.Quality == null && i.Name.IsNullOrWhiteSpace()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class ItemGroupIdValidator<T> : PropertyValidator
|
||||
{
|
||||
public ItemGroupIdValidator()
|
||||
: base("Groups must have an ID")
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||
|
||||
if (items.Any(i => i.Quality == null && i.Id == 0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class UniqueIdValidator<T> : PropertyValidator
|
||||
{
|
||||
public UniqueIdValidator()
|
||||
: base("Groups must have a unique ID")
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||
|
||||
if (items.Where(i => i.Id > 0).Select(i => i.Id).GroupBy(i => i).Any(g => g.Count() > 1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class UniqueQualityIdValidator<T> : PropertyValidator
|
||||
{
|
||||
public UniqueQualityIdValidator()
|
||||
: base("Qualities can only be used once")
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||
var qualityIds = new HashSet<int>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.Id > 0)
|
||||
{
|
||||
foreach (var quality in item.Items)
|
||||
{
|
||||
if (qualityIds.Contains(quality.Quality.Id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
qualityIds.Add(quality.Quality.Id);
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
if (qualityIds.Contains(item.Quality.Id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
qualityIds.Add(item.Quality.Id);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,8 +20,10 @@ namespace Radarr.Api.V2.Profiles.Quality
|
|||
_profileService = profileService;
|
||||
_formatService = formatService;
|
||||
SharedValidator.RuleFor(c => c.Name).NotEmpty();
|
||||
SharedValidator.RuleFor(c => c.Cutoff).NotNull();
|
||||
SharedValidator.RuleFor(c => c.Items).MustHaveAllowedQuality();
|
||||
// TODO: Need to validate the cutoff is allowed and the ID/quality ID exists
|
||||
// TODO: Need to validate the Items to ensure groups have names and at no item has no name, no items and no quality
|
||||
SharedValidator.RuleFor(c => c.Cutoff).ValidCutoff();
|
||||
SharedValidator.RuleFor(c => c.Items).ValidItems();
|
||||
SharedValidator.RuleFor(c => c.Language).ValidLanguage();
|
||||
SharedValidator.RuleFor(c => c.FormatItems).Must(items =>
|
||||
{
|
||||
|
|
|
@ -11,18 +11,25 @@ namespace Radarr.Api.V2.Profiles.Quality
|
|||
public class QualityProfileResource : RestResource
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public NzbDrone.Core.Qualities.Quality Cutoff { get; set; }
|
||||
public int Cutoff { get; set; }
|
||||
public string PreferredTags { get; set; }
|
||||
public List<ProfileQualityItemResource> Items { get; set; }
|
||||
public List<QualityProfileQualityItemResource> Items { get; set; }
|
||||
public CustomFormatResource FormatCutoff { get; set; }
|
||||
public List<ProfileFormatItemResource> FormatItems { get; set; }
|
||||
public Language Language { get; set; }
|
||||
}
|
||||
|
||||
public class ProfileQualityItemResource : RestResource
|
||||
public class QualityProfileQualityItemResource : RestResource
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public NzbDrone.Core.Qualities.Quality Quality { get; set; }
|
||||
public List<QualityProfileQualityItemResource> Items { get; set; }
|
||||
public bool Allowed { get; set; }
|
||||
|
||||
public QualityProfileQualityItemResource()
|
||||
{
|
||||
Items = new List<QualityProfileQualityItemResource>();
|
||||
}
|
||||
}
|
||||
|
||||
public class ProfileFormatItemResource : RestResource
|
||||
|
@ -40,7 +47,6 @@ namespace Radarr.Api.V2.Profiles.Quality
|
|||
return new QualityProfileResource
|
||||
{
|
||||
Id = model.Id,
|
||||
|
||||
Name = model.Name,
|
||||
Cutoff = model.Cutoff,
|
||||
PreferredTags = model.PreferredTags != null ? string.Join(",", model.PreferredTags) : "",
|
||||
|
@ -51,13 +57,16 @@ namespace Radarr.Api.V2.Profiles.Quality
|
|||
};
|
||||
}
|
||||
|
||||
public static ProfileQualityItemResource ToResource(this ProfileQualityItem model)
|
||||
public static QualityProfileQualityItemResource ToResource(this ProfileQualityItem model)
|
||||
{
|
||||
if (model == null) return null;
|
||||
|
||||
return new ProfileQualityItemResource
|
||||
return new QualityProfileQualityItemResource
|
||||
{
|
||||
Id = model.Id,
|
||||
Name = model.Name,
|
||||
Quality = model.Quality,
|
||||
Items = model.Items.ConvertAll(ToResource),
|
||||
Allowed = model.Allowed
|
||||
};
|
||||
}
|
||||
|
@ -78,9 +87,8 @@ namespace Radarr.Api.V2.Profiles.Quality
|
|||
return new Profile
|
||||
{
|
||||
Id = resource.Id,
|
||||
|
||||
Name = resource.Name,
|
||||
Cutoff = (NzbDrone.Core.Qualities.Quality)resource.Cutoff.Id,
|
||||
Cutoff = resource.Cutoff,
|
||||
PreferredTags = resource.PreferredTags.Split(',').ToList(),
|
||||
Items = resource.Items.ConvertAll(ToModel),
|
||||
FormatCutoff = resource.FormatCutoff.ToModel(),
|
||||
|
@ -89,13 +97,16 @@ namespace Radarr.Api.V2.Profiles.Quality
|
|||
};
|
||||
}
|
||||
|
||||
public static ProfileQualityItem ToModel(this ProfileQualityItemResource resource)
|
||||
public static ProfileQualityItem ToModel(this QualityProfileQualityItemResource resource)
|
||||
{
|
||||
if (resource == null) return null;
|
||||
|
||||
return new ProfileQualityItem
|
||||
{
|
||||
Quality = (NzbDrone.Core.Qualities.Quality)resource.Quality.Id,
|
||||
Id = resource.Id,
|
||||
Name = resource.Name,
|
||||
Quality = resource.Quality != null ? (NzbDrone.Core.Qualities.Quality)resource.Quality.Id : null,
|
||||
Items = resource.Items.ConvertAll(ToModel),
|
||||
Allowed = resource.Allowed
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,54 +1,25 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using Radarr.Http;
|
||||
using Radarr.Http.Mapping;
|
||||
|
||||
namespace Radarr.Api.V2.Profiles.Quality
|
||||
{
|
||||
public class QualityProfileSchemaModule : RadarrRestModule<QualityProfileResource>
|
||||
{
|
||||
private readonly IQualityDefinitionService _qualityDefinitionService;
|
||||
private readonly ICustomFormatService _formatService;
|
||||
private readonly IProfileService _profileService;
|
||||
|
||||
public QualityProfileSchemaModule(IQualityDefinitionService qualityDefinitionService, ICustomFormatService formatService)
|
||||
: base("/profile/schema")
|
||||
public QualityProfileSchemaModule(IProfileService profileService)
|
||||
: base("/qualityprofile/schema")
|
||||
{
|
||||
_qualityDefinitionService = qualityDefinitionService;
|
||||
_formatService = formatService;
|
||||
_profileService = profileService;
|
||||
|
||||
GetResourceAll = GetAll;
|
||||
GetResourceSingle = GetSchema;
|
||||
}
|
||||
|
||||
private List<QualityProfileResource> GetAll()
|
||||
private QualityProfileResource GetSchema()
|
||||
{
|
||||
var items = _qualityDefinitionService.All()
|
||||
.OrderBy(v => v.Weight)
|
||||
.Select(v => new ProfileQualityItem { Quality = v.Quality, Allowed = false })
|
||||
.ToList();
|
||||
var qualityProfile = _profileService.GetDefaultProfile(string.Empty);
|
||||
|
||||
var formatItems = _formatService.All().Select(v => new ProfileFormatItem
|
||||
{
|
||||
Format = v, Allowed = true
|
||||
}).ToList();
|
||||
|
||||
formatItems.Insert(0, new ProfileFormatItem
|
||||
{
|
||||
Format = CustomFormat.None,
|
||||
Allowed = true
|
||||
});
|
||||
|
||||
var profile = new Profile();
|
||||
profile.Cutoff = NzbDrone.Core.Qualities.Quality.Unknown;
|
||||
profile.Items = items;
|
||||
profile.FormatCutoff = CustomFormat.None;
|
||||
profile.FormatItems = formatItems;
|
||||
profile.Language = Language.English;
|
||||
|
||||
return new List<QualityProfileResource> { profile.ToResource() };
|
||||
return qualityProfile.ToResource();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FluentValidation;
|
||||
using FluentValidation.Validators;
|
||||
|
||||
namespace Radarr.Api.V2.Profiles.Quality
|
||||
{
|
||||
public static class QualityProfileValidation
|
||||
{
|
||||
public static IRuleBuilderOptions<T, IList<ProfileQualityItemResource>> MustHaveAllowedQuality<T>(this IRuleBuilder<T, IList<ProfileQualityItemResource>> ruleBuilder)
|
||||
{
|
||||
ruleBuilder.SetValidator(new NotEmptyValidator(null));
|
||||
|
||||
return ruleBuilder.SetValidator(new AllowedValidator<T>());
|
||||
}
|
||||
}
|
||||
|
||||
public class AllowedValidator<T> : PropertyValidator
|
||||
{
|
||||
public AllowedValidator()
|
||||
: base("Must contain at least one allowed quality")
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
var list = context.PropertyValue as IList<ProfileQualityItemResource>;
|
||||
|
||||
if (list == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!list.Any(c => c.Allowed))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -153,10 +153,11 @@
|
|||
<Compile Include="Profiles\Delay\DelayProfileResource.cs" />
|
||||
<Compile Include="Profiles\Languages\LanguageModule.cs" />
|
||||
<Compile Include="Profiles\Languages\LanguageResource.cs" />
|
||||
<Compile Include="Profiles\Quality\QualityCutoffValidator.cs" />
|
||||
<Compile Include="Profiles\Quality\QualityItemsValidator.cs" />
|
||||
<Compile Include="Profiles\Quality\QualityProfileModule.cs" />
|
||||
<Compile Include="Profiles\Quality\QualityProfileResource.cs" />
|
||||
<Compile Include="Profiles\Quality\QualityProfileSchemaModule.cs" />
|
||||
<Compile Include="Profiles\Quality\QualityProfileValidation.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="ProviderModuleBase.cs" />
|
||||
<Compile Include="ProviderResource.cs" />
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue