Add v5 Auto Tagging endpoints

This commit is contained in:
Mark McDowall 2026-04-09 11:08:48 -07:00
parent 510cbe54e8
commit 3f019e67f2
No known key found for this signature in database
3 changed files with 217 additions and 0 deletions

View file

@ -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<AutoTaggingResource>
{
private readonly IAutoTaggingService _autoTaggingService;
private readonly List<IAutoTaggingSpecification> _specifications;
public AutoTaggingController(IAutoTaggingService autoTaggingService,
List<IAutoTaggingSpecification> 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<AutoTaggingResource> GetAll()
{
return _autoTaggingService.All().ToResource();
}
[RestPostById]
[Consumes("application/json")]
public ActionResult<AutoTaggingResource> Create([FromBody] AutoTaggingResource autoTagResource)
{
var model = autoTagResource.ToModel(_specifications);
Validate(model);
return Created(_autoTaggingService.Insert(model).Id);
}
[RestPutById]
[Consumes("application/json")]
public ActionResult<AutoTaggingResource> 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<AutoTaggingSpecificationSchema> 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);
}
}
}

View file

@ -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<int> Tags { get; set; } = [];
public List<AutoTaggingSpecificationSchema> 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<AutoTaggingResource> ToResource(this IEnumerable<AutoTag> models)
{
return models.Select(m => m.ToResource()).ToList();
}
public static AutoTag ToModel(this AutoTaggingResource resource, List<IAutoTaggingSpecification> 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<IAutoTaggingSpecification> 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;
}
}

View file

@ -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<Field> 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)
};
}
}