Medium Support (Multi-disc Albums), Quality Grouping (#121)
* Multi Disc Stage 1 - Backend Work * Quality Group Functionality * Fixed: Only show wanted album types on ArtistDetail page * Add Media Count Column to ArtistDetail Page * Parser updates for multidisc cases, other usenet release title formats * Search for Tracks by Medium Number in Addition to Title and TrackNumber * Medium Renaming Token for Track Naming * fixup Codacy and Comment Cleanup * fixup remove commentspull/123/head
parent
e1e7cad951
commit
21428cba6f
@ -1,5 +1,7 @@
|
|||||||
|
export const EXTRA_SMALL = 'extraSmall';
|
||||||
export const SMALL = 'small';
|
export const SMALL = 'small';
|
||||||
export const MEDIUM = 'medium';
|
export const MEDIUM = 'medium';
|
||||||
export const LARGE = 'large';
|
export const LARGE = 'large';
|
||||||
|
export const EXTRA_LARGE = 'extraLarge';
|
||||||
|
|
||||||
export const all = [SMALL, MEDIUM, LARGE];
|
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE];
|
||||||
|
@ -1,3 +1,18 @@
|
|||||||
|
.formGroupsContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroupWrapper {
|
||||||
|
flex: 0 0 calc($formGroupSmallWidth - 100px);
|
||||||
|
}
|
||||||
|
|
||||||
.deleteButtonContainer {
|
.deleteButtonContainer {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointLarge) {
|
||||||
|
.formGroupsContainer {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,105 @@
|
|||||||
|
.qualityProfileItemGroup {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fafafa;
|
||||||
|
|
||||||
|
&.editGroups {
|
||||||
|
background: #fcfcfc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qualityProfileItemGroupInfo {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkInputContainer {
|
||||||
|
composes: checkInputContainer from './QualityProfileItem.css';
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkInput {
|
||||||
|
composes: checkInput from './QualityProfileItem.css';
|
||||||
|
}
|
||||||
|
|
||||||
|
.nameInput {
|
||||||
|
composes: text from 'Components/Form/TextInput.css';
|
||||||
|
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nameContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&.notAllowed {
|
||||||
|
color: #c6c6c6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.groupQualities {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 2px 0 2px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qualityNameContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-left: 2px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qualityNameLabel {
|
||||||
|
composes: qualityNameContainer;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteGroupButton {
|
||||||
|
composes: buton from 'Components/Link/IconButton.css';
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 5px;
|
||||||
|
margin-left: 8px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragHandle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
width: $dragHandleWidth;
|
||||||
|
text-align: center;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragIcon {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isDragging {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items {
|
||||||
|
margin: 0 50px 0 35px;
|
||||||
|
}
|
@ -0,0 +1,200 @@
|
|||||||
|
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 Label from 'Components/Label';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
|
import TextInput from 'Components/Form/TextInput';
|
||||||
|
import QualityProfileItemDragSource from './QualityProfileItemDragSource';
|
||||||
|
import styles from './QualityProfileItemGroup.css';
|
||||||
|
|
||||||
|
class QualityProfileItemGroup extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onAllowedChange = ({ value }) => {
|
||||||
|
const {
|
||||||
|
groupId,
|
||||||
|
onItemGroupAllowedChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onItemGroupAllowedChange(groupId, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
onNameChange = ({ value }) => {
|
||||||
|
const {
|
||||||
|
groupId,
|
||||||
|
onItemGroupNameChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onItemGroupNameChange(groupId, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteGroupPress = ({ value }) => {
|
||||||
|
const {
|
||||||
|
groupId,
|
||||||
|
onDeleteGroupPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onDeleteGroupPress(groupId, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
editGroups,
|
||||||
|
groupId,
|
||||||
|
name,
|
||||||
|
allowed,
|
||||||
|
items,
|
||||||
|
qualityIndex,
|
||||||
|
isDragging,
|
||||||
|
isDraggingUp,
|
||||||
|
isDraggingDown,
|
||||||
|
connectDragSource,
|
||||||
|
onQualityProfileItemAllowedChange,
|
||||||
|
onQualityProfileItemDragMove,
|
||||||
|
onQualityProfileItemDragEnd
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.qualityProfileItemGroup,
|
||||||
|
editGroups && styles.editGroups,
|
||||||
|
isDragging && styles.isDragging,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={styles.qualityProfileItemGroupInfo}>
|
||||||
|
{
|
||||||
|
editGroups &&
|
||||||
|
<div className={styles.qualityNameContainer}>
|
||||||
|
<IconButton
|
||||||
|
className={styles.deleteGroupButton}
|
||||||
|
name={icons.UNGROUP}
|
||||||
|
title="Ungroup"
|
||||||
|
onPress={this.onDeleteGroupPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
className={styles.nameInput}
|
||||||
|
name="name"
|
||||||
|
value={name}
|
||||||
|
onChange={this.onNameChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!editGroups &&
|
||||||
|
<label
|
||||||
|
className={styles.qualityNameLabel}
|
||||||
|
>
|
||||||
|
<CheckInput
|
||||||
|
className={styles.checkInput}
|
||||||
|
containerClassName={styles.checkInputContainer}
|
||||||
|
name="allowed"
|
||||||
|
value={allowed}
|
||||||
|
onChange={this.onAllowedChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.nameContainer}>
|
||||||
|
<div className={classNames(
|
||||||
|
styles.name,
|
||||||
|
!allowed && styles.notAllowed
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.groupQualities}>
|
||||||
|
{
|
||||||
|
items.map(({ quality }) => {
|
||||||
|
return (
|
||||||
|
<Label key={quality.id}>
|
||||||
|
{quality.name}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}).reverse()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
connectDragSource(
|
||||||
|
<div className={styles.dragHandle}>
|
||||||
|
<Icon
|
||||||
|
className={styles.dragIcon}
|
||||||
|
name={icons.REORDER}
|
||||||
|
title="Reorder"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
editGroups &&
|
||||||
|
<div className={styles.items}>
|
||||||
|
{
|
||||||
|
items.map(({ quality }, index) => {
|
||||||
|
return (
|
||||||
|
<QualityProfileItemDragSource
|
||||||
|
key={quality.id}
|
||||||
|
editGroups={editGroups}
|
||||||
|
groupId={groupId}
|
||||||
|
qualityId={quality.id}
|
||||||
|
name={quality.name}
|
||||||
|
allowed={allowed}
|
||||||
|
items={items}
|
||||||
|
qualityIndex={`${qualityIndex}.${index + 1}`}
|
||||||
|
isDragging={isDragging}
|
||||||
|
isDraggingUp={isDraggingUp}
|
||||||
|
isDraggingDown={isDraggingDown}
|
||||||
|
isInGroup={true}
|
||||||
|
onQualityProfileItemAllowedChange={onQualityProfileItemAllowedChange}
|
||||||
|
onQualityProfileItemDragMove={onQualityProfileItemDragMove}
|
||||||
|
onQualityProfileItemDragEnd={onQualityProfileItemDragEnd}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}).reverse()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QualityProfileItemGroup.propTypes = {
|
||||||
|
editGroups: PropTypes.bool,
|
||||||
|
groupId: PropTypes.number.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
allowed: PropTypes.bool.isRequired,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
qualityIndex: PropTypes.string.isRequired,
|
||||||
|
isDragging: PropTypes.bool.isRequired,
|
||||||
|
isDraggingUp: PropTypes.bool.isRequired,
|
||||||
|
isDraggingDown: PropTypes.bool.isRequired,
|
||||||
|
connectDragSource: PropTypes.func,
|
||||||
|
onItemGroupAllowedChange: PropTypes.func.isRequired,
|
||||||
|
onQualityProfileItemAllowedChange: PropTypes.func.isRequired,
|
||||||
|
onItemGroupNameChange: PropTypes.func.isRequired,
|
||||||
|
onDeleteGroupPress: PropTypes.func.isRequired,
|
||||||
|
onQualityProfileItemDragMove: PropTypes.func.isRequired,
|
||||||
|
onQualityProfileItemDragEnd: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
QualityProfileItemGroup.defaultProps = {
|
||||||
|
// The drag preview will not connect the drag handle.
|
||||||
|
connectDragSource: (node) => node
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QualityProfileItemGroup;
|
@ -1,6 +1,15 @@
|
|||||||
|
.editGroupsButton {
|
||||||
|
composes: button from 'Components/Link/Button.css';
|
||||||
|
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editGroupsButtonIcon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.qualities {
|
.qualities {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
/* TODO: This should consider the number of qualities in the list */
|
transition: min-height 200ms;
|
||||||
min-height: 550px;
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
export default function getQualities(qualities) {
|
||||||
|
if (!qualities) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return qualities.reduce((acc, item) => {
|
||||||
|
if (item.quality) {
|
||||||
|
acc.push(item.quality);
|
||||||
|
} else {
|
||||||
|
const groupQualities = item.items.map((i) => i.quality);
|
||||||
|
acc.push(...groupQualities);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
}
|
@ -1,32 +1,30 @@
|
|||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
|
||||||
export default function createAjaxRequest() {
|
export default function createAjaxRequest(ajaxOptions) {
|
||||||
return function(ajaxOptions) {
|
const requestXHR = new window.XMLHttpRequest();
|
||||||
const requestXHR = new window.XMLHttpRequest();
|
let aborted = false;
|
||||||
let aborted = false;
|
let complete = false;
|
||||||
let complete = false;
|
|
||||||
|
|
||||||
function abortRequest() {
|
function abortRequest() {
|
||||||
if (!complete) {
|
if (!complete) {
|
||||||
aborted = true;
|
aborted = true;
|
||||||
requestXHR.abort();
|
requestXHR.abort();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const request = $.ajax({
|
const request = $.ajax({
|
||||||
xhr: () => requestXHR,
|
xhr: () => requestXHR,
|
||||||
...ajaxOptions
|
...ajaxOptions
|
||||||
}).then(null, (xhr, textStatus, errorThrown) => {
|
}).then(null, (xhr, textStatus, errorThrown) => {
|
||||||
xhr.aborted = aborted;
|
xhr.aborted = aborted;
|
||||||
|
|
||||||
return $.Deferred().reject(xhr, textStatus, errorThrown).promise();
|
return $.Deferred().reject(xhr, textStatus, errorThrown).promise();
|
||||||
}).always(() => {
|
}).always(() => {
|
||||||
complete = true;
|
complete = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
request,
|
request,
|
||||||
abortRequest
|
abortRequest
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NzbDrone.Core.Music;
|
||||||
|
|
||||||
|
namespace Lidarr.Api.V1.Albums
|
||||||
|
{
|
||||||
|
public class MediumResource
|
||||||
|
{
|
||||||
|
public int MediumNumber { get; set; }
|
||||||
|
public string MediumName { get; set; }
|
||||||
|
public string MediumFormat { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SeasonResourceMapper
|
||||||
|
{
|
||||||
|
public static MediumResource ToResource(this Medium model)
|
||||||
|
{
|
||||||
|
if (model == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MediumResource
|
||||||
|
{
|
||||||
|
MediumNumber = model.Number,
|
||||||
|
MediumName = model.Name,
|
||||||
|
MediumFormat = model.Format
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Medium ToModel(this MediumResource resource)
|
||||||
|
{
|
||||||
|
if (resource == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Medium
|
||||||
|
{
|
||||||
|
Number = resource.MediumNumber,
|
||||||
|
Name = resource.MediumName,
|
||||||
|
Format = resource.MediumFormat
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<MediumResource> ToResource(this IEnumerable<Medium> models)
|
||||||
|
{
|
||||||
|
return models.Select(ToResource).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Medium> ToModel(this IEnumerable<MediumResource> resources)
|
||||||
|
{
|
||||||
|
return resources.Select(ToModel).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentValidation;
|
||||||
|
using FluentValidation.Validators;
|
||||||
|
|
||||||
|
namespace Lidarr.Api.V1.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.Id == cutoff || (i.Quality != null && i.Quality.Id == cutoff));
|
||||||
|
|
||||||
|
if (cutoffItem == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cutoffItem.Allowed)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,197 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentValidation;
|
||||||
|
using FluentValidation.Validators;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
|
||||||
|
namespace Lidarr.Api.V1.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
using FizzWare.NBuilder;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System.Linq;
|
||||||
|
using NzbDrone.Core.Languages;
|
||||||
|
using NzbDrone.Core.Profiles.Qualities;
|
||||||
|
using NzbDrone.Core.Qualities;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
using NzbDrone.Core.Music;
|
||||||
|
using NzbDrone.Core.Profiles.Languages;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.MusicTests.ArtistRepositoryTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
|
||||||
|
public class ArtistRepositoryFixture : DbTest<ArtistRepository, Artist>
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void should_lazyload_quality_profile()
|
||||||
|
{
|
||||||
|
var profile = new Profile
|
||||||
|
{
|
||||||
|
Items = Qualities.QualityFixture.GetDefaultQualities(Quality.FLAC, Quality.MP3_192, Quality.MP3_320),
|
||||||
|
|
||||||
|
Cutoff = Quality.FLAC.Id,
|
||||||
|
Name = "TestProfile"
|
||||||
|
};
|
||||||
|
|
||||||
|
var langProfile = new LanguageProfile
|
||||||
|
{
|
||||||
|
Name = "TestProfile",
|
||||||
|
Languages = Languages.LanguageFixture.GetDefaultLanguages(Language.English),
|
||||||
|
Cutoff = Language.English
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Mocker.Resolve<ProfileRepository>().Insert(profile);
|
||||||
|
Mocker.Resolve<LanguageProfileRepository>().Insert(langProfile);
|
||||||
|
|
||||||
|
var series = Builder<Artist>.CreateNew().BuildNew();
|
||||||
|
series.ProfileId = profile.Id;
|
||||||
|
series.LanguageProfileId = langProfile.Id;
|
||||||
|
|
||||||
|
Subject.Insert(series);
|
||||||
|
|
||||||
|
|
||||||
|
StoredModel.Profile.Should().NotBeNull();
|
||||||
|
StoredModel.LanguageProfile.Should().NotBeNull();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue