1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-05 13:20:20 -05:00

Compare commits

...

6 Commits

Author SHA1 Message Date
Mark McDowall
974c4a601b Show save error in UI 2024-09-06 20:53:55 -07:00
Mark McDowall
9549038121 Convert QualityDefinitionLimits to static 2024-09-06 20:35:19 -07:00
Robert Dailey
2f193ac58a Add API validation and tests for quality limits 2024-09-06 09:32:11 -05:00
Robert Dailey
e893ca4f1c Support validation of collections in RestController 2024-09-06 09:32:11 -05:00
Robert Dailey
039d7775ed feat: Shift quality definition limits management to the backend
This update moves the minimum, maximum, and preferred quality limits to
the backend, accessible via the new `/qualitydefinition/limits`
endpoint. This change improves support for unofficial Sonarr API clients
and enables a more flexible frontend.
2024-09-06 09:32:11 -05:00
Robert Dailey
87bd5e62f2 Add .idea directory to gitignore
For users of the Jetbrains IDEs, the `.idea` directory isn't strictly
necessary for version control. It's better to ignore it than tie a repo
to specific tooling.
2024-09-06 09:32:11 -05:00
11 changed files with 196 additions and 4 deletions

3
.gitignore vendored
View File

@@ -162,3 +162,6 @@ src/.idea/
# API doc generation
.config/
# Ignore Jetbrains IntelliJ Workspace Directories
.idea/

View File

@@ -1,3 +1,7 @@
.saveError {
margin-bottom: 20px;
}
.header {
display: flex;
font-weight: bold;

View File

@@ -5,6 +5,7 @@ interface CssExports {
'header': string;
'megabytesPerMinute': string;
'quality': string;
'saveError': string;
'sizeLimit': string;
'sizeLimitHelpText': string;
'sizeLimitHelpTextContainer': string;

View File

@@ -1,7 +1,9 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import PageSectionContent from 'Components/Page/PageSectionContent';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import QualityDefinitionConnector from './QualityDefinitionConnector';
import styles from './QualityDefinitions.css';
@@ -15,15 +17,44 @@ class QualityDefinitions extends Component {
const {
items,
advancedSettings,
saveError,
...otherProps
} = this.props;
console.log(saveError);
return (
<FieldSet legend={translate('QualityDefinitions')}>
<PageSectionContent
errorMessage={translate('QualityDefinitionsLoadError')}
{...otherProps}
>
{
saveError ?
<div className={styles.saveError}>
<Alert kind={kinds.DANGER}>
{translate('QualityDefinitionsSaveError')}
<ul>
{
Array.isArray(saveError.responseJSON) ?
saveError.responseJSON.map((error, index) => {
return (
<li key={index}>
{error.errorMessage}
</li>
);
}) :
<li>
{
JSON.stringify(saveError.responseJSON)
}
</li>
}
</ul>
</Alert>
</div> : null
}
<div className={styles.header}>
<div className={styles.quality}>
{translate('Quality')}
@@ -72,6 +103,7 @@ class QualityDefinitions extends Component {
QualityDefinitions.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
saveError: PropTypes.object,
defaultProfile: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
advancedSettings: PropTypes.bool.isRequired

View File

@@ -0,0 +1,92 @@
using FluentValidation.TestHelper;
using NUnit.Framework;
using NzbDrone.Core.Qualities;
using Sonarr.Api.V3.Qualities;
namespace NzbDrone.Api.Test.v3.Qualities;
[Parallelizable(ParallelScope.All)]
public class QualityDefinitionResourceValidatorTests
{
private readonly QualityDefinitionResourceValidator _validator = new ();
[Test]
public void Validate_fails_when_min_size_is_below_min_limit()
{
var resource = new QualityDefinitionResource { MinSize = QualityDefinitionLimits.Min - 1 };
var result = _validator.TestValidate(resource);
result.ShouldHaveValidationErrorFor(r => r.MinSize)
.WithErrorCode("GreaterThanOrEqualTo");
}
[Test]
public void Validate_fails_when_min_size_is_above_preferred_size()
{
var resource = new QualityDefinitionResource
{
MinSize = 10,
PreferredSize = 5
};
var result = _validator.TestValidate(resource);
result.ShouldHaveValidationErrorFor(r => r.MinSize)
.WithErrorCode("LessThanOrEqualTo");
}
[Test]
public void Validate_passes_when_min_size_is_within_limits()
{
var resource = new QualityDefinitionResource
{
MinSize = QualityDefinitionLimits.Min,
PreferredSize = QualityDefinitionLimits.Max
};
var result = _validator.TestValidate(resource);
result.ShouldNotHaveAnyValidationErrors();
}
[Test]
public void Validate_fails_when_max_size_is_below_preferred_size()
{
var resource = new QualityDefinitionResource
{
MaxSize = 5,
PreferredSize = 10
};
var result = _validator.TestValidate(resource);
result.ShouldHaveValidationErrorFor(r => r.MaxSize)
.WithErrorCode("GreaterThanOrEqualTo");
}
[Test]
public void Validate_fails_when_max_size_exceeds_max_limit()
{
var resource = new QualityDefinitionResource { MaxSize = QualityDefinitionLimits.Max + 1 };
var result = _validator.TestValidate(resource);
result.ShouldHaveValidationErrorFor(r => r.MaxSize)
.WithErrorCode("LessThanOrEqualTo");
}
[Test]
public void Validate_passes_when_max_size_is_within_limits()
{
var resource = new QualityDefinitionResource
{
MaxSize = QualityDefinitionLimits.Max,
PreferredSize = QualityDefinitionLimits.Min
};
var result = _validator.TestValidate(resource);
result.ShouldNotHaveAnyValidationErrors();
}
}

View File

@@ -1591,6 +1591,7 @@
"QualityCutoffNotMet": "Quality cutoff has not been met",
"QualityDefinitions": "Quality Definitions",
"QualityDefinitionsLoadError": "Unable to load Quality Definitions",
"QualityDefinitionsSaveError": "Unable to save Quality Definitions",
"QualityLimitsSeriesRuntimeHelpText": "Limits are automatically adjusted for the series runtime and number of episodes in the file.",
"QualityProfile": "Quality Profile",
"QualityProfileInUseSeriesListCollection": "Can't delete a quality profile that is attached to a series, list, or collection",

View File

@@ -0,0 +1,7 @@
namespace NzbDrone.Core.Qualities;
public static class QualityDefinitionLimits
{
public const int Min = 0;
public const int Max = 1000;
}

View File

@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Api.V3.Qualities;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Qualities;
@@ -20,6 +21,9 @@ namespace Sonarr.Api.V3.Qualities
: base(signalRBroadcaster)
{
_qualityDefinitionService = qualityDefinitionService;
SharedValidator.RuleFor(c => c)
.SetValidator(new QualityDefinitionResourceValidator());
}
[RestPutById]
@@ -55,6 +59,12 @@ namespace Sonarr.Api.V3.Qualities
.ToResource());
}
[HttpGet("limits")]
public ActionResult<QualityDefinitionLimitsResource> GetLimits()
{
return Ok(new QualityDefinitionLimitsResource());
}
[NonAction]
public void Handle(CommandExecutedEvent message)
{

View File

@@ -0,0 +1,10 @@
using NzbDrone.Core.Qualities;
namespace NzbDrone.Api.V3.Qualities
{
public class QualityDefinitionLimitsResource
{
public int Min { get; set; } = QualityDefinitionLimits.Min;
public int Max { get; set; } = QualityDefinitionLimits.Max;
}
}

View File

@@ -0,0 +1,28 @@
using FluentValidation;
using NzbDrone.Core.Qualities;
namespace Sonarr.Api.V3.Qualities;
public class QualityDefinitionResourceValidator : AbstractValidator<QualityDefinitionResource>
{
public QualityDefinitionResourceValidator()
{
When(c => c.MinSize is not null, () =>
{
RuleFor(c => c.MinSize)
.GreaterThanOrEqualTo(QualityDefinitionLimits.Min)
.WithErrorCode("GreaterThanOrEqualTo")
.LessThanOrEqualTo(c => c.PreferredSize ?? QualityDefinitionLimits.Max)
.WithErrorCode("LessThanOrEqualTo");
});
When(c => c.MaxSize is not null, () =>
{
RuleFor(c => c.MaxSize)
.GreaterThanOrEqualTo(c => c.PreferredSize ?? QualityDefinitionLimits.Min)
.WithErrorCode("GreaterThanOrEqualTo")
.LessThanOrEqualTo(QualityDefinitionLimits.Max)
.WithErrorCode("LessThanOrEqualTo");
});
}
}

View File

@@ -70,11 +70,15 @@ namespace Sonarr.Http.REST
var skipValidate = skipAttribute?.Skip ?? false;
var skipShared = skipAttribute?.SkipShared ?? false;
if (Request.Method == "POST" || Request.Method == "PUT")
if (Request.Method is "POST" or "PUT")
{
var resourceArgs = context.ActionArguments.Values.Where(x => x.GetType() == typeof(TResource))
.Select(x => x as TResource)
.ToList();
var resourceArgs = context.ActionArguments.Values
.SelectMany(x => x switch
{
TResource single => new[] { single },
IEnumerable<TResource> multiple => multiple,
_ => Enumerable.Empty<TResource>()
});
foreach (var resource in resourceArgs)
{