diff --git a/src/Sonarr.Api.V5/AutoTagging/AutoTaggingController.cs b/src/Sonarr.Api.V5/AutoTagging/AutoTaggingController.cs new file mode 100644 index 000000000..0c3332ac0 --- /dev/null +++ b/src/Sonarr.Api.V5/AutoTagging/AutoTaggingController.cs @@ -0,0 +1,114 @@ +using FluentValidation; +using FluentValidation.Results; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.AutoTagging; +using NzbDrone.Core.AutoTagging.Specifications; +using NzbDrone.Core.Validation; +using Sonarr.Http; +using Sonarr.Http.REST; +using Sonarr.Http.REST.Attributes; + +namespace Sonarr.Api.V5.AutoTagging; + +[V5ApiController] +public class AutoTaggingController : RestController +{ + private readonly IAutoTaggingService _autoTaggingService; + private readonly List _specifications; + + public AutoTaggingController(IAutoTaggingService autoTaggingService, + List specifications) + { + _autoTaggingService = autoTaggingService; + _specifications = specifications; + + SharedValidator.RuleFor(c => c.Name).NotEmpty(); + SharedValidator.RuleFor(c => c.Name) + .Must((v, c) => !_autoTaggingService.All().Any(f => f.Name == c && f.Id != v.Id)).WithMessage("Must be unique."); + SharedValidator.RuleFor(c => c.Tags).NotEmpty(); + SharedValidator.RuleFor(c => c).Custom((autoTag, context) => + { + if (!autoTag.Specifications.Any()) + { + context.AddFailure("Must contain at least one Condition"); + } + + if (autoTag.Specifications.Any(s => s.Name.IsNullOrWhiteSpace())) + { + context.AddFailure("Condition name(s) cannot be empty or consist of only spaces"); + } + }); + } + + protected override AutoTaggingResource GetResourceById(int id) + { + return _autoTaggingService.GetById(id).ToResource(); + } + + [HttpGet] + [Produces("application/json")] + public List GetAll() + { + return _autoTaggingService.All().ToResource(); + } + + [RestPostById] + [Consumes("application/json")] + public ActionResult Create([FromBody] AutoTaggingResource autoTagResource) + { + var model = autoTagResource.ToModel(_specifications); + + Validate(model); + + return Created(_autoTaggingService.Insert(model).Id); + } + + [RestPutById] + [Consumes("application/json")] + public ActionResult Update([FromBody] AutoTaggingResource resource) + { + var model = resource.ToModel(_specifications); + + Validate(model); + + _autoTaggingService.Update(model); + + return Accepted(model.Id); + } + + [RestDeleteById] + public NoContent DeleteAutoTagging(int id) + { + _autoTaggingService.Delete(id); + + return TypedResults.NoContent(); + } + + [HttpGet("schema")] + [Produces("application/json")] + public List GetTemplates() + { + return _specifications.OrderBy(x => x.Order).Select(x => x.ToSchema()).ToList(); + } + + private void Validate(AutoTag definition) + { + foreach (var validationResult in definition.Specifications.Select(spec => spec.Validate())) + { + VerifyValidationResult(validationResult); + } + } + + private void VerifyValidationResult(ValidationResult validationResult) + { + var result = new NzbDroneValidationResult(validationResult.Errors); + + if (!result.IsValid) + { + throw new ValidationException(result.Errors); + } + } +} diff --git a/src/Sonarr.Api.V5/AutoTagging/AutoTaggingResource.cs b/src/Sonarr.Api.V5/AutoTagging/AutoTaggingResource.cs new file mode 100644 index 000000000..eb1bc42f5 --- /dev/null +++ b/src/Sonarr.Api.V5/AutoTagging/AutoTaggingResource.cs @@ -0,0 +1,72 @@ +using System.Text.Json.Serialization; +using NzbDrone.Core.AutoTagging; +using NzbDrone.Core.AutoTagging.Specifications; +using Sonarr.Http.ClientSchema; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V5.AutoTagging; + +public class AutoTaggingResource : RestResource +{ + [JsonIgnore(Condition = JsonIgnoreCondition.Never)] + public override int Id { get; set; } + public string? Name { get; set; } + public bool RemoveTagsAutomatically { get; set; } + public HashSet Tags { get; set; } = []; + public List Specifications { get; set; } = []; +} + +public static class AutoTaggingResourceMapper +{ + public static AutoTaggingResource ToResource(this AutoTag model) + { + return new AutoTaggingResource + { + Id = model.Id, + Name = model.Name, + RemoveTagsAutomatically = model.RemoveTagsAutomatically, + Tags = model.Tags, + Specifications = model.Specifications.Select(x => x.ToSchema()).ToList() + }; + } + + public static List ToResource(this IEnumerable models) + { + return models.Select(m => m.ToResource()).ToList(); + } + + public static AutoTag ToModel(this AutoTaggingResource resource, List specifications) + { + return new AutoTag + { + Id = resource.Id, + Name = resource.Name, + RemoveTagsAutomatically = resource.RemoveTagsAutomatically, + Tags = resource.Tags, + Specifications = resource.Specifications.Select(x => MapSpecification(x, specifications)).ToList() + }; + } + + private static IAutoTaggingSpecification MapSpecification(AutoTaggingSpecificationSchema resource, List specifications) + { + var matchingSpec = + specifications.SingleOrDefault(x => x.GetType().Name == resource.Implementation); + + if (matchingSpec is null) + { + throw new ArgumentException( + $"{resource.Implementation} is not a valid specification implementation"); + } + + var type = matchingSpec.GetType(); + + // Finding the exact current specification isn't possible given the dynamic nature of them and the possibility that multiple + // of the same type exist within the same format. Passing in null is safe as long as there never exists a specification that + // relies on additional privacy. + var spec = (IAutoTaggingSpecification)SchemaBuilder.ReadFromSchema(resource.Fields, type, null); + spec.Name = resource.Name; + spec.Negate = resource.Negate; + spec.Required = resource.Required; + return spec; + } +} diff --git a/src/Sonarr.Api.V5/AutoTagging/AutoTaggingSpecificationSchema.cs b/src/Sonarr.Api.V5/AutoTagging/AutoTaggingSpecificationSchema.cs new file mode 100644 index 000000000..73e392762 --- /dev/null +++ b/src/Sonarr.Api.V5/AutoTagging/AutoTaggingSpecificationSchema.cs @@ -0,0 +1,31 @@ +using NzbDrone.Core.AutoTagging.Specifications; +using Sonarr.Http.ClientSchema; +using Sonarr.Http.REST; + +namespace Sonarr.Api.V5.AutoTagging; + +public class AutoTaggingSpecificationSchema : RestResource +{ + public string? Name { get; set; } + public string? Implementation { get; set; } + public string? ImplementationName { get; set; } + public bool Negate { get; set; } + public bool Required { get; set; } + public List Fields { get; set; } = []; +} + +public static class AutoTaggingSpecificationSchemaMapper +{ + public static AutoTaggingSpecificationSchema ToSchema(this IAutoTaggingSpecification model) + { + return new AutoTaggingSpecificationSchema + { + Name = model.Name, + Implementation = model.GetType().Name, + ImplementationName = model.ImplementationName, + Negate = model.Negate, + Required = model.Required, + Fields = SchemaBuilder.ToSchema(model) + }; + } +}