fix(sonarr): Validate and filter RPs after they're loaded

If there is invalid / empty data in the Trash Guide JSON data, filter
those out after they are loaded.
pull/108/head
Robert Dailey 2 years ago
parent bf61ab0fac
commit 1129f178a8

@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Docker: Resolved errors related to `/tmp/.net` directory not existing.
- An exception that says "Cannot write to a closed TextWriter" would sometimes occur at the end of
running a command.
- Sonarr: Validate the TRaSH Guide data better to avoid uploading bad/empty data to Sonarr.
## [2.2.1] - 2022-06-18

@ -3,6 +3,7 @@ using NUnit.Framework;
using TestLibrary.AutoFixture;
using TrashLib.Sonarr.Config;
using TrashLib.Sonarr.ReleaseProfile;
using TrashLib.Sonarr.ReleaseProfile.Filters;
namespace TrashLib.Tests.Sonarr.ReleaseProfile;

@ -0,0 +1,79 @@
using FluentAssertions;
using NUnit.Framework;
using TestLibrary.AutoFixture;
using TrashLib.Sonarr.ReleaseProfile;
using TrashLib.Sonarr.ReleaseProfile.Filters;
namespace TrashLib.Tests.Sonarr.ReleaseProfile.Filters;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class ReleaseProfileDataValidationFiltererTest
{
[Test, AutoMockData]
public void Valid_data_is_not_filtered_out(ReleaseProfileDataValidationFilterer sut)
{
var data = new[]
{
new ReleaseProfileData
{
TrashId = "trash_id",
Name = "name",
Required = new[] {new TermData {Term = "term1"}},
Ignored = new[] {new TermData {Term = "term2"}},
Preferred = new[] {new PreferredTermData {Terms = new[] {new TermData {Term = "term3"}}}}
}
};
var result = sut.FilterProfiles(data);
result.Should().BeEquivalentTo(data);
}
[Test, AutoMockData]
public void Invalid_terms_are_filtered_out(ReleaseProfileDataValidationFilterer sut)
{
var data = new[]
{
new ReleaseProfileData
{
TrashId = "trash_id",
Name = "name",
Required = new[] {new TermData {Term = ""}},
Ignored = new[] {new TermData {Term = "term2"}},
Preferred = new[] {new PreferredTermData {Terms = new[] {new TermData {Term = "term3"}}}}
}
};
var result = sut.FilterProfiles(data);
result.Should().ContainSingle().Which.Should().BeEquivalentTo(new ReleaseProfileData
{
TrashId = "trash_id",
Name = "name",
Required = Array.Empty<TermData>(),
Ignored = new[] {new TermData {Term = "term2"}},
Preferred = new[] {new PreferredTermData {Terms = new[] {new TermData {Term = "term3"}}}}
});
}
[Test, AutoMockData]
public void Whole_release_profile_filtered_out_if_all_terms_invalid(ReleaseProfileDataValidationFilterer sut)
{
var data = new[]
{
new ReleaseProfileData
{
TrashId = "trash_id",
Name = "name",
Required = new[] {new TermData {Term = ""}},
Ignored = new[] {new TermData {Term = ""}},
Preferred = new[] {new PreferredTermData {Terms = new[] {new TermData {Term = ""}}}}
}
};
var result = sut.FilterProfiles(data);
result.Should().BeEmpty();
}
}

@ -1,69 +1,58 @@
using System.Collections.ObjectModel;
using Common.FluentValidation;
using FluentValidation.Results;
using Serilog;
using TrashLib.Sonarr.Config;
namespace TrashLib.Sonarr.ReleaseProfile;
namespace TrashLib.Sonarr.ReleaseProfile.Filters;
public class ReleaseProfileDataFilterer
{
private readonly ILogger _log;
private readonly ReleaseProfileDataValidationFilterer _validator;
public ReleaseProfileDataFilterer(ILogger log)
{
_log = log;
}
private void LogInvalidTerm(List<ValidationFailure> failures, string filterDescription)
{
_log.Debug("Validation failed on term data ({Filter}): {Failures}", filterDescription, failures);
_validator = new ReleaseProfileDataValidationFilterer(log);
}
public ReadOnlyCollection<TermData> ExcludeTerms(IEnumerable<TermData> terms,
IEnumerable<string> excludeFilter)
{
return terms
.Where(x => !excludeFilter.Contains(x.TrashId, StringComparer.InvariantCultureIgnoreCase))
.IsValid(new TermDataValidator(), (e, x) => LogInvalidTerm(e, $"Exclude: {x}"))
.ToList().AsReadOnly();
var result = terms.Where(x => !excludeFilter.Contains(x.TrashId, StringComparer.InvariantCultureIgnoreCase));
return _validator.FilterTerms(result).ToList().AsReadOnly();
}
public ReadOnlyCollection<PreferredTermData> ExcludeTerms(IEnumerable<PreferredTermData> terms,
IReadOnlyCollection<string> excludeFilter)
{
return terms
var result = terms
.Select(x => new PreferredTermData
{
Score = x.Score,
Terms = ExcludeTerms(x.Terms, excludeFilter)
})
.IsValid(new PreferredTermDataValidator(), (e, x) => LogInvalidTerm(e, $"Exclude Preferred: {x}"))
.ToList()
.AsReadOnly();
});
return _validator.FilterTerms(result).ToList().AsReadOnly();
}
public ReadOnlyCollection<TermData> IncludeTerms(IEnumerable<TermData> terms,
IEnumerable<string> includeFilter)
{
return terms
.Where(x => includeFilter.Contains(x.TrashId, StringComparer.InvariantCultureIgnoreCase))
.IsValid(new TermDataValidator(), (e, x) => LogInvalidTerm(e, $"Include: {x}"))
.ToList().AsReadOnly();
var result = terms.Where(x => includeFilter.Contains(x.TrashId, StringComparer.InvariantCultureIgnoreCase));
return _validator.FilterTerms(result).ToList().AsReadOnly();
}
public ReadOnlyCollection<PreferredTermData> IncludeTerms(IEnumerable<PreferredTermData> terms,
IReadOnlyCollection<string> includeFilter)
{
return terms
var result = terms
.Select(x => new PreferredTermData
{
Score = x.Score,
Terms = IncludeTerms(x.Terms, includeFilter)
})
.IsValid(new PreferredTermDataValidator(), (e, x) => LogInvalidTerm(e, $"Include Preferred {x}"))
.ToList()
.AsReadOnly();
});
return _validator.FilterTerms(result).ToList().AsReadOnly();
}
public ReleaseProfileData? FilterProfile(ReleaseProfileData selectedProfile,

@ -0,0 +1,51 @@
using Common.FluentValidation;
using FluentValidation.Results;
using Serilog;
namespace TrashLib.Sonarr.ReleaseProfile.Filters;
public class ReleaseProfileDataValidationFilterer
{
private readonly ILogger _log;
public ReleaseProfileDataValidationFilterer(ILogger log)
{
_log = log;
}
private void LogInvalidTerm(List<ValidationFailure> failures, string filterDescription)
{
_log.Debug("Validation failed on term data ({Filter}): {Failures}", filterDescription, failures);
}
public IEnumerable<TermData> FilterTerms(IEnumerable<TermData> terms)
{
return terms.IsValid(new TermDataValidator(), (e, x) => LogInvalidTerm(e, x.ToString()));
}
public IEnumerable<PreferredTermData> FilterTerms(IEnumerable<PreferredTermData> terms)
{
return terms.IsValid(new PreferredTermDataValidator(), (e, x) => LogInvalidTerm(e, x.ToString()));
}
private ReleaseProfileData FilterProfile(ReleaseProfileData profile)
{
return profile with
{
Required = FilterTerms(profile.Required).ToList(),
Ignored = FilterTerms(profile.Ignored).ToList(),
Preferred = FilterTerms(profile.Preferred).ToList()
};
}
public IEnumerable<ReleaseProfileData> FilterProfiles(IEnumerable<ReleaseProfileData> data)
{
return data
.Select(FilterProfile)
.IsValid(new ReleaseProfileDataValidator(), (e, x) =>
{
_log.Warning("Excluding invalid release profile: {Profile}", x.ToString());
_log.Debug("Release profile excluded for these reasons: {Reasons}", e);
});
}
}

@ -13,11 +13,6 @@ public class ReleaseProfileFilterPipeline : IReleaseProfileFilterPipeline
public ReleaseProfileData Process(ReleaseProfileData profile, ReleaseProfileConfig config)
{
foreach (var filter in _filters)
{
profile = filter.Transform(profile, config);
}
return profile;
return _filters.Aggregate(profile, (current, filter) => filter.Transform(current, config));
}
}

@ -1,9 +1,9 @@
using System.IO.Abstractions;
using Common.Extensions;
using Common.FluentValidation;
using MoreLinq;
using Newtonsoft.Json;
using Serilog;
using TrashLib.Sonarr.ReleaseProfile.Filters;
using TrashLib.Startup;
namespace TrashLib.Sonarr.ReleaseProfile.Guide;
@ -32,9 +32,12 @@ public class LocalRepoReleaseProfileJsonParser : ISonarrGuideService
var tasks = jsonDir.GetFiles("*.json")
.Select(f => LoadAndParseFile(f, converter));
return Task.WhenAll(tasks).Result
var data = Task.WhenAll(tasks).Result
// Make non-nullable type and filter out null values
.Choose(x => x is not null ? (true, x) : default);
var validator = new ReleaseProfileDataValidationFilterer(_log);
return validator.FilterProfiles(data);
}
private async Task<ReleaseProfileData?> LoadAndParseFile(IFileInfo file, params JsonConverter[] converters)
@ -71,8 +74,6 @@ public class LocalRepoReleaseProfileJsonParser : ISonarrGuideService
public IReadOnlyCollection<ReleaseProfileData> GetReleaseProfileData()
{
return _data.Value
.IsValid(new ReleaseProfileDataValidator())
.ToList();
return _data.Value.ToList();
}
}

@ -15,6 +15,7 @@ internal class PreferredTermDataValidator : AbstractValidator<PreferredTermData>
public PreferredTermDataValidator()
{
RuleFor(x => x.Terms).NotEmpty();
RuleForEach(x => x.Terms).SetValidator(new TermDataValidator());
}
}

Loading…
Cancel
Save