mirror of
https://github.com/Sonarr/Sonarr
synced 2026-05-08 21:21:47 +02:00
Add v5 Quality Profiles endpoints
This commit is contained in:
parent
4713615b17
commit
e47c5bd66b
6 changed files with 499 additions and 0 deletions
28
src/Sonarr.Api.V5/Profiles/Quality/QualityCutoffValidator.cs
Normal file
28
src/Sonarr.Api.V5/Profiles/Quality/QualityCutoffValidator.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
using FluentValidation;
|
||||
using FluentValidation.Validators;
|
||||
|
||||
namespace Sonarr.Api.V5.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
|
||||
{
|
||||
protected override string GetDefaultMessageTemplate() => "Cutoff must be an allowed quality or group";
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
var cutoff = (int)context.PropertyValue;
|
||||
dynamic instance = context.ParentContext.InstanceToValidate;
|
||||
var items = instance.Items as IList<QualityProfileQualityItemResource>;
|
||||
|
||||
var cutoffItem = items?.SingleOrDefault(i => (i.Quality == null && i.Id == cutoff) || i.Quality?.Id == cutoff);
|
||||
|
||||
return cutoffItem is { Allowed: true };
|
||||
}
|
||||
}
|
||||
195
src/Sonarr.Api.V5/Profiles/Quality/QualityItemsValidator.cs
Normal file
195
src/Sonarr.Api.V5/Profiles/Quality/QualityItemsValidator.cs
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
using FluentValidation;
|
||||
using FluentValidation.Validators;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace Sonarr.Api.V5.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 GroupItemValidator<T>());
|
||||
ruleBuilder.SetValidator(new ItemGroupIdValidator<T>());
|
||||
ruleBuilder.SetValidator(new UniqueIdValidator<T>());
|
||||
ruleBuilder.SetValidator(new UniqueQualityIdValidator<T>());
|
||||
ruleBuilder.SetValidator(new AllQualitiesValidator<T>());
|
||||
|
||||
return ruleBuilder.SetValidator(new ItemGroupNameValidator<T>());
|
||||
}
|
||||
}
|
||||
|
||||
public class AllowedValidator<T> : PropertyValidator
|
||||
{
|
||||
protected override string GetDefaultMessageTemplate() => "Must contain at least one allowed quality";
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
return context.PropertyValue is IList<QualityProfileQualityItemResource> list &&
|
||||
list.Any(c => c.Allowed);
|
||||
}
|
||||
}
|
||||
|
||||
public class GroupItemValidator<T> : PropertyValidator
|
||||
{
|
||||
protected override string GetDefaultMessageTemplate() => "Groups must contain multiple qualities";
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
if (context.PropertyValue is not IList<QualityProfileQualityItemResource> items)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !items.Any(i => i.Name.IsNotNullOrWhiteSpace() && i.Items.Count <= 1);
|
||||
}
|
||||
}
|
||||
|
||||
public class QualityNameValidator<T> : PropertyValidator
|
||||
{
|
||||
protected override string GetDefaultMessageTemplate() => "Individual qualities should not be named";
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
if (context.PropertyValue is not IList<QualityProfileQualityItemResource> items)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !items.Any(i => i.Name.IsNotNullOrWhiteSpace() && i.Quality != null);
|
||||
}
|
||||
}
|
||||
|
||||
public class ItemGroupNameValidator<T> : PropertyValidator
|
||||
{
|
||||
protected override string GetDefaultMessageTemplate() => "Groups must have a name";
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
if (context.PropertyValue is not IList<QualityProfileQualityItemResource> items)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !items.Any(i => i.Quality == null && i.Name.IsNullOrWhiteSpace());
|
||||
}
|
||||
}
|
||||
|
||||
public class ItemGroupIdValidator<T> : PropertyValidator
|
||||
{
|
||||
protected override string GetDefaultMessageTemplate() => "Groups must have an ID";
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
if (context.PropertyValue is not IList<QualityProfileQualityItemResource> items)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !items.Any(i => i.Quality == null && i.Id == 0);
|
||||
}
|
||||
}
|
||||
|
||||
public class UniqueIdValidator<T> : PropertyValidator
|
||||
{
|
||||
protected override string GetDefaultMessageTemplate() => "Groups must have a unique ID";
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
if (context.PropertyValue is not IList<QualityProfileQualityItemResource> items)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ids = items.Where(i => i.Id > 0).Select(i => i.Id);
|
||||
var groupedIds = ids.GroupBy(i => i);
|
||||
|
||||
return groupedIds.All(g => g.Count() == 1);
|
||||
}
|
||||
}
|
||||
|
||||
public class UniqueQualityIdValidator<T> : PropertyValidator
|
||||
{
|
||||
protected override string GetDefaultMessageTemplate() => "Qualities can only be used once";
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
if (context.PropertyValue is not IList<QualityProfileQualityItemResource> items)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public class AllQualitiesValidator<T> : PropertyValidator
|
||||
{
|
||||
protected override string GetDefaultMessageTemplate() => "Must contain all qualities";
|
||||
|
||||
protected override bool IsValid(PropertyValidatorContext context)
|
||||
{
|
||||
if (context.PropertyValue is not IList<QualityProfileQualityItemResource> items)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var qualityIds = new HashSet<int>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.Id > 0)
|
||||
{
|
||||
foreach (var quality in item.Items)
|
||||
{
|
||||
qualityIds.Add(quality.Quality!.Id);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qualityIds.Add(item.Quality!.Id);
|
||||
}
|
||||
}
|
||||
|
||||
var allQualityIds = NzbDrone.Core.Qualities.Quality.All;
|
||||
|
||||
foreach (var quality in allQualityIds)
|
||||
{
|
||||
if (!qualityIds.Contains(quality.Id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
using FluentValidation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using Sonarr.Http;
|
||||
using Sonarr.Http.REST;
|
||||
using Sonarr.Http.REST.Attributes;
|
||||
|
||||
namespace Sonarr.Api.V5.Profiles.Quality;
|
||||
|
||||
[V5ApiController]
|
||||
public class QualityProfileController : RestController<QualityProfileResource>
|
||||
{
|
||||
private readonly IQualityProfileService _profileService;
|
||||
|
||||
public QualityProfileController(IQualityProfileService profileService, ICustomFormatService formatService)
|
||||
{
|
||||
_profileService = profileService;
|
||||
SharedValidator.RuleFor(c => c.Name).NotEmpty();
|
||||
|
||||
SharedValidator.RuleFor(c => c.MinUpgradeFormatScore).GreaterThanOrEqualTo(1);
|
||||
SharedValidator.RuleFor(c => c.Cutoff).ValidCutoff();
|
||||
SharedValidator.RuleFor(c => c.Items).ValidItems();
|
||||
|
||||
SharedValidator.RuleFor(c => c.FormatItems).Must(items =>
|
||||
{
|
||||
var all = formatService.All().Select(f => f.Id).ToList();
|
||||
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).Custom((profile, context) =>
|
||||
{
|
||||
if (profile.FormatItems.Where(x => x.Score > 0).Sum(x => x.Score) < profile.MinFormatScore &&
|
||||
profile.FormatItems.Max(x => x.Score) < profile.MinFormatScore)
|
||||
{
|
||||
context.AddFailure("Minimum Custom Format Score can never be satisfied");
|
||||
}
|
||||
});
|
||||
|
||||
SharedValidator.RuleFor(c => c)
|
||||
.SetValidator(new QualityProfileResourceValidator());
|
||||
}
|
||||
|
||||
[RestPostById]
|
||||
[Consumes("application/json")]
|
||||
public ActionResult<QualityProfileResource> Create([FromBody] QualityProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
model = _profileService.Add(model);
|
||||
return Created(model.Id);
|
||||
}
|
||||
|
||||
[RestDeleteById]
|
||||
public ActionResult DeleteProfile(int id)
|
||||
{
|
||||
_profileService.Delete(id);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[RestPutById]
|
||||
[Consumes("application/json")]
|
||||
public ActionResult<QualityProfileResource> Update([FromBody] QualityProfileResource resource)
|
||||
{
|
||||
var model = resource.ToModel();
|
||||
|
||||
_profileService.Update(model);
|
||||
|
||||
return Accepted(model.Id);
|
||||
}
|
||||
|
||||
protected override QualityProfileResource GetResourceById(int id)
|
||||
{
|
||||
return _profileService.Get(id).ToResource();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Produces("application/json")]
|
||||
public List<QualityProfileResource> GetAll()
|
||||
{
|
||||
return _profileService.All().ToResource();
|
||||
}
|
||||
}
|
||||
125
src/Sonarr.Api.V5/Profiles/Quality/QualityProfileResource.cs
Normal file
125
src/Sonarr.Api.V5/Profiles/Quality/QualityProfileResource.cs
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using Sonarr.Http.REST;
|
||||
|
||||
namespace Sonarr.Api.V5.Profiles.Quality;
|
||||
|
||||
public class QualityProfileResource : RestResource
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public bool UpgradeAllowed { get; set; }
|
||||
public int Cutoff { get; set; }
|
||||
public List<QualityProfileQualityItemResource> Items { get; set; } = [];
|
||||
public int MinFormatScore { get; set; }
|
||||
public int CutoffFormatScore { get; set; }
|
||||
public int MinUpgradeFormatScore { get; set; }
|
||||
public List<ProfileFormatItemResource> FormatItems { 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 double? MinSize { get; set; }
|
||||
public double? MaxSize { get; set; }
|
||||
public double? PreferredSize { get; set; }
|
||||
}
|
||||
|
||||
public class ProfileFormatItemResource : RestResource
|
||||
{
|
||||
public int Format { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public int Score { get; set; }
|
||||
}
|
||||
|
||||
public static class ProfileResourceMapper
|
||||
{
|
||||
public static QualityProfileResource ToResource(this QualityProfile model)
|
||||
{
|
||||
return new QualityProfileResource
|
||||
{
|
||||
Id = model.Id,
|
||||
Name = model.Name,
|
||||
UpgradeAllowed = model.UpgradeAllowed,
|
||||
Cutoff = model.Cutoff,
|
||||
Items = model.Items.ConvertAll(ToResource),
|
||||
MinFormatScore = model.MinFormatScore,
|
||||
CutoffFormatScore = model.CutoffFormatScore,
|
||||
MinUpgradeFormatScore = model.MinUpgradeFormatScore,
|
||||
FormatItems = model.FormatItems.ConvertAll(ToResource)
|
||||
};
|
||||
}
|
||||
|
||||
public static QualityProfileQualityItemResource ToResource(this QualityProfileQualityItem model)
|
||||
{
|
||||
return new QualityProfileQualityItemResource
|
||||
{
|
||||
Id = model.Id,
|
||||
Name = model.Name,
|
||||
Quality = model.Quality,
|
||||
Items = model.Items.ConvertAll(ToResource),
|
||||
Allowed = model.Allowed,
|
||||
MinSize = model.MinSize,
|
||||
MaxSize = model.MaxSize,
|
||||
PreferredSize = model.PreferredSize
|
||||
};
|
||||
}
|
||||
|
||||
public static ProfileFormatItemResource ToResource(this ProfileFormatItem model)
|
||||
{
|
||||
return new ProfileFormatItemResource
|
||||
{
|
||||
Format = model.Format.Id,
|
||||
Name = model.Format.Name,
|
||||
Score = model.Score
|
||||
};
|
||||
}
|
||||
|
||||
public static QualityProfile ToModel(this QualityProfileResource resource)
|
||||
{
|
||||
return new QualityProfile
|
||||
{
|
||||
Id = resource.Id,
|
||||
Name = resource.Name,
|
||||
UpgradeAllowed = resource.UpgradeAllowed,
|
||||
Cutoff = resource.Cutoff,
|
||||
Items = resource.Items.ConvertAll(ToModel),
|
||||
MinFormatScore = resource.MinFormatScore,
|
||||
CutoffFormatScore = resource.CutoffFormatScore,
|
||||
MinUpgradeFormatScore = resource.MinUpgradeFormatScore,
|
||||
FormatItems = resource.FormatItems.ConvertAll(ToModel)
|
||||
};
|
||||
}
|
||||
|
||||
public static QualityProfileQualityItem ToModel(this QualityProfileQualityItemResource resource)
|
||||
{
|
||||
return new QualityProfileQualityItem
|
||||
{
|
||||
Id = resource.Id,
|
||||
Name = resource.Name,
|
||||
Quality = resource.Quality != null ? (NzbDrone.Core.Qualities.Quality)resource.Quality.Id : null,
|
||||
Items = resource.Items.ConvertAll(ToModel),
|
||||
Allowed = resource.Allowed,
|
||||
MinSize = resource.MinSize,
|
||||
MaxSize = resource.MaxSize,
|
||||
PreferredSize = resource.PreferredSize
|
||||
};
|
||||
}
|
||||
|
||||
public static ProfileFormatItem ToModel(this ProfileFormatItemResource resource)
|
||||
{
|
||||
return new ProfileFormatItem
|
||||
{
|
||||
Format = new CustomFormat { Id = resource.Format },
|
||||
Score = resource.Score
|
||||
};
|
||||
}
|
||||
|
||||
public static List<QualityProfileResource> ToResource(this IEnumerable<QualityProfile> models)
|
||||
{
|
||||
return models.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
using FluentValidation;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace Sonarr.Api.V5.Profiles.Quality;
|
||||
|
||||
public class QualityProfileResourceValidator : AbstractValidator<QualityProfileResource>
|
||||
{
|
||||
public QualityProfileResourceValidator()
|
||||
{
|
||||
RuleForEach(c => c.Items)
|
||||
.SetValidator(new QualityProfileQualityItemResourceValidator());
|
||||
}
|
||||
}
|
||||
|
||||
public class QualityProfileQualityItemResourceValidator : AbstractValidator<QualityProfileQualityItemResource>
|
||||
{
|
||||
public QualityProfileQualityItemResourceValidator()
|
||||
{
|
||||
RuleFor(c => c.MinSize)
|
||||
.GreaterThanOrEqualTo(QualityDefinitionLimits.Min)
|
||||
.WithErrorCode("GreaterThanOrEqualTo")
|
||||
.LessThanOrEqualTo(c => c.MinSize ?? 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using Sonarr.Http;
|
||||
|
||||
namespace Sonarr.Api.V5.Profiles.Quality
|
||||
{
|
||||
[V5ApiController("qualityprofile/schema")]
|
||||
public class QualityProfileSchemaController : Controller
|
||||
{
|
||||
private readonly IQualityProfileService _profileService;
|
||||
|
||||
public QualityProfileSchemaController(IQualityProfileService profileService)
|
||||
{
|
||||
_profileService = profileService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public QualityProfileResource GetSchema()
|
||||
{
|
||||
var qualityProfile = _profileService.GetDefaultProfile(string.Empty);
|
||||
|
||||
return qualityProfile.ToResource();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue