mirror of
https://github.com/Radarr/Radarr.git
synced 2024-08-18 08:19:38 +02:00
New: User defined scores for each Custom Format
Brings it more into line with Sonarr preferred words
This commit is contained in:
parent
da80793204
commit
50d6c5e61e
@ -63,6 +63,15 @@ const columns = [
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: 'Custom Format score'
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'indexerFlags',
|
||||
label: React.createElement(Icon, { name: icons.FLAG }),
|
||||
|
@ -20,6 +20,14 @@
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.customFormatScore {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 55px;
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.rejected,
|
||||
.indexerFlags,
|
||||
.download {
|
||||
|
@ -114,6 +114,7 @@ class InteractiveSearchRow extends Component {
|
||||
leechers,
|
||||
quality,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
languages,
|
||||
indexerFlags,
|
||||
rejections,
|
||||
@ -182,6 +183,11 @@ class InteractiveSearchRow extends Component {
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.customFormatScore}>
|
||||
{customFormatScore > 0 && `+${customFormatScore}`}
|
||||
{customFormatScore < 0 && customFormatScore}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.indexerFlags}>
|
||||
{
|
||||
!!indexerFlags.length &&
|
||||
@ -281,6 +287,7 @@ InteractiveSearchRow.propTypes = {
|
||||
leechers: PropTypes.number,
|
||||
quality: PropTypes.object.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFormatScore: PropTypes.number.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
rejections: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
|
@ -94,10 +94,18 @@ class CustomFormat extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
let kind = kinds.DEFAULT;
|
||||
if (item.required) {
|
||||
kind = kinds.SUCCESS;
|
||||
}
|
||||
if (item.negate) {
|
||||
kind = kinds.DANGER;
|
||||
}
|
||||
|
||||
return (
|
||||
<Label
|
||||
key={index}
|
||||
kind={item.required ? kinds.DANGER : kinds.DEFAULT}
|
||||
kind={kind}
|
||||
>
|
||||
{item.name}
|
||||
</Label>
|
||||
|
@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import Alert from 'Components/Alert';
|
||||
import Link from 'Components/Link/Link';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
@ -48,8 +49,8 @@ class AddSpecificationModalContent extends Component {
|
||||
<div>
|
||||
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>Radarr supports custom conditions against the following release properties</div>
|
||||
<div>Visit github for more details</div>
|
||||
<div>Radarr supports custom conditions against the release properties below.</div>
|
||||
<div>Visit <Link to='https://github.com/Radarr/Radarr/wiki/Custom-Formats-Aphrodite'>GitHub</Link> for more details.</div>
|
||||
</Alert>
|
||||
|
||||
<div className={styles.specifications}>
|
||||
|
@ -90,14 +90,14 @@ class Specification extends Component {
|
||||
|
||||
{
|
||||
negate &&
|
||||
<Label kind={kinds.INVERSE}>
|
||||
<Label kind={kinds.DANGER}>
|
||||
{'Negated'}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
required &&
|
||||
<Label kind={kinds.DANGER}>
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{'Required'}
|
||||
</Label>
|
||||
}
|
||||
|
6
frontend/src/Settings/Profiles/Profiles.css
Normal file
6
frontend/src/Settings/Profiles/Profiles.css
Normal file
@ -0,0 +1,6 @@
|
||||
.addCustomFormatMessage {
|
||||
color: $helpTextColor;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: 20px;
|
||||
}
|
@ -3,9 +3,11 @@ import { DndProvider } from 'react-dnd';
|
||||
import HTML5Backend from 'react-dnd-html5-backend';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import QualityProfilesConnector from './Quality/QualityProfilesConnector';
|
||||
import DelayProfilesConnector from './Delay/DelayProfilesConnector';
|
||||
import styles from './Profiles.css';
|
||||
// Only a single DragDrop Context can exist so it's done here to allow editing
|
||||
// quality profiles and reordering delay profiles to work.
|
||||
|
||||
@ -25,6 +27,11 @@ class Profiles extends Component {
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<QualityProfilesConnector />
|
||||
<DelayProfilesConnector />
|
||||
<div className={styles.addCustomFormatMessage}>
|
||||
Looking for Release Profiles? Try
|
||||
<Link to='/settings/customformats'> Custom Formats </Link>
|
||||
instead.
|
||||
</div>
|
||||
</DndProvider>
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
|
@ -99,7 +99,6 @@ class EditQualityProfileModalContent extends Component {
|
||||
isInUse,
|
||||
onInputChange,
|
||||
onCutoffChange,
|
||||
onFormatCutoffChange,
|
||||
onLanguageChange,
|
||||
onSavePress,
|
||||
onModalClose,
|
||||
@ -112,7 +111,8 @@ class EditQualityProfileModalContent extends Component {
|
||||
name,
|
||||
upgradeAllowed,
|
||||
cutoff,
|
||||
formatCutoff,
|
||||
minFormatScore,
|
||||
cutoffFormatScore,
|
||||
language,
|
||||
items,
|
||||
formatItems
|
||||
@ -201,19 +201,35 @@ class EditQualityProfileModalContent extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
upgradeAllowed.value &&
|
||||
formatItems.value.length > 0 &&
|
||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||
<FormLabel size={sizes.SMALL}>
|
||||
Upgrade Until Format
|
||||
Minimum Custom Format Score
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="formatCutoff"
|
||||
{...formatCutoff}
|
||||
values={customFormats}
|
||||
helpText="Once this custom format is reached Radarr will no longer download movies"
|
||||
onChange={onFormatCutoffChange}
|
||||
type={inputTypes.NUMBER}
|
||||
name="minFormatScore"
|
||||
{...minFormatScore}
|
||||
helpText="Minimum custom format score allowed to download"
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
{
|
||||
upgradeAllowed.value && formatItems.value.length > 0 &&
|
||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||
<FormLabel size={sizes.SMALL}>
|
||||
Upgrade Until Custom Format Score
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="cutoffFormatScore"
|
||||
{...cutoffFormatScore}
|
||||
helpText="Once this custom format score is reached Radarr will no longer download movies"
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
@ -320,7 +336,6 @@ EditQualityProfileModalContent.propTypes = {
|
||||
isInUse: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onCutoffChange: PropTypes.func.isRequired,
|
||||
onFormatCutoffChange: PropTypes.func.isRequired,
|
||||
onLanguageChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onContentHeightChange: PropTypes.func.isRequired,
|
||||
|
@ -70,19 +70,19 @@ function createFormatsSelector() {
|
||||
return [];
|
||||
}
|
||||
|
||||
return _.reduceRight(items.value, (result, { allowed, id, name, format }) => {
|
||||
if (allowed) {
|
||||
if (id) {
|
||||
result.push({
|
||||
key: id,
|
||||
value: name
|
||||
});
|
||||
} else {
|
||||
result.push({
|
||||
key: format,
|
||||
value: name
|
||||
});
|
||||
}
|
||||
return _.reduceRight(items.value, (result, { id, name, format, score }) => {
|
||||
if (id) {
|
||||
result.push({
|
||||
key: id,
|
||||
value: name,
|
||||
score
|
||||
});
|
||||
} else {
|
||||
result.push({
|
||||
key: format,
|
||||
value: name,
|
||||
score
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
@ -193,30 +193,6 @@ class EditQualityProfileModalContentConnector extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
ensureFormatCutoff = (qualityProfile) => {
|
||||
const cutoff = qualityProfile.formatCutoff.value;
|
||||
|
||||
const cutoffItem = _.find(qualityProfile.formatItems.value, (i) => {
|
||||
if (!cutoff) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return i.id === cutoff || (i.format === cutoff);
|
||||
});
|
||||
|
||||
// If the cutoff isn't allowed anymore or there isn't a cutoff set one
|
||||
if (!cutoff || !cutoffItem || !cutoffItem.allowed) {
|
||||
const firstAllowed = _.find(qualityProfile.formatItems.value, { allowed: true });
|
||||
let cutoffId = null;
|
||||
|
||||
if (firstAllowed) {
|
||||
cutoffId = firstAllowed.format;
|
||||
}
|
||||
|
||||
this.props.setQualityProfileValue({ name: 'formatCutoff', value: cutoffId });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
@ -239,13 +215,6 @@ class EditQualityProfileModalContentConnector extends Component {
|
||||
this.props.setQualityProfileValue({ name, value: cutoffId });
|
||||
}
|
||||
|
||||
onFormatCutoffChange = ({ name, value }) => {
|
||||
const id = parseInt(value);
|
||||
const cutoffId = _.find(this.props.item.formatItems.value, (i) => i.format === id).format;
|
||||
|
||||
this.props.setQualityProfileValue({ name, value: cutoffId });
|
||||
}
|
||||
|
||||
onLanguageChange = ({ name, value }) => {
|
||||
|
||||
const id = parseInt(value);
|
||||
@ -274,19 +243,17 @@ class EditQualityProfileModalContentConnector extends Component {
|
||||
this.ensureCutoff(qualityProfile);
|
||||
}
|
||||
|
||||
onQualityProfileFormatItemAllowedChange = (id, allowed) => {
|
||||
onQualityProfileFormatItemScoreChange = (id, score) => {
|
||||
const qualityProfile = _.cloneDeep(this.props.item);
|
||||
const formatItems = qualityProfile.formatItems.value;
|
||||
const item = _.find(qualityProfile.formatItems.value, (i) => i.format === id);
|
||||
|
||||
item.allowed = allowed;
|
||||
item.score = score;
|
||||
|
||||
this.props.setQualityProfileValue({
|
||||
name: 'formatItems',
|
||||
value: formatItems
|
||||
});
|
||||
|
||||
this.ensureFormatCutoff(qualityProfile);
|
||||
}
|
||||
|
||||
onItemGroupAllowedChange = (id, allowed) => {
|
||||
@ -505,39 +472,6 @@ class EditQualityProfileModalContentConnector extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
onQualityProfileFormatItemDragMove = (dragIndex, dropIndex) => {
|
||||
if (this.state.dragIndex !== dragIndex || this.state.dropIndex !== dropIndex) {
|
||||
this.setState({
|
||||
dragIndex,
|
||||
dropIndex
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onQualityProfileFormatItemDragEnd = ({ id }, didDrop) => {
|
||||
const {
|
||||
dragIndex,
|
||||
dropIndex
|
||||
} = this.state;
|
||||
|
||||
if (didDrop && dropIndex !== null) {
|
||||
const qualityProfile = _.cloneDeep(this.props.item);
|
||||
|
||||
const formats = qualityProfile.formatItems.value.splice(dragIndex, 1);
|
||||
qualityProfile.formatItems.value.splice(dropIndex, 0, formats[0]);
|
||||
|
||||
this.props.setQualityProfileValue({
|
||||
name: 'formatItems',
|
||||
value: qualityProfile.formatItems.value
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
dragIndex: null,
|
||||
dropIndex: null
|
||||
});
|
||||
}
|
||||
|
||||
onToggleEditGroupsMode = () => {
|
||||
this.setState({ editGroups: !this.state.editGroups });
|
||||
}
|
||||
@ -557,18 +491,15 @@ class EditQualityProfileModalContentConnector extends Component {
|
||||
onSavePress={this.onSavePress}
|
||||
onInputChange={this.onInputChange}
|
||||
onCutoffChange={this.onCutoffChange}
|
||||
onFormatCutoffChange={this.onFormatCutoffChange}
|
||||
onLanguageChange={this.onLanguageChange}
|
||||
onCreateGroupPress={this.onCreateGroupPress}
|
||||
onDeleteGroupPress={this.onDeleteGroupPress}
|
||||
onQualityProfileItemAllowedChange={this.onQualityProfileItemAllowedChange}
|
||||
onQualityProfileFormatItemAllowedChange={this.onQualityProfileFormatItemAllowedChange}
|
||||
onItemGroupAllowedChange={this.onItemGroupAllowedChange}
|
||||
onItemGroupNameChange={this.onItemGroupNameChange}
|
||||
onQualityProfileItemDragMove={this.onQualityProfileItemDragMove}
|
||||
onQualityProfileItemDragEnd={this.onQualityProfileItemDragEnd}
|
||||
onQualityProfileFormatItemDragMove={this.onQualityProfileFormatItemDragMove}
|
||||
onQualityProfileFormatItemDragEnd={this.onQualityProfileFormatItemDragEnd}
|
||||
onQualityProfileFormatItemScoreChange={this.onQualityProfileFormatItemScoreChange}
|
||||
onToggleEditGroupsMode={this.onToggleEditGroupsMode}
|
||||
/>
|
||||
);
|
||||
|
@ -1,3 +1,9 @@
|
||||
.qualityProfileFormatItemContainer {
|
||||
display: flex;
|
||||
padding: $qualityProfileItemDragSourcePadding 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.qualityProfileFormatItem {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
@ -7,38 +13,33 @@
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.checkContainer {
|
||||
position: relative;
|
||||
margin-right: 4px;
|
||||
margin-bottom: 7px;
|
||||
margin-left: 8px;
|
||||
.formatNameContainer {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
margin-bottom: 0;
|
||||
margin-left: 14px;
|
||||
width: 100%;
|
||||
font-weight: normal;
|
||||
line-height: $qualityProfileItemHeight;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.formatName {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
margin-bottom: 0;
|
||||
margin-left: 2px;
|
||||
font-weight: normal;
|
||||
line-height: 36px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dragHandle {
|
||||
.scoreContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
width: $dragHandleWidth;
|
||||
text-align: center;
|
||||
cursor: grab;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.dragIcon {
|
||||
top: 0;
|
||||
}
|
||||
.scoreInput {
|
||||
composes: input from '~Components/Form/Input.css';
|
||||
|
||||
.isDragging {
|
||||
opacity: 0.25;
|
||||
width: 100px;
|
||||
height: 30px;
|
||||
border: unset;
|
||||
border-radius: unset;
|
||||
background-color: unset;
|
||||
}
|
||||
|
@ -1,9 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import NumberInput from 'Components/Form/NumberInput';
|
||||
import styles from './QualityProfileFormatItem.css';
|
||||
|
||||
class QualityProfileFormatItem extends Component {
|
||||
@ -11,13 +8,12 @@ class QualityProfileFormatItem extends Component {
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onAllowedChange = ({ value }) => {
|
||||
onScoreChange = ({ value }) => {
|
||||
const {
|
||||
formatId,
|
||||
onQualityProfileFormatItemAllowedChange
|
||||
formatId
|
||||
} = this.props;
|
||||
|
||||
onQualityProfileFormatItemAllowedChange(formatId, value);
|
||||
this.props.onScoreChange(formatId, value);
|
||||
}
|
||||
|
||||
//
|
||||
@ -26,40 +22,32 @@ class QualityProfileFormatItem extends Component {
|
||||
render() {
|
||||
const {
|
||||
name,
|
||||
allowed,
|
||||
isDragging,
|
||||
connectDragSource
|
||||
score
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.qualityProfileFormatItem,
|
||||
isDragging && styles.isDragging
|
||||
)}
|
||||
className={styles.qualityProfileFormatItemContainer}
|
||||
>
|
||||
<label
|
||||
className={styles.formatName}
|
||||
<div
|
||||
className={styles.qualityProfileFormatItem}
|
||||
>
|
||||
<CheckInput
|
||||
containerClassName={styles.checkContainer}
|
||||
name={name}
|
||||
value={allowed}
|
||||
onChange={this.onAllowedChange}
|
||||
/>
|
||||
{name}
|
||||
</label>
|
||||
|
||||
{
|
||||
connectDragSource(
|
||||
<div className={styles.dragHandle}>
|
||||
<Icon
|
||||
className={styles.dragIcon}
|
||||
name={icons.REORDER}
|
||||
/>
|
||||
<label
|
||||
className={styles.formatNameContainer}
|
||||
>
|
||||
<div className={styles.formatName}>
|
||||
{name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<NumberInput
|
||||
containerClassName={styles.scoreContainer}
|
||||
className={styles.scoreInput}
|
||||
name={name}
|
||||
value={score}
|
||||
onChange={this.onScoreChange}
|
||||
/>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -68,16 +56,13 @@ class QualityProfileFormatItem extends Component {
|
||||
QualityProfileFormatItem.propTypes = {
|
||||
formatId: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
allowed: PropTypes.bool.isRequired,
|
||||
sortIndex: PropTypes.number.isRequired,
|
||||
isDragging: PropTypes.bool.isRequired,
|
||||
connectDragSource: PropTypes.func,
|
||||
onQualityProfileFormatItemAllowedChange: PropTypes.func
|
||||
score: PropTypes.number.isRequired,
|
||||
onScoreChange: PropTypes.func
|
||||
};
|
||||
|
||||
QualityProfileFormatItem.defaultProps = {
|
||||
// The drag preview will not connect the drag handle.
|
||||
connectDragSource: (node) => node
|
||||
// To handle the case score is deleted during edit
|
||||
score: 0
|
||||
};
|
||||
|
||||
export default QualityProfileFormatItem;
|
||||
|
@ -1,4 +0,0 @@
|
||||
.dragPreview {
|
||||
width: 380px;
|
||||
opacity: 0.75;
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { DragLayer } from 'react-dnd';
|
||||
import dimensions from 'Styles/Variables/dimensions.js';
|
||||
import { QUALITY_PROFILE_FORMAT_ITEM } from 'Helpers/dragTypes';
|
||||
import DragPreviewLayer from 'Components/DragPreviewLayer';
|
||||
import QualityProfileFormatItem from './QualityProfileFormatItem';
|
||||
import styles from './QualityProfileFormatItemDragPreview.css';
|
||||
|
||||
const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth);
|
||||
const formLabelLargeWidth = parseInt(dimensions.formLabelLargeWidth);
|
||||
const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth);
|
||||
const dragHandleWidth = parseInt(dimensions.dragHandleWidth);
|
||||
|
||||
function collectDragLayer(monitor) {
|
||||
return {
|
||||
item: monitor.getItem(),
|
||||
itemType: monitor.getItemType(),
|
||||
currentOffset: monitor.getSourceClientOffset()
|
||||
};
|
||||
}
|
||||
|
||||
class QualityProfileFormatItemDragPreview extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
item,
|
||||
itemType,
|
||||
currentOffset
|
||||
} = this.props;
|
||||
|
||||
if (!currentOffset || itemType !== QUALITY_PROFILE_FORMAT_ITEM) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The offset is shifted because the drag handle is on the right edge of the
|
||||
// list item and the preview is wider than the drag handle.
|
||||
|
||||
const { x, y } = currentOffset;
|
||||
const handleOffset = formGroupSmallWidth - formLabelLargeWidth - formLabelRightMarginWidth - dragHandleWidth;
|
||||
const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`;
|
||||
|
||||
const style = {
|
||||
position: 'absolute',
|
||||
WebkitTransform: transform,
|
||||
msTransform: transform,
|
||||
transform
|
||||
};
|
||||
|
||||
const {
|
||||
formatId,
|
||||
name,
|
||||
allowed,
|
||||
sortIndex
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<DragPreviewLayer>
|
||||
<div
|
||||
className={styles.dragPreview}
|
||||
style={style}
|
||||
>
|
||||
<QualityProfileFormatItem
|
||||
formatId={formatId}
|
||||
name={name}
|
||||
allowed={allowed}
|
||||
sortIndex={sortIndex}
|
||||
isDragging={false}
|
||||
/>
|
||||
</div>
|
||||
</DragPreviewLayer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QualityProfileFormatItemDragPreview.propTypes = {
|
||||
item: PropTypes.object,
|
||||
itemType: PropTypes.string,
|
||||
currentOffset: PropTypes.shape({
|
||||
x: PropTypes.number.isRequired,
|
||||
y: PropTypes.number.isRequired
|
||||
})
|
||||
};
|
||||
|
||||
export default DragLayer(collectDragLayer)(QualityProfileFormatItemDragPreview);
|
@ -1,18 +0,0 @@
|
||||
.qualityProfileFormatItemDragSource {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.qualityProfileFormatItemPlaceholder {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
border: 1px dotted #aaa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.qualityProfileFormatItemPlaceholderBefore {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.qualityProfileFormatItemPlaceholderAfter {
|
||||
margin-top: 8px;
|
||||
}
|
@ -1,157 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import { DragSource, DropTarget } from 'react-dnd';
|
||||
import classNames from 'classnames';
|
||||
import { QUALITY_PROFILE_FORMAT_ITEM } from 'Helpers/dragTypes';
|
||||
import QualityProfileFormatItem from './QualityProfileFormatItem';
|
||||
import styles from './QualityProfileFormatItemDragSource.css';
|
||||
|
||||
const qualityProfileFormatItemDragSource = {
|
||||
beginDrag({ formatId, name, allowed, sortIndex }) {
|
||||
return {
|
||||
formatId,
|
||||
name,
|
||||
allowed,
|
||||
sortIndex
|
||||
};
|
||||
},
|
||||
|
||||
endDrag(props, monitor, component) {
|
||||
props.onQualityProfileFormatItemDragEnd(monitor.getItem(), monitor.didDrop());
|
||||
}
|
||||
};
|
||||
|
||||
const qualityProfileFormatItemDropTarget = {
|
||||
hover(props, monitor, component) {
|
||||
const dragIndex = monitor.getItem().sortIndex;
|
||||
const hoverIndex = props.sortIndex;
|
||||
|
||||
const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
|
||||
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
||||
|
||||
// Moving up, only trigger if drag position is above 50%
|
||||
if (dragIndex < hoverIndex && hoverClientY > hoverMiddleY) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Moving down, only trigger if drag position is below 50%
|
||||
if (dragIndex > hoverIndex && hoverClientY < hoverMiddleY) {
|
||||
return;
|
||||
}
|
||||
|
||||
props.onQualityProfileFormatItemDragMove(dragIndex, hoverIndex);
|
||||
}
|
||||
};
|
||||
|
||||
function collectDragSource(connect, monitor) {
|
||||
return {
|
||||
connectDragSource: connect.dragSource(),
|
||||
isDragging: monitor.isDragging()
|
||||
};
|
||||
}
|
||||
|
||||
function collectDropTarget(connect, monitor) {
|
||||
return {
|
||||
connectDropTarget: connect.dropTarget(),
|
||||
isOver: monitor.isOver()
|
||||
};
|
||||
}
|
||||
|
||||
class QualityProfileFormatItemDragSource extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
formatId,
|
||||
name,
|
||||
allowed,
|
||||
sortIndex,
|
||||
isDragging,
|
||||
isDraggingUp,
|
||||
isDraggingDown,
|
||||
isOver,
|
||||
connectDragSource,
|
||||
connectDropTarget,
|
||||
onQualityProfileFormatItemAllowedChange
|
||||
} = this.props;
|
||||
|
||||
const isBefore = !isDragging && isDraggingUp && isOver;
|
||||
const isAfter = !isDragging && isDraggingDown && isOver;
|
||||
|
||||
// if (isDragging && !isOver) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
return connectDropTarget(
|
||||
<div
|
||||
className={classNames(
|
||||
styles.qualityProfileFormatItemDragSource,
|
||||
isBefore && styles.isDraggingUp,
|
||||
isAfter && styles.isDraggingDown
|
||||
)}
|
||||
>
|
||||
{
|
||||
isBefore &&
|
||||
<div
|
||||
className={classNames(
|
||||
styles.qualityProfileFormatItemPlaceholder,
|
||||
styles.qualityProfileFormatItemPlaceholderBefore
|
||||
)}
|
||||
/>
|
||||
}
|
||||
|
||||
<QualityProfileFormatItem
|
||||
formatId={formatId}
|
||||
name={name}
|
||||
allowed={allowed}
|
||||
sortIndex={sortIndex}
|
||||
isDragging={isDragging}
|
||||
isOver={isOver}
|
||||
connectDragSource={connectDragSource}
|
||||
onQualityProfileFormatItemAllowedChange={onQualityProfileFormatItemAllowedChange}
|
||||
/>
|
||||
|
||||
{
|
||||
isAfter &&
|
||||
<div
|
||||
className={classNames(
|
||||
styles.qualityProfileFormatItemPlaceholder,
|
||||
styles.qualityProfileFormatItemPlaceholderAfter
|
||||
)}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QualityProfileFormatItemDragSource.propTypes = {
|
||||
formatId: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
allowed: PropTypes.bool.isRequired,
|
||||
sortIndex: PropTypes.number.isRequired,
|
||||
isDragging: PropTypes.bool,
|
||||
isDraggingUp: PropTypes.bool,
|
||||
isDraggingDown: PropTypes.bool,
|
||||
isOver: PropTypes.bool,
|
||||
connectDragSource: PropTypes.func,
|
||||
connectDropTarget: PropTypes.func,
|
||||
onQualityProfileFormatItemAllowedChange: PropTypes.func.isRequired,
|
||||
onQualityProfileFormatItemDragMove: PropTypes.func.isRequired,
|
||||
onQualityProfileFormatItemDragEnd: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default DropTarget(
|
||||
QUALITY_PROFILE_FORMAT_ITEM,
|
||||
qualityProfileFormatItemDropTarget,
|
||||
collectDropTarget
|
||||
)(DragSource(
|
||||
QUALITY_PROFILE_FORMAT_ITEM,
|
||||
qualityProfileFormatItemDragSource,
|
||||
collectDragSource
|
||||
)(QualityProfileFormatItemDragSource));
|
@ -1,6 +1,31 @@
|
||||
.formats {
|
||||
margin-top: 10px;
|
||||
/* TODO: This should consider the number of languages in the list */
|
||||
min-height: 550px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.headerContainer {
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
line-height: 35px;
|
||||
}
|
||||
|
||||
.headerTitle {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.headerScore {
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
padding-left: 16px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.addCustomFormatMessage {
|
||||
max-width: $formGroupExtraSmallWidth;
|
||||
color: $helpTextColor;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
@ -1,37 +1,87 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputHelpText from 'Components/Form/FormInputHelpText';
|
||||
import QualityProfileFormatItemDragSource from './QualityProfileFormatItemDragSource';
|
||||
import QualityProfileFormatItemDragPreview from './QualityProfileFormatItemDragPreview';
|
||||
import Link from 'Components/Link/Link';
|
||||
import QualityProfileFormatItem from './QualityProfileFormatItem';
|
||||
import styles from './QualityProfileFormatItems.css';
|
||||
|
||||
function calcOrder(profileFormatItems) {
|
||||
const items = profileFormatItems.reduce((acc, cur, index) => {
|
||||
acc[cur.format] = index;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return [...profileFormatItems].sort((a, b) => {
|
||||
if (b.score !== a.score) {
|
||||
return b.score - a.score;
|
||||
}
|
||||
return a.name > b.name ? 1 : -1;
|
||||
}).map((x) => items[x.format]);
|
||||
}
|
||||
|
||||
class QualityProfileFormatItems extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
order: calcOrder(this.props.profileFormatItems)
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onScoreChange = (formatId, value) => {
|
||||
const {
|
||||
onQualityProfileFormatItemScoreChange
|
||||
} = this.props;
|
||||
|
||||
onQualityProfileFormatItemScoreChange(formatId, value);
|
||||
this.reorderItems();
|
||||
}
|
||||
|
||||
reorderItems = _.debounce(() => this.setState({ order: calcOrder(this.props.profileFormatItems) }), 1000);
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dragIndex,
|
||||
dropIndex,
|
||||
profileFormatItems,
|
||||
errors,
|
||||
warnings,
|
||||
...otherProps
|
||||
warnings
|
||||
} = this.props;
|
||||
|
||||
const isDragging = dropIndex !== null;
|
||||
const isDraggingUp = isDragging && dropIndex > dragIndex;
|
||||
const isDraggingDown = isDragging && dropIndex < dragIndex;
|
||||
const {
|
||||
order
|
||||
} = this.state;
|
||||
|
||||
if (profileFormatItems.length < 1) {
|
||||
return (
|
||||
<div className={styles.addCustomFormatMessage}>
|
||||
Want more control over which downloads are preferred? Add a
|
||||
<Link to='/settings/customformats'> Custom Format </Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormGroup>
|
||||
<FormLabel>Custom Formats</FormLabel>
|
||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||
<FormLabel size={sizes.SMALL}>
|
||||
Custom Formats
|
||||
</FormLabel>
|
||||
|
||||
<div>
|
||||
<FormInputHelpText
|
||||
text="Custom Formats higher in the list are more preferred. Only checked custom formats are wanted"
|
||||
text='Radarr scores each release using the sum of scores for matching custom formats. If a new release would improve the score, at the same or better quality, then Radarr will grab it.'
|
||||
/>
|
||||
|
||||
{
|
||||
@ -61,25 +111,32 @@ class QualityProfileFormatItems extends Component {
|
||||
}
|
||||
|
||||
<div className={styles.formats}>
|
||||
<div className={styles.headerContainer}>
|
||||
<div className={styles.headerTitle}>
|
||||
Custom Format
|
||||
</div>
|
||||
<div className={styles.headerScore}>
|
||||
Score
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
profileFormatItems.map(({ allowed, format, name }, index) => {
|
||||
order.map((index) => {
|
||||
const {
|
||||
format,
|
||||
name,
|
||||
score
|
||||
} = profileFormatItems[index];
|
||||
return (
|
||||
<QualityProfileFormatItemDragSource
|
||||
<QualityProfileFormatItem
|
||||
key={format}
|
||||
formatId={format}
|
||||
name={name}
|
||||
allowed={allowed}
|
||||
sortIndex={index}
|
||||
isDragging={isDragging}
|
||||
isDraggingUp={isDraggingUp}
|
||||
isDraggingDown={isDraggingDown}
|
||||
{...otherProps}
|
||||
score={score}
|
||||
onScoreChange={this.onScoreChange}
|
||||
/>
|
||||
);
|
||||
}).reverse()
|
||||
})
|
||||
}
|
||||
|
||||
<QualityProfileFormatItemDragPreview />
|
||||
</div>
|
||||
</div>
|
||||
</FormGroup>
|
||||
@ -88,11 +145,10 @@ class QualityProfileFormatItems extends Component {
|
||||
}
|
||||
|
||||
QualityProfileFormatItems.propTypes = {
|
||||
dragIndex: PropTypes.number,
|
||||
dropIndex: PropTypes.number,
|
||||
profileFormatItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
errors: PropTypes.arrayOf(PropTypes.object),
|
||||
warnings: PropTypes.arrayOf(PropTypes.object)
|
||||
warnings: PropTypes.arrayOf(PropTypes.object),
|
||||
onQualityProfileFormatItemScoreChange: PropTypes.func
|
||||
};
|
||||
|
||||
QualityProfileFormatItems.defaultProps = {
|
||||
|
@ -23,13 +23,10 @@ public ProfileModule(IProfileService profileService, ICustomFormatService format
|
||||
SharedValidator.RuleFor(c => c.FormatItems).Must(items =>
|
||||
{
|
||||
var all = _formatService.All().Select(f => f.Id).ToList();
|
||||
all.Add(CustomFormat.None.Id);
|
||||
var ids = items.Select(i => i.Format.Id);
|
||||
|
||||
return all.Except(ids).Empty();
|
||||
}).WithMessage("All Custom Formats and no extra ones need to be present inside your Profile! Try refreshing your browser.");
|
||||
SharedValidator.RuleFor(c => c.FormatCutoff)
|
||||
.Must(c => _formatService.All().Select(f => f.Id).Contains(c.Id) || c.Id == CustomFormat.None.Id).WithMessage("The Custom Format Cutoff must be a valid Custom Format! Try refreshing your browser.");
|
||||
|
||||
GetResourceAll = GetAll;
|
||||
GetResourceById = GetById;
|
||||
|
@ -15,7 +15,8 @@ public class ProfileResource : RestResource
|
||||
public Quality Cutoff { get; set; }
|
||||
public string PreferredTags { get; set; }
|
||||
public List<ProfileQualityItemResource> Items { get; set; }
|
||||
public CustomFormatResource FormatCutoff { get; set; }
|
||||
public int MinFormatScore { get; set; }
|
||||
public int CutoffFormatScore { get; set; }
|
||||
public List<ProfileFormatItemResource> FormatItems { get; set; }
|
||||
public Language Language { get; set; }
|
||||
}
|
||||
@ -29,7 +30,7 @@ public class ProfileQualityItemResource : RestResource
|
||||
public class ProfileFormatItemResource : RestResource
|
||||
{
|
||||
public CustomFormatResource Format { get; set; }
|
||||
public bool Allowed { get; set; }
|
||||
public int Score { get; set; }
|
||||
}
|
||||
|
||||
public static class ProfileResourceMapper
|
||||
@ -60,23 +61,6 @@ public static ProfileResource ToResource(this Profile model)
|
||||
? cutoffItem.Quality
|
||||
: cutoffItem.Items.First().Quality;
|
||||
|
||||
var formatCutoffItem = model.FormatItems.First(q =>
|
||||
{
|
||||
if (q.Id == model.FormatCutoff)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (q.Format == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return q.Format.Id == model.FormatCutoff;
|
||||
});
|
||||
|
||||
var formatCutoff = formatCutoffItem.Format;
|
||||
|
||||
return new ProfileResource
|
||||
{
|
||||
Id = model.Id,
|
||||
@ -100,7 +84,8 @@ public static ProfileResource ToResource(this Profile model)
|
||||
|
||||
return new List<ProfileQualityItemResource> { ToResource(i) };
|
||||
}).ToList(),
|
||||
FormatCutoff = formatCutoff.ToResource(),
|
||||
MinFormatScore = model.MinFormatScore,
|
||||
CutoffFormatScore = model.CutoffFormatScore,
|
||||
FormatItems = model.FormatItems.ConvertAll(ToResource),
|
||||
Language = model.Language
|
||||
};
|
||||
@ -125,7 +110,7 @@ public static ProfileFormatItemResource ToResource(this ProfileFormatItem model)
|
||||
return new ProfileFormatItemResource
|
||||
{
|
||||
Format = model.Format.ToResource(),
|
||||
Allowed = model.Allowed
|
||||
Score = model.Score,
|
||||
};
|
||||
}
|
||||
|
||||
@ -144,7 +129,8 @@ public static Profile ToModel(this ProfileResource resource)
|
||||
Cutoff = resource.Cutoff.Id,
|
||||
PreferredTags = resource.PreferredTags.Split(',').ToList(),
|
||||
Items = resource.Items.ConvertAll(ToModel),
|
||||
FormatCutoff = resource.FormatCutoff.ToModel().Id,
|
||||
MinFormatScore = resource.MinFormatScore,
|
||||
CutoffFormatScore = resource.CutoffFormatScore,
|
||||
FormatItems = resource.FormatItems.ConvertAll(ToModel),
|
||||
Language = resource.Language
|
||||
};
|
||||
@ -169,7 +155,7 @@ public static ProfileFormatItem ToModel(this ProfileFormatItemResource resource)
|
||||
return new ProfileFormatItem
|
||||
{
|
||||
Format = resource.Format.ToModel(),
|
||||
Allowed = resource.Allowed
|
||||
Score = resource.Score
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -32,21 +32,18 @@ private List<ProfileResource> GetAll()
|
||||
var formatItems = _formatService.All().Select(v => new ProfileFormatItem
|
||||
{
|
||||
Format = v,
|
||||
Allowed = true
|
||||
Score = 0
|
||||
}).ToList();
|
||||
|
||||
formatItems.Insert(0, new ProfileFormatItem
|
||||
var profile = new Profile
|
||||
{
|
||||
Format = CustomFormat.None,
|
||||
Allowed = true
|
||||
});
|
||||
|
||||
var profile = new Profile();
|
||||
profile.Cutoff = Quality.Unknown.Id;
|
||||
profile.Items = items;
|
||||
profile.FormatCutoff = CustomFormat.None.Id;
|
||||
profile.FormatItems = formatItems;
|
||||
profile.Language = Language.English;
|
||||
Cutoff = Quality.Unknown.Id,
|
||||
Items = items,
|
||||
MinFormatScore = 0,
|
||||
CutoffFormatScore = 0,
|
||||
FormatItems = formatItems,
|
||||
Language = Language.English
|
||||
};
|
||||
|
||||
return new List<ProfileResource> { profile.ToResource() };
|
||||
}
|
||||
|
@ -1,130 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Test.CustomFormats;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Qualities
|
||||
{
|
||||
[TestFixture]
|
||||
public class CustomFormatsComparerFixture : CoreTest
|
||||
{
|
||||
private CustomFormat _customFormat1;
|
||||
private CustomFormat _customFormat2;
|
||||
private CustomFormat _customFormat3;
|
||||
private CustomFormat _customFormat4;
|
||||
|
||||
public CustomFormatsComparer Subject { get; set; }
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
}
|
||||
|
||||
private void GivenDefaultProfileWithFormats()
|
||||
{
|
||||
_customFormat1 = new CustomFormat("My Format 1", new LanguageSpecification { Value = (int)Language.English }) { Id = 1 };
|
||||
_customFormat2 = new CustomFormat("My Format 2", new LanguageSpecification { Value = (int)Language.French }) { Id = 2 };
|
||||
_customFormat3 = new CustomFormat("My Format 3", new LanguageSpecification { Value = (int)Language.Spanish }) { Id = 3 };
|
||||
_customFormat4 = new CustomFormat("My Format 4", new LanguageSpecification { Value = (int)Language.Italian }) { Id = 4 };
|
||||
|
||||
CustomFormatsFixture.GivenCustomFormats(CustomFormat.None, _customFormat1, _customFormat2, _customFormat3, _customFormat4);
|
||||
|
||||
Subject = new CustomFormatsComparer(new Profile { Items = QualityFixture.GetDefaultQualities(), FormatItems = CustomFormatsFixture.GetSampleFormatItems() });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_lesser_when_first_format_is_worse()
|
||||
{
|
||||
GivenDefaultProfileWithFormats();
|
||||
|
||||
var first = new List<CustomFormat> { _customFormat1 };
|
||||
var second = new List<CustomFormat> { _customFormat2 };
|
||||
|
||||
var compare = Subject.Compare(first, second);
|
||||
|
||||
compare.Should().BeLessThan(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_zero_when_formats_are_equal()
|
||||
{
|
||||
GivenDefaultProfileWithFormats();
|
||||
|
||||
var first = new List<CustomFormat> { _customFormat2 };
|
||||
var second = new List<CustomFormat> { _customFormat2 };
|
||||
|
||||
var compare = Subject.Compare(first, second);
|
||||
|
||||
compare.Should().Be(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_greater_when_first_format_is_better()
|
||||
{
|
||||
GivenDefaultProfileWithFormats();
|
||||
|
||||
var first = new List<CustomFormat> { _customFormat3 };
|
||||
var second = new List<CustomFormat> { _customFormat2 };
|
||||
|
||||
var compare = Subject.Compare(first, second);
|
||||
|
||||
compare.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_greater_when_multiple_formats_better()
|
||||
{
|
||||
GivenDefaultProfileWithFormats();
|
||||
|
||||
var first = new List<CustomFormat> { _customFormat3, _customFormat4 };
|
||||
var second = new List<CustomFormat> { _customFormat2 };
|
||||
|
||||
var compare = Subject.Compare(first, second);
|
||||
|
||||
compare.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_greater_when_best_format_is_better()
|
||||
{
|
||||
GivenDefaultProfileWithFormats();
|
||||
|
||||
var first = new List<CustomFormat> { _customFormat1, _customFormat3 };
|
||||
var second = new List<CustomFormat> { _customFormat2 };
|
||||
|
||||
var compare = Subject.Compare(first, second);
|
||||
|
||||
compare.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_greater_when_best_format_equal_but_more_lower_formats()
|
||||
{
|
||||
GivenDefaultProfileWithFormats();
|
||||
|
||||
var first = new List<CustomFormat> { _customFormat1, _customFormat2 };
|
||||
var second = new List<CustomFormat> { _customFormat2 };
|
||||
|
||||
var compare = Subject.Compare(first, second);
|
||||
|
||||
compare.Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_be_greater_when_best_format_worse_but_more_lower_formats()
|
||||
{
|
||||
GivenDefaultProfileWithFormats();
|
||||
|
||||
var first = new List<CustomFormat> { _customFormat1, _customFormat2, _customFormat3 };
|
||||
var second = new List<CustomFormat> { _customFormat4 };
|
||||
|
||||
var compare = Subject.Compare(first, second);
|
||||
|
||||
compare.Should().BeLessThan(0);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
@ -19,19 +20,15 @@ public static void GivenCustomFormats(params CustomFormat[] formats)
|
||||
|
||||
public static List<ProfileFormatItem> GetSampleFormatItems(params string[] allowed)
|
||||
{
|
||||
return _customFormats.Select(f => new ProfileFormatItem { Format = f, Allowed = allowed.Contains(f.Name) }).ToList();
|
||||
var allowedItems = _customFormats.Where(x => allowed.Contains(x.Name)).Select((f, index) => new ProfileFormatItem { Format = f, Score = (int)Math.Pow(2, index) }).ToList();
|
||||
var disallowedItems = _customFormats.Where(x => !allowed.Contains(x.Name)).Select(f => new ProfileFormatItem { Format = f, Score = -1 * (int)Math.Pow(2, allowedItems.Count) });
|
||||
|
||||
return disallowedItems.Concat(allowedItems).ToList();
|
||||
}
|
||||
|
||||
public static List<ProfileFormatItem> GetDefaultFormatItems()
|
||||
{
|
||||
return new List<ProfileFormatItem>
|
||||
{
|
||||
new ProfileFormatItem
|
||||
{
|
||||
Allowed = true,
|
||||
Format = CustomFormat.None
|
||||
}
|
||||
};
|
||||
return new List<ProfileFormatItem>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
@ -32,8 +33,12 @@ public void Setup()
|
||||
_format2.Id = 2;
|
||||
|
||||
var fakeSeries = Builder<Movie>.CreateNew()
|
||||
.With(c => c.Profile = new Profile { Cutoff = Quality.Bluray1080p.Id })
|
||||
.Build();
|
||||
.With(c => c.Profile = new Profile
|
||||
{
|
||||
Cutoff = Quality.Bluray1080p.Id,
|
||||
MinFormatScore = 1
|
||||
})
|
||||
.Build();
|
||||
|
||||
_remoteMovie = new RemoteMovie
|
||||
{
|
||||
@ -41,32 +46,38 @@ public void Setup()
|
||||
ParsedMovieInfo = new ParsedMovieInfo { Quality = new QualityModel(Quality.DVD, new Revision(version: 2)) },
|
||||
};
|
||||
|
||||
CustomFormatsFixture.GivenCustomFormats(CustomFormat.None, _format1, _format2);
|
||||
CustomFormatsFixture.GivenCustomFormats(_format1, _format2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_allow_if_format_is_defined_in_profile()
|
||||
public void should_allow_if_format_score_greater_than_min()
|
||||
{
|
||||
_remoteMovie.CustomFormats = new List<CustomFormat> { _format1 };
|
||||
_remoteMovie.Movie.Profile.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name);
|
||||
_remoteMovie.CustomFormatScore = _remoteMovie.Movie.Profile.CalculateCustomFormatScore(_remoteMovie.CustomFormats);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_deny_if_format_is_defined_in_profile()
|
||||
public void should_deny_if_format_score_not_greater_than_min()
|
||||
{
|
||||
_remoteMovie.CustomFormats = new List<CustomFormat> { _format2 };
|
||||
_remoteMovie.Movie.Profile.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name);
|
||||
_remoteMovie.CustomFormatScore = _remoteMovie.Movie.Profile.CalculateCustomFormatScore(_remoteMovie.CustomFormats);
|
||||
|
||||
Console.WriteLine(_remoteMovie.CustomFormatScore);
|
||||
Console.WriteLine(_remoteMovie.Movie.Profile.MinFormatScore);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_deny_if_one_format_is_defined_in_profile()
|
||||
public void should_deny_if_format_score_not_greater_than_min_2()
|
||||
{
|
||||
_remoteMovie.CustomFormats = new List<CustomFormat> { _format2, _format1 };
|
||||
_remoteMovie.Movie.Profile.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name);
|
||||
_remoteMovie.CustomFormatScore = _remoteMovie.Movie.Profile.CalculateCustomFormatScore(_remoteMovie.CustomFormats);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
@ -76,24 +87,28 @@ public void should_allow_if_all_format_is_defined_in_profile()
|
||||
{
|
||||
_remoteMovie.CustomFormats = new List<CustomFormat> { _format2, _format1 };
|
||||
_remoteMovie.Movie.Profile.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name, _format2.Name);
|
||||
_remoteMovie.CustomFormatScore = _remoteMovie.Movie.Profile.CalculateCustomFormatScore(_remoteMovie.CustomFormats);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_deny_if_no_format_was_parsed_and_none_not_in_profile()
|
||||
public void should_deny_if_no_format_was_parsed_and_min_score_positive()
|
||||
{
|
||||
_remoteMovie.CustomFormats = new List<CustomFormat> { };
|
||||
_remoteMovie.Movie.Profile.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name, _format2.Name);
|
||||
_remoteMovie.CustomFormatScore = _remoteMovie.Movie.Profile.CalculateCustomFormatScore(_remoteMovie.CustomFormats);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_allow_if_no_format_was_parsed_and_none_in_profile()
|
||||
public void should_allow_if_no_format_was_parsed_min_score_is_zero()
|
||||
{
|
||||
_remoteMovie.CustomFormats = new List<CustomFormat> { };
|
||||
_remoteMovie.Movie.Profile.FormatItems = CustomFormatsFixture.GetSampleFormatItems(CustomFormat.None.Name, _format1.Name, _format2.Name);
|
||||
_remoteMovie.Movie.Profile.FormatItems = CustomFormatsFixture.GetSampleFormatItems(_format1.Name, _format2.Name);
|
||||
_remoteMovie.Movie.Profile.MinFormatScore = 0;
|
||||
_remoteMovie.CustomFormatScore = _remoteMovie.Movie.Profile.CalculateCustomFormatScore(_remoteMovie.CustomFormats);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
@ -40,9 +40,9 @@ public void Setup()
|
||||
|
||||
private void GivenProfile(Profile profile)
|
||||
{
|
||||
CustomFormatsFixture.GivenCustomFormats(CustomFormat.None);
|
||||
profile.FormatItems = CustomFormatsFixture.GetSampleFormatItems("None");
|
||||
profile.FormatCutoff = CustomFormat.None.Id;
|
||||
CustomFormatsFixture.GivenCustomFormats();
|
||||
profile.FormatItems = CustomFormatsFixture.GetSampleFormatItems();
|
||||
profile.MinFormatScore = 0;
|
||||
_remoteMovie.Movie.Profile = profile;
|
||||
|
||||
Console.WriteLine(profile.ToJson());
|
||||
@ -74,7 +74,7 @@ private void GivenCustomFormatHigher()
|
||||
{
|
||||
_customFormat = new CustomFormat("My Format", new ResolutionSpecification { Value = (int)Resolution.R1080p }) { Id = 1 };
|
||||
|
||||
CustomFormatsFixture.GivenCustomFormats(_customFormat, CustomFormat.None);
|
||||
CustomFormatsFixture.GivenCustomFormats(_customFormat);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -126,8 +126,8 @@ public void should_return_false_if_custom_formats_is_met_and_quality_and_format_
|
||||
{
|
||||
Cutoff = Quality.HDTV720p.Id,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
FormatCutoff = CustomFormat.None.Id,
|
||||
FormatItems = CustomFormatsFixture.GetSampleFormatItems("None", "My Format")
|
||||
MinFormatScore = 0,
|
||||
FormatItems = CustomFormatsFixture.GetSampleFormatItems("My Format")
|
||||
});
|
||||
|
||||
GivenFileQuality(new QualityModel(Quality.HDTV720p));
|
||||
@ -135,7 +135,7 @@ public void should_return_false_if_custom_formats_is_met_and_quality_and_format_
|
||||
|
||||
GivenCustomFormatHigher();
|
||||
|
||||
GivenOldCustomFormats(new List<CustomFormat> { CustomFormat.None });
|
||||
GivenOldCustomFormats(new List<CustomFormat>());
|
||||
GivenNewCustomFormats(new List<CustomFormat> { _customFormat });
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
|
||||
|
@ -38,7 +38,7 @@ public void Setup()
|
||||
Mocker.Resolve<UpgradableSpecification>();
|
||||
_upgradeHistory = Mocker.Resolve<HistorySpecification>();
|
||||
|
||||
CustomFormatsFixture.GivenCustomFormats(CustomFormat.None);
|
||||
CustomFormatsFixture.GivenCustomFormats();
|
||||
|
||||
_fakeMovie = Builder<Movie>.CreateNew()
|
||||
.With(c => c.Profile = new Profile
|
||||
@ -46,7 +46,7 @@ public void Setup()
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
Cutoff = Quality.Bluray1080p.Id,
|
||||
FormatItems = CustomFormatsFixture.GetSampleFormatItems("None"),
|
||||
FormatCutoff = CustomFormat.None.Id
|
||||
MinFormatScore = 0
|
||||
})
|
||||
.Build();
|
||||
|
||||
@ -162,8 +162,8 @@ public void should_not_be_upgradable_if_episode_is_of_same_quality_as_existing()
|
||||
{
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
Cutoff = Quality.Bluray1080p.Id,
|
||||
FormatItems = CustomFormatsFixture.GetSampleFormatItems("None"),
|
||||
FormatCutoff = CustomFormat.None.Id
|
||||
FormatItems = CustomFormatsFixture.GetSampleFormatItems(),
|
||||
MinFormatScore = 0
|
||||
};
|
||||
|
||||
_parseResultSingle.ParsedMovieInfo.Quality = new QualityModel(Quality.WEBDL1080p, new Revision(version: 1));
|
||||
@ -185,8 +185,8 @@ public void should_not_be_upgradable_if_cutoff_already_met()
|
||||
{
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
Cutoff = Quality.WEBDL1080p.Id,
|
||||
FormatItems = CustomFormatsFixture.GetSampleFormatItems("None"),
|
||||
FormatCutoff = CustomFormat.None.Id
|
||||
FormatItems = CustomFormatsFixture.GetSampleFormatItems(),
|
||||
MinFormatScore = 0
|
||||
};
|
||||
|
||||
_parseResultSingle.ParsedMovieInfo.Quality = new QualityModel(Quality.WEBDL1080p, new Revision(version: 1));
|
||||
@ -220,8 +220,8 @@ public void should_return_false_if_cutoff_already_met_and_cdh_is_disabled()
|
||||
{
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
Cutoff = Quality.WEBDL1080p.Id,
|
||||
FormatItems = CustomFormatsFixture.GetSampleFormatItems("None"),
|
||||
FormatCutoff = CustomFormat.None.Id
|
||||
FormatItems = CustomFormatsFixture.GetSampleFormatItems(),
|
||||
MinFormatScore = 0
|
||||
};
|
||||
|
||||
_parseResultSingle.ParsedMovieInfo.Quality = new QualityModel(Quality.Bluray1080p, new Revision(version: 1));
|
||||
|
@ -36,7 +36,7 @@ public void Setup()
|
||||
_customFormat1 = new CustomFormat("My Format 1", new LanguageSpecification { Value = (int)Language.English }) { Id = 1 };
|
||||
_customFormat2 = new CustomFormat("My Format 2", new LanguageSpecification { Value = (int)Language.French }) { Id = 2 };
|
||||
|
||||
CustomFormatsFixture.GivenCustomFormats(CustomFormat.None, _customFormat1, _customFormat2);
|
||||
CustomFormatsFixture.GivenCustomFormats(_customFormat1, _customFormat2);
|
||||
}
|
||||
|
||||
private RemoteMovie GivenRemoteMovie(QualityModel quality, int age = 0, long size = 0, DownloadProtocol downloadProtocol = DownloadProtocol.Usenet)
|
||||
@ -51,7 +51,8 @@ private RemoteMovie GivenRemoteMovie(QualityModel quality, int age = 0, long siz
|
||||
{
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
PreferredTags = new List<string> { "DTS-HD", "SPARKS" },
|
||||
FormatItems = CustomFormatsFixture.GetSampleFormatItems()
|
||||
FormatItems = CustomFormatsFixture.GetSampleFormatItems(_customFormat1.Name, _customFormat2.Name),
|
||||
MinFormatScore = 0
|
||||
})
|
||||
.With(m => m.Title = "A Movie").Build();
|
||||
|
||||
@ -62,6 +63,7 @@ private RemoteMovie GivenRemoteMovie(QualityModel quality, int age = 0, long siz
|
||||
remoteMovie.Release.Title = "A Movie 1998";
|
||||
|
||||
remoteMovie.CustomFormats = new List<CustomFormat>();
|
||||
remoteMovie.CustomFormatScore = 0;
|
||||
|
||||
return remoteMovie;
|
||||
}
|
||||
@ -328,11 +330,11 @@ public void should_prefer_better_custom_format()
|
||||
{
|
||||
var quality1 = new QualityModel(Quality.Bluray720p);
|
||||
var remoteMovie1 = GivenRemoteMovie(quality1);
|
||||
remoteMovie1.CustomFormats.Add(CustomFormat.None);
|
||||
|
||||
var quality2 = new QualityModel(Quality.Bluray720p);
|
||||
var remoteMovie2 = GivenRemoteMovie(quality2);
|
||||
remoteMovie2.CustomFormats.Add(_customFormat1);
|
||||
remoteMovie2.CustomFormatScore = remoteMovie2.Movie.Profile.CalculateCustomFormatScore(remoteMovie2.CustomFormats);
|
||||
|
||||
var decisions = new List<DownloadDecision>();
|
||||
decisions.Add(new DownloadDecision(remoteMovie1));
|
||||
@ -348,10 +350,12 @@ public void should_prefer_better_custom_format2()
|
||||
var quality1 = new QualityModel(Quality.Bluray720p);
|
||||
var remoteMovie1 = GivenRemoteMovie(quality1);
|
||||
remoteMovie1.CustomFormats.Add(_customFormat1);
|
||||
remoteMovie1.CustomFormatScore = remoteMovie1.Movie.Profile.CalculateCustomFormatScore(remoteMovie1.CustomFormats);
|
||||
|
||||
var quality2 = new QualityModel(Quality.Bluray720p);
|
||||
var remoteMovie2 = GivenRemoteMovie(quality2);
|
||||
remoteMovie2.CustomFormats.Add(_customFormat2);
|
||||
remoteMovie2.CustomFormatScore = remoteMovie2.Movie.Profile.CalculateCustomFormatScore(remoteMovie2.CustomFormats);
|
||||
|
||||
var decisions = new List<DownloadDecision>();
|
||||
decisions.Add(new DownloadDecision(remoteMovie1));
|
||||
@ -367,10 +371,12 @@ public void should_prefer_2_custom_formats()
|
||||
var quality1 = new QualityModel(Quality.Bluray720p);
|
||||
var remoteMovie1 = GivenRemoteMovie(quality1);
|
||||
remoteMovie1.CustomFormats.Add(_customFormat1);
|
||||
remoteMovie1.CustomFormatScore = remoteMovie1.Movie.Profile.CalculateCustomFormatScore(remoteMovie1.CustomFormats);
|
||||
|
||||
var quality2 = new QualityModel(Quality.Bluray720p);
|
||||
var remoteMovie2 = GivenRemoteMovie(quality2);
|
||||
remoteMovie2.CustomFormats.AddRange(new List<CustomFormat> { _customFormat1, _customFormat2 });
|
||||
remoteMovie2.CustomFormatScore = remoteMovie2.Movie.Profile.CalculateCustomFormatScore(remoteMovie2.CustomFormats);
|
||||
|
||||
var decisions = new List<DownloadDecision>();
|
||||
decisions.Add(new DownloadDecision(remoteMovie1));
|
||||
|
@ -48,7 +48,7 @@ public class QualityUpgradeSpecificationFixture : CoreTest<UpgradableSpecificati
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
CustomFormatsFixture.GivenCustomFormats(CustomFormat.None, _customFormat1, _customFormat2);
|
||||
CustomFormatsFixture.GivenCustomFormats(_customFormat1, _customFormat2);
|
||||
}
|
||||
|
||||
private void GivenAutoDownloadPropers(bool autoDownloadPropers)
|
||||
@ -73,7 +73,8 @@ public void IsUpgradeTest(Quality current,
|
||||
var profile = new Profile
|
||||
{
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
FormatItems = CustomFormatsFixture.GetSampleFormatItems()
|
||||
FormatItems = CustomFormatsFixture.GetSampleFormatItems(_customFormat1.Name, _customFormat2.Name),
|
||||
MinFormatScore = 0
|
||||
};
|
||||
|
||||
Subject.IsUpgradable(profile,
|
||||
|
@ -29,14 +29,14 @@ public void Setup()
|
||||
{
|
||||
Mocker.Resolve<UpgradableSpecification>();
|
||||
|
||||
CustomFormatsFixture.GivenCustomFormats(CustomFormat.None);
|
||||
CustomFormatsFixture.GivenCustomFormats();
|
||||
|
||||
_movie = Builder<Movie>.CreateNew()
|
||||
.With(e => e.Profile = new Profile
|
||||
{
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
FormatItems = CustomFormatsFixture.GetSampleFormatItems("None"),
|
||||
FormatCutoff = CustomFormat.None.Id,
|
||||
FormatItems = CustomFormatsFixture.GetSampleFormatItems(),
|
||||
MinFormatScore = 0,
|
||||
UpgradeAllowed = true
|
||||
})
|
||||
.Build();
|
||||
@ -48,7 +48,7 @@ public void Setup()
|
||||
_remoteMovie = Builder<RemoteMovie>.CreateNew()
|
||||
.With(r => r.Movie = _movie)
|
||||
.With(r => r.ParsedMovieInfo = new ParsedMovieInfo { Quality = new QualityModel(Quality.DVD) })
|
||||
.With(x => x.CustomFormats = new List<CustomFormat> { CustomFormat.None })
|
||||
.With(x => x.CustomFormats = new List<CustomFormat>())
|
||||
.Build();
|
||||
|
||||
Mocker.GetMock<ICustomFormatCalculationService>()
|
||||
@ -104,7 +104,7 @@ public void should_return_true_when_quality_in_queue_is_lower()
|
||||
{
|
||||
Quality = new QualityModel(Quality.SDTV)
|
||||
})
|
||||
.With(x => x.CustomFormats = new List<CustomFormat> { CustomFormat.None })
|
||||
.With(x => x.CustomFormats = new List<CustomFormat>())
|
||||
.Build();
|
||||
|
||||
GivenQueue(new List<RemoteMovie> { remoteMovie });
|
||||
|
@ -31,7 +31,7 @@ public void Setup()
|
||||
Mocker.Resolve<UpgradableSpecification>();
|
||||
_upgradeDisk = Mocker.Resolve<UpgradeDiskSpecification>();
|
||||
|
||||
CustomFormatsFixture.GivenCustomFormats(CustomFormat.None);
|
||||
CustomFormatsFixture.GivenCustomFormats();
|
||||
|
||||
_firstFile = new MovieFile { Quality = new QualityModel(Quality.Bluray1080p, new Revision(version: 2)), DateAdded = DateTime.Now };
|
||||
|
||||
@ -39,8 +39,8 @@ public void Setup()
|
||||
.With(c => c.Profile = new Profile
|
||||
{
|
||||
Cutoff = Quality.Bluray1080p.Id, Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
FormatItems = CustomFormatsFixture.GetSampleFormatItems("None"),
|
||||
FormatCutoff = CustomFormat.None.Id
|
||||
FormatItems = CustomFormatsFixture.GetSampleFormatItems(),
|
||||
MinFormatScore = 0
|
||||
})
|
||||
.With(e => e.MovieFile = _firstFile)
|
||||
.Build();
|
||||
|
@ -36,7 +36,7 @@ public void should_load_quality_profile()
|
||||
{
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(Quality.Bluray1080p, Quality.DVD, Quality.HDTV720p),
|
||||
FormatItems = CustomFormatsFixture.GetDefaultFormatItems(),
|
||||
FormatCutoff = CustomFormat.None.Id,
|
||||
MinFormatScore = 0,
|
||||
Cutoff = Quality.Bluray1080p.Id,
|
||||
Name = "TestProfile"
|
||||
};
|
||||
|
@ -22,7 +22,7 @@ public void should_be_able_to_read_and_write()
|
||||
var profile = new Profile
|
||||
{
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(Quality.Bluray1080p, Quality.DVD, Quality.HDTV720p),
|
||||
FormatCutoff = CustomFormat.None.Id,
|
||||
MinFormatScore = 0,
|
||||
FormatItems = CustomFormatsFixture.GetDefaultFormatItems(),
|
||||
Cutoff = Quality.Bluray1080p.Id,
|
||||
Name = "TestProfile"
|
||||
|
@ -17,13 +17,6 @@ public CustomFormat(string name, params ICustomFormatSpecification[] specs)
|
||||
Specifications = specs.ToList();
|
||||
}
|
||||
|
||||
public static CustomFormat None => new CustomFormat
|
||||
{
|
||||
Id = 0,
|
||||
Name = "None",
|
||||
Specifications = new List<ICustomFormatSpecification>()
|
||||
};
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public List<ICustomFormatSpecification> Specifications { get; set; }
|
||||
|
@ -1,34 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Core.Profiles;
|
||||
|
||||
namespace NzbDrone.Core.CustomFormats
|
||||
{
|
||||
public class CustomFormatsComparer : IComparer<List<CustomFormat>>
|
||||
{
|
||||
private readonly Profile _profile;
|
||||
|
||||
public CustomFormatsComparer(Profile profile)
|
||||
{
|
||||
Ensure.That(profile, () => profile).IsNotNull();
|
||||
Ensure.That(profile.Items, () => profile.Items).HasItems();
|
||||
|
||||
_profile = profile;
|
||||
}
|
||||
|
||||
public int Compare(List<CustomFormat> left, List<CustomFormat> right)
|
||||
{
|
||||
var leftIndicies = _profile.GetIndices(left);
|
||||
var rightIndicies = _profile.GetIndices(right);
|
||||
|
||||
// Summing powers of two ensures last format always trumps, but we order correctly if we
|
||||
// have extra formats lower down the list
|
||||
var leftTotal = leftIndicies.Select(x => Math.Pow(2, x)).Sum();
|
||||
var rightTotal = rightIndicies.Select(x => Math.Pow(2, x)).Sum();
|
||||
|
||||
return leftTotal.CompareTo(rightTotal);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,156 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using Dapper;
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Converters;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(169)]
|
||||
public class custom_format_scores : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("Profiles").AddColumn("MinFormatScore").AsInt32().WithDefaultValue(0);
|
||||
Alter.Table("Profiles").AddColumn("CutoffFormatScore").AsInt32().WithDefaultValue(0);
|
||||
|
||||
Execute.WithConnection(MigrateOrderToScores);
|
||||
|
||||
Delete.Column("FormatCutoff").FromTable("Profiles");
|
||||
}
|
||||
|
||||
private void MigrateOrderToScores(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<ProfileFormatItem168>>());
|
||||
SqlMapper.AddTypeHandler(new EmbeddedDocumentConverter<List<ProfileFormatItem169>>());
|
||||
|
||||
var rows = conn.Query<Profile168>("SELECT Id, FormatCutoff, FormatItems from Profiles", transaction: tran);
|
||||
var newRows = new List<Profile169>();
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
// Things ranked less than None should have a negative score
|
||||
// Things ranked higher than None have a positive score
|
||||
var allowedBelowNone = new List<ProfileFormatItem168>();
|
||||
var allowedAboveNone = new List<ProfileFormatItem168>();
|
||||
var disallowed = new List<ProfileFormatItem168>();
|
||||
|
||||
var noneEnabled = row.FormatItems.Single(x => x.Format == 0).Allowed;
|
||||
|
||||
// If none was disabled, we count everything as above none
|
||||
var foundNone = !noneEnabled;
|
||||
foreach (var item in row.FormatItems)
|
||||
{
|
||||
if (item.Format == 0)
|
||||
{
|
||||
foundNone = true;
|
||||
}
|
||||
else if (!item.Allowed)
|
||||
{
|
||||
disallowed.Add(item);
|
||||
}
|
||||
else if (foundNone)
|
||||
{
|
||||
allowedAboveNone.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
allowedBelowNone.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Set up allowed with scores 1, 2, 4, 8 etc so they replicate existing ranking behaviour
|
||||
var allowedPositive = allowedAboveNone.Select((x, index) => new ProfileFormatItem169
|
||||
{
|
||||
Format = x.Format,
|
||||
Score = (int)Math.Pow(2, index)
|
||||
}).ToList();
|
||||
|
||||
// reverse so we have most wanted first
|
||||
allowedBelowNone.Reverse();
|
||||
var allowedNegative = allowedBelowNone.Select((x, index) => new ProfileFormatItem169
|
||||
{
|
||||
Format = x.Format,
|
||||
Score = -1 * (int)Math.Pow(2, index)
|
||||
}).ToList();
|
||||
|
||||
// The minimum format score should be the minimum score achievable by the allowed formats
|
||||
// By construction, if None disabled then allowedNegative is empty and min is 1
|
||||
// If none was enabled, we could have some below None (with negative score) and
|
||||
// we should set min score negative to allow for these
|
||||
// If someone had no allowed formats and none disabled then keep minScore at 0
|
||||
// (This was a broken config that meant nothing would download)
|
||||
var minScore = 0;
|
||||
if (allowedPositive.Any() && !noneEnabled)
|
||||
{
|
||||
minScore = 1;
|
||||
}
|
||||
else if (allowedNegative.Any())
|
||||
{
|
||||
minScore = ((int)Math.Pow(2, allowedNegative.Count) * -1) + 1;
|
||||
}
|
||||
|
||||
// Previously anything matching a disabled format was banned from downloading
|
||||
// To replicate this, set score negative enough that matching a disabled format
|
||||
// must produce a score below the minimum
|
||||
var disallowedScore = (-1 * (int)Math.Pow(2, allowedPositive.Count)) + Math.Max(minScore, 0);
|
||||
var newDisallowed = disallowed.Select(x => new ProfileFormatItem169
|
||||
{
|
||||
Format = x.Format,
|
||||
Score = disallowedScore
|
||||
});
|
||||
|
||||
var newItems = newDisallowed.Concat(allowedNegative).Concat(allowedPositive).OrderBy(x => x.Score).ToList();
|
||||
|
||||
// Set the cutoff score to be the score associated with old cutoff format.
|
||||
// This can never be achieved by any combination of lesser formats given the 2^n scoring scheme
|
||||
// If the cutoff is None (Id == 0) then set cutoff score to zero
|
||||
var cutoffScore = 0;
|
||||
if (row.FormatCutoff != 0)
|
||||
{
|
||||
cutoffScore = newItems.Single(x => x.Format == row.FormatCutoff).Score;
|
||||
}
|
||||
|
||||
newRows.Add(new Profile169
|
||||
{
|
||||
Id = row.Id,
|
||||
MinFormatScore = minScore,
|
||||
CutoffFormatScore = cutoffScore,
|
||||
FormatItems = newItems
|
||||
});
|
||||
}
|
||||
|
||||
var sql = $"UPDATE Profiles SET MinFormatScore = @MinFormatScore, CutoffFormatScore = @CutoffFormatScore, FormatItems = @FormatItems WHERE Id = @Id";
|
||||
|
||||
conn.Execute(sql, newRows, transaction: tran);
|
||||
}
|
||||
|
||||
private class Profile168 : ModelBase
|
||||
{
|
||||
public int FormatCutoff { get; set; }
|
||||
public List<ProfileFormatItem168> FormatItems { get; set; }
|
||||
}
|
||||
|
||||
private class ProfileFormatItem168
|
||||
{
|
||||
public int Format { get; set; }
|
||||
public bool Allowed { get; set; }
|
||||
}
|
||||
|
||||
private class Profile169 : ModelBase
|
||||
{
|
||||
public int MinFormatScore { get; set; }
|
||||
public int CutoffFormatScore { get; set; }
|
||||
public List<ProfileFormatItem169> FormatItems { get; set; }
|
||||
}
|
||||
|
||||
private class ProfileFormatItem169
|
||||
{
|
||||
public int Format { get; set; }
|
||||
public int Score { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
@ -55,7 +55,7 @@ private int CompareAll(params int[] comparers)
|
||||
private int CompareQuality(DownloadDecision x, DownloadDecision y)
|
||||
{
|
||||
return CompareAll(CompareBy(x.RemoteMovie, y.RemoteMovie, remoteMovie => remoteMovie.Movie.Profile.GetIndex(remoteMovie.ParsedMovieInfo.Quality.Quality)),
|
||||
CompareBy(x.RemoteMovie, y.RemoteMovie, remoteMovie => remoteMovie.Movie.Profile.GetIndices(remoteMovie.CustomFormats).Select(i => Math.Pow(2, i)).Sum()),
|
||||
CompareBy(x.RemoteMovie, y.RemoteMovie, remoteMovie => remoteMovie.CustomFormatScore),
|
||||
CompareBy(x.RemoteMovie, y.RemoteMovie, remoteMovie => remoteMovie.ParsedMovieInfo.Quality.Revision.Real),
|
||||
CompareBy(x.RemoteMovie, y.RemoteMovie, remoteMovie => remoteMovie.ParsedMovieInfo.Quality.Revision.Version));
|
||||
}
|
||||
|
@ -108,6 +108,7 @@ private IEnumerable<DownloadDecision> GetDecisions(List<ReleaseInfo> reports, Se
|
||||
result.ReleaseName = report.Title;
|
||||
var remoteMovie = result.RemoteMovie;
|
||||
remoteMovie.CustomFormats = _formatCalculator.ParseCustomFormat(parsedMovieInfo);
|
||||
remoteMovie.CustomFormatScore = remoteMovie?.Movie?.Profile?.CalculateCustomFormatScore(remoteMovie.CustomFormats) ?? 0;
|
||||
remoteMovie.Release = report;
|
||||
remoteMovie.MappingResult = result.MappingResultType;
|
||||
|
||||
|
@ -1,8 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
@ -10,26 +6,17 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class CustomFormatAllowedbyProfileSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
|
||||
public CustomFormatAllowedbyProfileSpecification(Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public virtual Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
var formats = subject.CustomFormats.Any() ? subject.CustomFormats : new List<CustomFormat> { CustomFormat.None };
|
||||
_logger.Debug("Checking if report meets custom format requirements. {0}", formats.ConcatToString());
|
||||
var notAllowedFormats = subject.Movie.Profile.FormatItems.Where(v => v.Allowed == false).Select(f => f.Format).ToList();
|
||||
var notWantedFormats = notAllowedFormats.Intersect(formats);
|
||||
if (notWantedFormats.Any())
|
||||
var minScore = subject.Movie.Profile.MinFormatScore;
|
||||
var score = subject.CustomFormatScore;
|
||||
|
||||
if (score < minScore)
|
||||
{
|
||||
_logger.Debug("Custom Formats {0} rejected by Movie's profile", notWantedFormats.ConcatToString());
|
||||
return Decision.Reject("Custom Formats {0} not wanted in profile", notWantedFormats.ConcatToString());
|
||||
return Decision.Reject("Custom Formats {0} have score {1} below Movie's minimum {2}", subject.CustomFormats.ConcatToString(), score, minScore);
|
||||
}
|
||||
|
||||
return Decision.Accept();
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
@ -55,9 +54,10 @@ public bool IsUpgradable(Profile profile, QualityModel currentQuality, List<Cust
|
||||
return true;
|
||||
}
|
||||
|
||||
var customFormatCompare = new CustomFormatsComparer(profile).Compare(newCustomFormats, currentCustomFormats);
|
||||
var currentFormatScore = profile.CalculateCustomFormatScore(currentCustomFormats);
|
||||
var newFormatScore = profile.CalculateCustomFormatScore(newCustomFormats);
|
||||
|
||||
if (customFormatCompare <= 0)
|
||||
if (newFormatScore <= currentFormatScore)
|
||||
{
|
||||
_logger.Debug("New item's custom formats [{0}] do not improve on [{1}], skipping",
|
||||
newCustomFormats.ConcatToString(),
|
||||
@ -88,15 +88,8 @@ public bool QualityCutoffNotMet(Profile profile, QualityModel currentQuality, Qu
|
||||
|
||||
private bool CustomFormatCutoffNotMet(Profile profile, List<CustomFormat> currentFormats)
|
||||
{
|
||||
var cutoff = new List<CustomFormat> { profile.FormatItems.Single(x => x.Format.Id == profile.FormatCutoff).Format };
|
||||
var cutoffCompare = new CustomFormatsComparer(profile).Compare(currentFormats, cutoff);
|
||||
|
||||
if (cutoffCompare < 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
var score = profile.CalculateCustomFormatScore(currentFormats);
|
||||
return score < profile.CutoffFormatScore;
|
||||
}
|
||||
|
||||
public bool CutoffNotMet(Profile profile, QualityModel currentQuality, List<CustomFormat> currentFormats, QualityModel newQuality = null)
|
||||
@ -120,7 +113,7 @@ public bool IsRevisionUpgrade(QualityModel currentQuality, QualityModel newQuali
|
||||
public bool IsUpgradeAllowed(Profile qualityProfile, QualityModel currentQuality, List<CustomFormat> currentCustomFormats, QualityModel newQuality, List<CustomFormat> newCustomFormats)
|
||||
{
|
||||
var isQualityUpgrade = new QualityModelComparer(qualityProfile).Compare(newQuality, currentQuality) > 0;
|
||||
var isCustomFormatUpgrade = new CustomFormatsComparer(qualityProfile).Compare(newCustomFormats, currentCustomFormats) > 0;
|
||||
var isCustomFormatUpgrade = qualityProfile.CalculateCustomFormatScore(newCustomFormats) > qualityProfile.CalculateCustomFormatScore(currentCustomFormats);
|
||||
|
||||
if ((isQualityUpgrade || isCustomFormatUpgrade) && qualityProfile.UpgradeAllowed)
|
||||
{
|
||||
|
@ -10,6 +10,7 @@ public class RemoteMovie
|
||||
public ReleaseInfo Release { get; set; }
|
||||
public ParsedMovieInfo ParsedMovieInfo { get; set; }
|
||||
public List<CustomFormat> CustomFormats { get; set; }
|
||||
public int CustomFormatScore { get; set; }
|
||||
public Movie Movie { get; set; }
|
||||
public MappingResultType MappingResult { get; set; }
|
||||
public bool DownloadAllowed { get; set; }
|
||||
|
@ -17,7 +17,8 @@ public Profile()
|
||||
public string Name { get; set; }
|
||||
public int Cutoff { get; set; }
|
||||
public List<ProfileQualityItem> Items { get; set; }
|
||||
public int FormatCutoff { get; set; }
|
||||
public int MinFormatScore { get; set; }
|
||||
public int CutoffFormatScore { get; set; }
|
||||
public List<ProfileFormatItem> FormatItems { get; set; }
|
||||
public List<string> PreferredTags { get; set; }
|
||||
public Language Language { get; set; }
|
||||
@ -75,10 +76,9 @@ public QualityIndex GetIndex(int id)
|
||||
return new QualityIndex();
|
||||
}
|
||||
|
||||
public List<int> GetIndices(List<CustomFormat> formats)
|
||||
public int CalculateCustomFormatScore(List<CustomFormat> formats)
|
||||
{
|
||||
var allFormats = formats.Any() ? formats : new List<CustomFormat> { CustomFormat.None };
|
||||
return allFormats.Select(f => FormatItems.FindIndex(v => Equals(v.Format, f))).ToList();
|
||||
return FormatItems.Where(x => formats.Contains(x.Format)).Sum(x => x.Score);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,6 @@ public class ProfileFormatItem : IEmbeddedDocument
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public int Id { get; set; }
|
||||
public CustomFormat Format { get; set; }
|
||||
public bool Allowed { get; set; }
|
||||
public int Score { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ protected override IEnumerable<Profile> GetResults(SqlBuilder.Template sql)
|
||||
{
|
||||
foreach (var formatItem in profile.FormatItems)
|
||||
{
|
||||
formatItem.Format = formatItem.Format.Id == 0 ? CustomFormat.None : cfs[formatItem.Format.Id];
|
||||
formatItem.Format = cfs[formatItem.Format.Id];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,7 +89,7 @@ public void Handle(CustomFormatAddedEvent message)
|
||||
{
|
||||
profile.FormatItems.Insert(0, new ProfileFormatItem
|
||||
{
|
||||
Allowed = false,
|
||||
Score = 0,
|
||||
Format = message.CustomFormat
|
||||
});
|
||||
|
||||
@ -103,9 +103,10 @@ public void Handle(CustomFormatDeletedEvent message)
|
||||
foreach (var profile in all)
|
||||
{
|
||||
profile.FormatItems = profile.FormatItems.Where(c => c.Format.Id != message.CustomFormat.Id).ToList();
|
||||
if (profile.FormatCutoff == message.CustomFormat.Id)
|
||||
if (!profile.FormatItems.Any())
|
||||
{
|
||||
profile.FormatCutoff = CustomFormat.None.Id;
|
||||
profile.MinFormatScore = 0;
|
||||
profile.CutoffFormatScore = 0;
|
||||
}
|
||||
|
||||
Update(profile);
|
||||
@ -243,20 +244,12 @@ public Profile GetDefaultProfile(string name, Quality cutoff = null, params Qual
|
||||
groupId++;
|
||||
}
|
||||
|
||||
var formatItems = new List<ProfileFormatItem>
|
||||
{
|
||||
new ProfileFormatItem
|
||||
{
|
||||
Id = 0,
|
||||
Allowed = true,
|
||||
Format = CustomFormat.None
|
||||
}
|
||||
}.Concat(_formatService.All().Select(format => new ProfileFormatItem
|
||||
var formatItems = _formatService.All().Select(format => new ProfileFormatItem
|
||||
{
|
||||
Id = format.Id,
|
||||
Allowed = false,
|
||||
Score = 0,
|
||||
Format = format
|
||||
})).ToList();
|
||||
}).ToList();
|
||||
|
||||
var qualityProfile = new Profile
|
||||
{
|
||||
@ -264,7 +257,8 @@ public Profile GetDefaultProfile(string name, Quality cutoff = null, params Qual
|
||||
Cutoff = profileCutoff,
|
||||
Items = items,
|
||||
Language = Language.English,
|
||||
FormatCutoff = CustomFormat.None.Id,
|
||||
MinFormatScore = 0,
|
||||
CutoffFormatScore = 0,
|
||||
FormatItems = formatItems
|
||||
};
|
||||
|
||||
|
@ -94,7 +94,7 @@ private IEnumerable<ICustomFormatSpecification> GetPresets()
|
||||
yield return new ReleaseTitleSpecification
|
||||
{
|
||||
Name = "Simple Hardcoded Subs",
|
||||
Value = @"C_RX_subs?"
|
||||
Value = @"subs?"
|
||||
};
|
||||
|
||||
yield return new ReleaseTitleSpecification
|
||||
|
@ -34,11 +34,6 @@ public static List<CustomFormatResource> ToResource(this IEnumerable<CustomForma
|
||||
|
||||
public static CustomFormat ToModel(this CustomFormatResource resource, List<ICustomFormatSpecification> specifications)
|
||||
{
|
||||
if (resource.Id == 0 && resource.Name == "None")
|
||||
{
|
||||
return CustomFormat.None;
|
||||
}
|
||||
|
||||
return new CustomFormat
|
||||
{
|
||||
Id = resource.Id,
|
||||
|
@ -17,6 +17,7 @@ public class ReleaseResource : RestResource
|
||||
public string Guid { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public List<CustomFormatResource> CustomFormats { get; set; }
|
||||
public int CustomFormatScore { get; set; }
|
||||
public int QualityWeight { get; set; }
|
||||
public int Age { get; set; }
|
||||
public double AgeHours { get; set; }
|
||||
@ -71,6 +72,7 @@ public static ReleaseResource ToResource(this DownloadDecision model)
|
||||
Guid = releaseInfo.Guid,
|
||||
Quality = parsedMovieInfo.Quality,
|
||||
CustomFormats = remoteMovie.CustomFormats.ToResource(),
|
||||
CustomFormatScore = remoteMovie.CustomFormatScore,
|
||||
|
||||
//QualityWeight
|
||||
Age = releaseInfo.Age,
|
||||
|
@ -26,13 +26,17 @@ public QualityProfileModule(IProfileService profileService, ICustomFormatService
|
||||
SharedValidator.RuleFor(c => c.FormatItems).Must(items =>
|
||||
{
|
||||
var all = _formatService.All().Select(f => f.Id).ToList();
|
||||
all.Add(CustomFormat.None.Id);
|
||||
var ids = items.Select(i => i.Format);
|
||||
|
||||
return all.Except(ids).Empty();
|
||||
}).WithMessage("All Custom Formats and no extra ones need to be present inside your Profile! Try refreshing your browser.");
|
||||
SharedValidator.RuleFor(c => c.FormatCutoff)
|
||||
.Must(c => _formatService.All().Select(f => f.Id).Contains(c) || c == CustomFormat.None.Id).WithMessage("The Custom Format Cutoff must be a valid Custom Format! Try refreshing your browser.");
|
||||
SharedValidator.RuleFor(c => c).Custom((profile, context) =>
|
||||
{
|
||||
if (profile.FormatItems.Sum(x => x.Score) < profile.MinFormatScore)
|
||||
{
|
||||
context.AddFailure("Minimum Custom Format Score can never be satisfied");
|
||||
}
|
||||
});
|
||||
|
||||
GetResourceAll = GetAll;
|
||||
GetResourceById = GetById;
|
||||
|
@ -15,29 +15,30 @@ public class QualityProfileResource : RestResource
|
||||
public int Cutoff { get; set; }
|
||||
public string PreferredTags { get; set; }
|
||||
public List<QualityProfileQualityItemResource> Items { get; set; }
|
||||
public int FormatCutoff { get; set; }
|
||||
public int MinFormatScore { get; set; }
|
||||
public int CutoffFormatScore { get; set; }
|
||||
public List<ProfileFormatItemResource> FormatItems { get; set; }
|
||||
public Language Language { get; set; }
|
||||
}
|
||||
|
||||
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 string Name { get; set; }
|
||||
public NzbDrone.Core.Qualities.Quality Quality { get; set; }
|
||||
public List<QualityProfileQualityItemResource> Items { get; set; }
|
||||
public bool Allowed { get; set; }
|
||||
}
|
||||
|
||||
public class ProfileFormatItemResource : RestResource
|
||||
{
|
||||
public int Format { get; set; }
|
||||
public string Name { get; set; }
|
||||
public bool Allowed { get; set; }
|
||||
public int Score { get; set; }
|
||||
}
|
||||
|
||||
public static class ProfileResourceMapper
|
||||
@ -57,7 +58,8 @@ public static QualityProfileResource ToResource(this Profile model)
|
||||
Cutoff = model.Cutoff,
|
||||
PreferredTags = model.PreferredTags != null ? string.Join(",", model.PreferredTags) : "",
|
||||
Items = model.Items.ConvertAll(ToResource),
|
||||
FormatCutoff = model.FormatCutoff,
|
||||
MinFormatScore = model.MinFormatScore,
|
||||
CutoffFormatScore = model.CutoffFormatScore,
|
||||
FormatItems = model.FormatItems.ConvertAll(ToResource),
|
||||
Language = model.Language
|
||||
};
|
||||
@ -86,7 +88,7 @@ public static ProfileFormatItemResource ToResource(this ProfileFormatItem model)
|
||||
{
|
||||
Format = model.Format.Id,
|
||||
Name = model.Format.Name,
|
||||
Allowed = model.Allowed
|
||||
Score = model.Score
|
||||
};
|
||||
}
|
||||
|
||||
@ -105,7 +107,8 @@ public static Profile ToModel(this QualityProfileResource resource)
|
||||
Cutoff = resource.Cutoff,
|
||||
PreferredTags = resource.PreferredTags.Split(',').ToList(),
|
||||
Items = resource.Items.ConvertAll(ToModel),
|
||||
FormatCutoff = resource.FormatCutoff,
|
||||
MinFormatScore = resource.MinFormatScore,
|
||||
CutoffFormatScore = resource.CutoffFormatScore,
|
||||
FormatItems = resource.FormatItems.ConvertAll(ToModel),
|
||||
Language = resource.Language
|
||||
};
|
||||
@ -133,7 +136,7 @@ public static ProfileFormatItem ToModel(this ProfileFormatItemResource resource)
|
||||
return new ProfileFormatItem
|
||||
{
|
||||
Format = new CustomFormat { Id = resource.Format },
|
||||
Allowed = resource.Allowed
|
||||
Score = resource.Score
|
||||
};
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user