diff --git a/CHANGELOG.md b/CHANGELOG.md index 91ff8042..7b65d087 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/TrashLib.Tests/Sonarr/ReleaseProfile/ReleaseProfileDataFiltererTest.cs b/src/TrashLib.Tests/Sonarr/ReleaseProfile/Filters/ReleaseProfileDataFiltererTest.cs similarity index 99% rename from src/TrashLib.Tests/Sonarr/ReleaseProfile/ReleaseProfileDataFiltererTest.cs rename to src/TrashLib.Tests/Sonarr/ReleaseProfile/Filters/ReleaseProfileDataFiltererTest.cs index 31c710c5..2b4a4ff4 100644 --- a/src/TrashLib.Tests/Sonarr/ReleaseProfile/ReleaseProfileDataFiltererTest.cs +++ b/src/TrashLib.Tests/Sonarr/ReleaseProfile/Filters/ReleaseProfileDataFiltererTest.cs @@ -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; diff --git a/src/TrashLib.Tests/Sonarr/ReleaseProfile/Filters/ReleaseProfileDataValidationFiltererTest.cs b/src/TrashLib.Tests/Sonarr/ReleaseProfile/Filters/ReleaseProfileDataValidationFiltererTest.cs new file mode 100644 index 00000000..4fdd543a --- /dev/null +++ b/src/TrashLib.Tests/Sonarr/ReleaseProfile/Filters/ReleaseProfileDataValidationFiltererTest.cs @@ -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(), + 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(); + } +} diff --git a/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileDataFilterer.cs b/src/TrashLib/Sonarr/ReleaseProfile/Filters/ReleaseProfileDataFilterer.cs similarity index 65% rename from src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileDataFilterer.cs rename to src/TrashLib/Sonarr/ReleaseProfile/Filters/ReleaseProfileDataFilterer.cs index e77be0e0..4940e979 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileDataFilterer.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/Filters/ReleaseProfileDataFilterer.cs @@ -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 failures, string filterDescription) - { - _log.Debug("Validation failed on term data ({Filter}): {Failures}", filterDescription, failures); + _validator = new ReleaseProfileDataValidationFilterer(log); } public ReadOnlyCollection ExcludeTerms(IEnumerable terms, IEnumerable 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 ExcludeTerms(IEnumerable terms, IReadOnlyCollection 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 IncludeTerms(IEnumerable terms, IEnumerable 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 IncludeTerms(IEnumerable terms, IReadOnlyCollection 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, diff --git a/src/TrashLib/Sonarr/ReleaseProfile/Filters/ReleaseProfileDataValidationFilterer.cs b/src/TrashLib/Sonarr/ReleaseProfile/Filters/ReleaseProfileDataValidationFilterer.cs new file mode 100644 index 00000000..51b86c27 --- /dev/null +++ b/src/TrashLib/Sonarr/ReleaseProfile/Filters/ReleaseProfileDataValidationFilterer.cs @@ -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 failures, string filterDescription) + { + _log.Debug("Validation failed on term data ({Filter}): {Failures}", filterDescription, failures); + } + + public IEnumerable FilterTerms(IEnumerable terms) + { + return terms.IsValid(new TermDataValidator(), (e, x) => LogInvalidTerm(e, x.ToString())); + } + + public IEnumerable FilterTerms(IEnumerable 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 FilterProfiles(IEnumerable 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); + }); + } +} diff --git a/src/TrashLib/Sonarr/ReleaseProfile/Filters/ReleaseProfileFilterPipeline.cs b/src/TrashLib/Sonarr/ReleaseProfile/Filters/ReleaseProfileFilterPipeline.cs index 0debb752..2a30d7ff 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/Filters/ReleaseProfileFilterPipeline.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/Filters/ReleaseProfileFilterPipeline.cs @@ -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)); } } diff --git a/src/TrashLib/Sonarr/ReleaseProfile/Guide/LocalRepoReleaseProfileJsonParser.cs b/src/TrashLib/Sonarr/ReleaseProfile/Guide/LocalRepoReleaseProfileJsonParser.cs index 5e85db9d..e55f220a 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/Guide/LocalRepoReleaseProfileJsonParser.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/Guide/LocalRepoReleaseProfileJsonParser.cs @@ -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 LoadAndParseFile(IFileInfo file, params JsonConverter[] converters) @@ -71,8 +74,6 @@ public class LocalRepoReleaseProfileJsonParser : ISonarrGuideService public IReadOnlyCollection GetReleaseProfileData() { - return _data.Value - .IsValid(new ReleaseProfileDataValidator()) - .ToList(); + return _data.Value.ToList(); } } diff --git a/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileDataValidator.cs b/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileDataValidator.cs index 16b9cbd2..25cff08d 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileDataValidator.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileDataValidator.cs @@ -15,6 +15,7 @@ internal class PreferredTermDataValidator : AbstractValidator public PreferredTermDataValidator() { RuleFor(x => x.Terms).NotEmpty(); + RuleForEach(x => x.Terms).SetValidator(new TermDataValidator()); } }