diff --git a/.gitignore b/.gitignore
index e633126a6c..95a3b62a66 100644
--- a/.gitignore
+++ b/.gitignore
@@ -165,15 +165,12 @@ Thumbs.db
/tools/Addins/*
packages.config.md5sum
-
-# Common IntelliJ Platform excludes
-
-# Ignore Rider projects completely for now
-.idea/
-
# ignore node_modules symlink
node_modules
node_modules.nosync
# API doc generation
.config/
+
+# Ignore Jetbrains IntelliJ Workspace Directories
+.idea/
diff --git a/src/NzbDrone.Api.Test/Radarr.Api.Test.csproj b/src/NzbDrone.Api.Test/Radarr.Api.Test.csproj
index 9bfecde2cb..d77de079c1 100644
--- a/src/NzbDrone.Api.Test/Radarr.Api.Test.csproj
+++ b/src/NzbDrone.Api.Test/Radarr.Api.Test.csproj
@@ -8,6 +8,7 @@
+
diff --git a/src/NzbDrone.Api.Test/v3/Qualities/QualityDefinitionResourceValidatorTest.cs b/src/NzbDrone.Api.Test/v3/Qualities/QualityDefinitionResourceValidatorTest.cs
new file mode 100644
index 0000000000..b8ae42d4a9
--- /dev/null
+++ b/src/NzbDrone.Api.Test/v3/Qualities/QualityDefinitionResourceValidatorTest.cs
@@ -0,0 +1,198 @@
+using FluentValidation.TestHelper;
+using NUnit.Framework;
+using NzbDrone.Core.Qualities;
+using Radarr.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,
+ PreferredSize = null,
+ MaxSize = null
+ };
+
+ var result = _validator.TestValidate(resource);
+
+ result.ShouldHaveValidationErrorFor(r => r.MinSize)
+ .WithErrorCode("GreaterThanOrEqualTo");
+ }
+
+ [Test]
+ public void Validate_fails_when_min_size_is_above_preferred_size_and_below_limit()
+ {
+ var resource = new QualityDefinitionResource
+ {
+ MinSize = 10,
+ PreferredSize = 5,
+ MaxSize = null
+ };
+
+ var result = _validator.TestValidate(resource);
+
+ result.ShouldHaveValidationErrorFor(r => r.MinSize)
+ .WithErrorCode("LessThanOrEqualTo");
+
+ result.ShouldHaveValidationErrorFor(r => r.PreferredSize)
+ .WithErrorCode("GreaterThanOrEqualTo");
+ }
+
+ [Test]
+ public void Validate_passes_when_min_size_is_within_limits()
+ {
+ var resource = new QualityDefinitionResource
+ {
+ MinSize = QualityDefinitionLimits.Min,
+ PreferredSize = null,
+ MaxSize = null
+ };
+
+ var result = _validator.TestValidate(resource);
+
+ result.ShouldNotHaveAnyValidationErrors();
+ }
+
+ [Test]
+ public void Validate_fails_when_max_size_is_below_preferred_size_and_above_limit()
+ {
+ var resource = new QualityDefinitionResource
+ {
+ MinSize = null,
+ PreferredSize = 10,
+ MaxSize = 5
+ };
+
+ var result = _validator.TestValidate(resource);
+
+ result.ShouldHaveValidationErrorFor(r => r.MaxSize)
+ .WithErrorCode("GreaterThanOrEqualTo");
+
+ result.ShouldHaveValidationErrorFor(r => r.PreferredSize)
+ .WithErrorCode("LessThanOrEqualTo");
+ }
+
+ [Test]
+ public void Validate_fails_when_max_size_exceeds_max_limit()
+ {
+ var resource = new QualityDefinitionResource
+ {
+ MinSize = null,
+ PreferredSize = null,
+ 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
+ {
+ MinSize = null,
+ PreferredSize = null,
+ MaxSize = QualityDefinitionLimits.Max
+ };
+
+ var result = _validator.TestValidate(resource);
+
+ result.ShouldNotHaveAnyValidationErrors();
+ }
+
+ [Test]
+ public void Validate_fails_when_preferred_size_is_below_min_size_and_above_max_size()
+ {
+ var resource = new QualityDefinitionResource
+ {
+ MinSize = 10,
+ PreferredSize = 7,
+ MaxSize = 5
+ };
+
+ var result = _validator.TestValidate(resource);
+
+ result.ShouldHaveValidationErrorFor(r => r.PreferredSize)
+ .WithErrorCode("GreaterThanOrEqualTo");
+
+ result.ShouldHaveValidationErrorFor(r => r.MaxSize)
+ .WithErrorCode("GreaterThanOrEqualTo");
+ }
+
+ [Test]
+ public void Validate_passes_when_preferred_size_is_null_and_other_sizes_are_valid()
+ {
+ var resource = new QualityDefinitionResource
+ {
+ MinSize = 5,
+ PreferredSize = null,
+ MaxSize = 10
+ };
+
+ var result = _validator.TestValidate(resource);
+
+ result.ShouldNotHaveAnyValidationErrors();
+ }
+
+ [Test]
+ public void Validate_passes_when_preferred_size_equals_limits()
+ {
+ var resource = new QualityDefinitionResource
+ {
+ MinSize = 5,
+ PreferredSize = 5,
+ MaxSize = 10
+ };
+
+ var result = _validator.TestValidate(resource);
+
+ result.ShouldNotHaveAnyValidationErrors();
+ }
+
+ [Test]
+ public void Validate_fails_when_all_sizes_are_provided_and_invalid()
+ {
+ var resource = new QualityDefinitionResource
+ {
+ MinSize = 15,
+ PreferredSize = 10,
+ MaxSize = 5
+ };
+
+ var result = _validator.TestValidate(resource);
+
+ result.ShouldHaveValidationErrorFor(r => r.MinSize)
+ .WithErrorCode("LessThanOrEqualTo");
+
+ result.ShouldHaveValidationErrorFor(r => r.MaxSize)
+ .WithErrorCode("GreaterThanOrEqualTo");
+
+ result.ShouldHaveValidationErrorFor(r => r.PreferredSize)
+ .WithErrorCode("GreaterThanOrEqualTo");
+ }
+
+ [Test]
+ public void Validate_passes_when_preferred_size_is_valid_within_limits()
+ {
+ var resource = new QualityDefinitionResource
+ {
+ MinSize = 5,
+ PreferredSize = 7,
+ MaxSize = 10
+ };
+
+ var result = _validator.TestValidate(resource);
+
+ result.ShouldNotHaveAnyValidationErrors();
+ }
+}
diff --git a/src/NzbDrone.Core/Qualities/QualityDefinitionLimits.cs b/src/NzbDrone.Core/Qualities/QualityDefinitionLimits.cs
new file mode 100644
index 0000000000..a019f1d99b
--- /dev/null
+++ b/src/NzbDrone.Core/Qualities/QualityDefinitionLimits.cs
@@ -0,0 +1,7 @@
+namespace NzbDrone.Core.Qualities;
+
+public static class QualityDefinitionLimits
+{
+ public const int Min = 0;
+ public const int Max = 2000;
+}
diff --git a/src/Radarr.Api.V3/Qualities/QualityDefinitionController.cs b/src/Radarr.Api.V3/Qualities/QualityDefinitionController.cs
index 7e6cbeef4e..719f184dd7 100644
--- a/src/Radarr.Api.V3/Qualities/QualityDefinitionController.cs
+++ b/src/Radarr.Api.V3/Qualities/QualityDefinitionController.cs
@@ -12,14 +12,21 @@
namespace Radarr.Api.V3.Qualities
{
[V3ApiController]
- public class QualityDefinitionController : RestControllerWithSignalR, IHandle
+ public class QualityDefinitionController :
+ RestControllerWithSignalR,
+ IHandle
{
private readonly IQualityDefinitionService _qualityDefinitionService;
- public QualityDefinitionController(IQualityDefinitionService qualityDefinitionService, IBroadcastSignalRMessage signalRBroadcaster)
+ public QualityDefinitionController(
+ IQualityDefinitionService qualityDefinitionService,
+ IBroadcastSignalRMessage signalRBroadcaster)
: base(signalRBroadcaster)
{
_qualityDefinitionService = qualityDefinitionService;
+
+ SharedValidator.RuleFor(c => c)
+ .SetValidator(new QualityDefinitionResourceValidator());
}
[RestPutById]
@@ -45,9 +52,7 @@ public List GetAll()
public object UpdateMany([FromBody] List resource)
{
// Read from request
- var qualityDefinitions = resource
- .ToModel()
- .ToList();
+ var qualityDefinitions = resource.ToModel().ToList();
_qualityDefinitionService.UpdateMany(qualityDefinitions);
@@ -55,6 +60,14 @@ public object UpdateMany([FromBody] List resource)
.ToResource());
}
+ [HttpGet("limits")]
+ public ActionResult GetLimits()
+ {
+ return Ok(new QualityDefinitionLimitsResource(
+ QualityDefinitionLimits.Min,
+ QualityDefinitionLimits.Max));
+ }
+
[NonAction]
public void Handle(CommandExecutedEvent message)
{
diff --git a/src/Radarr.Api.V3/Qualities/QualityDefinitionLimitsResource.cs b/src/Radarr.Api.V3/Qualities/QualityDefinitionLimitsResource.cs
new file mode 100644
index 0000000000..9c62485c3d
--- /dev/null
+++ b/src/Radarr.Api.V3/Qualities/QualityDefinitionLimitsResource.cs
@@ -0,0 +1,6 @@
+namespace Radarr.Api.V3.Qualities;
+
+// SA1313 still applies to records until https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3181 is available in a release build.
+#pragma warning disable SA1313
+public record QualityDefinitionLimitsResource(int Min, int Max);
+#pragma warning restore SA1313
diff --git a/src/Radarr.Api.V3/Qualities/QualityDefinitionResource.cs b/src/Radarr.Api.V3/Qualities/QualityDefinitionResource.cs
index 1522fe5636..6a01c5a933 100644
--- a/src/Radarr.Api.V3/Qualities/QualityDefinitionResource.cs
+++ b/src/Radarr.Api.V3/Qualities/QualityDefinitionResource.cs
@@ -8,11 +8,8 @@ namespace Radarr.Api.V3.Qualities
public class QualityDefinitionResource : RestResource
{
public Quality Quality { get; set; }
-
public string Title { get; set; }
-
public int Weight { get; set; }
-
public double? MinSize { get; set; }
public double? MaxSize { get; set; }
public double? PreferredSize { get; set; }
@@ -30,13 +27,9 @@ public static QualityDefinitionResource ToResource(this QualityDefinition model)
return new QualityDefinitionResource
{
Id = model.Id,
-
Quality = model.Quality,
-
Title = model.Title,
-
Weight = model.Weight,
-
MinSize = model.MinSize,
MaxSize = model.MaxSize,
PreferredSize = model.PreferredSize
@@ -53,13 +46,9 @@ public static QualityDefinition ToModel(this QualityDefinitionResource resource)
return new QualityDefinition
{
Id = resource.Id,
-
Quality = resource.Quality,
-
Title = resource.Title,
-
Weight = resource.Weight,
-
MinSize = resource.MinSize,
MaxSize = resource.MaxSize,
PreferredSize = resource.PreferredSize
diff --git a/src/Radarr.Api.V3/Qualities/QualityDefinitionResourceValidator.cs b/src/Radarr.Api.V3/Qualities/QualityDefinitionResourceValidator.cs
new file mode 100644
index 0000000000..61305a1b6a
--- /dev/null
+++ b/src/Radarr.Api.V3/Qualities/QualityDefinitionResourceValidator.cs
@@ -0,0 +1,31 @@
+using FluentValidation;
+using NzbDrone.Core.Qualities;
+
+namespace Radarr.Api.V3.Qualities;
+
+public class QualityDefinitionResourceValidator : AbstractValidator
+{
+ public QualityDefinitionResourceValidator()
+ {
+ RuleFor(c => c.MinSize)
+ .GreaterThanOrEqualTo(QualityDefinitionLimits.Min)
+ .WithErrorCode("GreaterThanOrEqualTo")
+ .LessThanOrEqualTo(c => c.PreferredSize ?? QualityDefinitionLimits.Max)
+ .WithErrorCode("LessThanOrEqualTo")
+ .When(c => c.MinSize is not null);
+
+ RuleFor(c => c.PreferredSize)
+ .GreaterThanOrEqualTo(c => c.MinSize ?? QualityDefinitionLimits.Min)
+ .WithErrorCode("GreaterThanOrEqualTo")
+ .LessThanOrEqualTo(c => c.MaxSize ?? QualityDefinitionLimits.Max)
+ .WithErrorCode("LessThanOrEqualTo")
+ .When(c => c.PreferredSize is not null);
+
+ RuleFor(c => c.MaxSize)
+ .GreaterThanOrEqualTo(c => c.PreferredSize ?? QualityDefinitionLimits.Min)
+ .WithErrorCode("GreaterThanOrEqualTo")
+ .LessThanOrEqualTo(QualityDefinitionLimits.Max)
+ .WithErrorCode("LessThanOrEqualTo")
+ .When(c => c.MaxSize is not null);
+ }
+}
diff --git a/src/Radarr.Http/REST/RestController.cs b/src/Radarr.Http/REST/RestController.cs
index 109a054302..a87ed04049 100644
--- a/src/Radarr.Http/REST/RestController.cs
+++ b/src/Radarr.Http/REST/RestController.cs
@@ -69,11 +69,15 @@ public override void OnActionExecuting(ActionExecutingContext context)
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 multiple => multiple,
+ _ => Enumerable.Empty()
+ });
foreach (var resource in resourceArgs)
{