From 593740900a12630fb9c3b28e3515af5f92ede03d Mon Sep 17 00:00:00 2001 From: Robert Dailey Date: Tue, 21 Dec 2021 17:09:45 -0600 Subject: [PATCH] refactor: convert to file-scoped namespaces --- .../Extensions/DictionaryExtensionsTest.cs | 107 ++- src/Common.Tests/ResourceDataReaderTest.cs | 53 +- src/Common/Extensions/CollectionExtensions.cs | 47 +- src/Common/Extensions/DictionaryExtensions.cs | 29 +- .../Extensions/FluentValidationExtensions.cs | 51 +- src/Common/Extensions/RegexExtensions.cs | 21 +- src/Common/Extensions/RxExtensions.cs | 67 +- src/Common/Extensions/StringExtensions.cs | 43 +- src/Common/JsonNetExtensions.cs | 31 +- src/Common/ResourceDataReader.cs | 55 +- .../YamlDotNet/CannotBeEmptyAttribute.cs | 17 +- .../YamlDotNet/ValidatingDeserializer.cs | 53 +- src/Common/YamlDotNet/YamlDotNetExtensions.cs | 39 +- .../YamlNullableEnumTypeConverter.cs | 97 ++- src/TestLibrary.Tests/StreamBuilderTest.cs | 19 +- src/TestLibrary.Tests/StringUtilsTest.cs | 19 +- .../FluentAssertions/JsonEquivalencyStep.cs | 25 +- src/TestLibrary/NSubstitute/Verify.cs | 43 +- src/TestLibrary/StreamBuilder.cs | 13 +- src/TestLibrary/StringUtils.cs | 9 +- .../Command/CreateConfigCommandTest.cs | 55 +- .../Command/Helpers/CliTypeActivatorTest.cs | 91 ++- src/Trash.Tests/CompositionRootTest.cs | 49 +- .../Config/ConfigurationLoaderTest.cs | 235 ++++--- src/Trash.Tests/LogJanitorTest.cs | 55 +- src/Trash/AppPaths.cs | 17 +- src/Trash/Command/CreateConfigCommand.cs | 57 +- .../Helpers/ActiveServiceCommandProvider.cs | 19 +- src/Trash/Command/Helpers/CacheStoragePath.cs | 19 +- src/Trash/Command/Helpers/CliTypeActivator.cs | 21 +- src/Trash/Command/Helpers/ExitCode.cs | 11 +- .../Helpers/IActiveServiceCommandProvider.cs | 9 +- src/Trash/Command/Helpers/IServiceCommand.cs | 15 +- src/Trash/Command/Helpers/ServiceCommand.cs | 169 +++-- src/Trash/Command/RadarrCommand.cs | 81 ++- src/Trash/Command/SonarrCommand.cs | 81 ++- src/Trash/CompositionRoot.cs | 151 +++-- src/Trash/Config/ConfigurationException.cs | 73 ++- src/Trash/Config/ConfigurationLoader.cs | 135 ++-- src/Trash/Config/IConfigurationLoader.cs | 15 +- src/Trash/Config/ObjectFactory.cs | 25 +- src/Trash/ILogJanitor.cs | 9 +- src/Trash/LogJanitor.cs | 29 +- src/Trash/Program.cs | 29 +- src/Trash/ResourcePaths.cs | 9 +- src/TrashLib.TestLibrary/CfTestUtils.cs | 15 +- src/TrashLib.Tests/Cache/ServiceCacheTest.cs | 299 +++++---- .../Radarr/CustomFormat/CachePersisterTest.cs | 231 ++++--- .../Processors/GuideProcessorTest.cs | 227 ++++--- .../Processors/GuideSteps/ConfigStepTest.cs | 331 +++++----- .../GuideSteps/CustomFormatStepTest.cs | 609 +++++++++--------- .../GuideSteps/QualityProfileStepTest.cs | 177 +++-- .../Processors/PersistenceProcessorTest.cs | 135 ++-- .../CustomFormatApiPersistenceStepTest.cs | 55 +- .../JsonTransactionStepTest.cs | 235 ++++--- .../QualityProfileApiPersistenceStepTest.cs | 215 +++---- .../RadarrQualityDataTest.cs | 203 +++--- .../Radarr/RadarrConfigurationTest.cs | 151 +++-- ...rReleaseProfileCompatibilityHandlerTest.cs | 141 ++-- .../SonarrQualityDataTest.cs | 167 +++-- .../ReleaseProfile/FilteredProfileDataTest.cs | 129 ++-- .../ReleaseProfileParserTest.cs | 409 ++++++------ .../Sonarr/ReleaseProfile/ScopedStateTest.cs | 245 ++++--- .../Sonarr/ReleaseProfileUpdaterTest.cs | 65 +- .../Sonarr/SonarrConfigurationTest.cs | 91 ++- src/TrashLib/Cache/CacheAutofacModule.cs | 13 +- .../Cache/CacheObjectNameAttribute.cs | 17 +- src/TrashLib/Cache/ICacheStoragePath.cs | 9 +- src/TrashLib/Cache/IServiceCache.cs | 11 +- src/TrashLib/Cache/ServiceCache.cs | 129 ++-- src/TrashLib/Config/ConfigAutofacModule.cs | 21 +- src/TrashLib/Config/ConfigurationProvider.cs | 17 +- src/TrashLib/Config/IConfigurationProvider.cs | 9 +- src/TrashLib/Config/IServerInfo.cs | 9 +- src/TrashLib/Config/IServiceConfiguration.cs | 11 +- src/TrashLib/Config/ServerInfo.cs | 37 +- src/TrashLib/Config/ServiceConfiguration.cs | 11 +- .../ExceptionTypes/VersionException.cs | 21 +- src/TrashLib/Extensions/FlurlExtensions.cs | 43 +- src/TrashLib/Extensions/FlurlLogging.cs | 33 +- .../Config/IRadarrValidationMessages.cs | 17 +- src/TrashLib/Radarr/Config/IResourcePaths.cs | 9 +- .../Radarr/Config/RadarrConfiguration.cs | 59 +- .../Config/RadarrConfigurationValidator.cs | 69 +- .../Radarr/Config/RadarrValidationMessages.cs | 27 +- .../CustomFormat/Api/CustomFormatService.cs | 75 ++- .../CustomFormat/Api/ICustomFormatService.cs | 15 +- .../Api/IQualityProfileService.cs | 11 +- .../CustomFormat/Api/QualityProfileService.cs | 45 +- .../Radarr/CustomFormat/ApiOperationType.cs | 15 +- .../Radarr/CustomFormat/CachePersister.cs | 87 ++- .../CustomFormat/CustomFormatUpdater.cs | 403 ++++++------ .../CustomFormat/Guide/IRadarrGuideService.cs | 9 +- .../Radarr/CustomFormat/ICachePersister.cs | 15 +- .../CustomFormat/ICustomFormatUpdater.cs | 9 +- .../Models/Cache/CustomFormatCache.cs | 37 +- .../Models/ProcessedConfigData.cs | 15 +- .../Models/ProcessedCustomFormatData.cs | 47 +- .../QualityProfileCustomFormatScoreMapping.cs | 21 +- .../CustomFormat/Models/UpdatedFormatScore.cs | 21 +- .../CustomFormat/Processors/GuideProcessor.cs | 129 ++-- .../Processors/GuideSteps/ConfigStep.cs | 89 ++- .../Processors/GuideSteps/CustomFormatStep.cs | 211 +++--- .../Processors/GuideSteps/IConfigStep.cs | 15 +- .../GuideSteps/ICustomFormatStep.cs | 19 +- .../GuideSteps/IQualityProfileStep.cs | 13 +- .../GuideSteps/QualityProfileStep.cs | 69 +- .../Processors/IGuideProcessor.cs | 27 +- .../Processors/IPersistenceProcessor.cs | 21 +- .../Processors/PersistenceProcessor.cs | 113 ++-- .../CustomFormatApiPersistenceStep.cs | 29 +- .../ICustomFormatApiPersistenceStep.cs | 9 +- .../PersistenceSteps/IJsonTransactionStep.cs | 15 +- .../IQualityProfileApiPersistenceStep.cs | 15 +- .../PersistenceSteps/JsonTransactionStep.cs | 259 ++++---- .../QualityProfileApiPersistenceStep.cs | 119 ++-- .../Api/IQualityDefinitionService.cs | 11 +- .../Objects/RadarrQualityDefinitionItem.cs | 41 +- .../Api/QualityDefinitionService.cs | 47 +- .../IRadarrQualityDefinitionGuideParser.cs | 11 +- .../IRadarrQualityDefinitionUpdater.cs | 9 +- .../QualityDefinition/RadarrQualityData.cs | 35 +- .../RadarrQualityDefinitionGuideParser.cs | 93 ++- .../RadarrQualityDefinitionType.cs | 9 +- .../RadarrQualityDefinitionUpdater.cs | 165 +++-- src/TrashLib/Radarr/RadarrAutofacModule.cs | 75 ++- src/TrashLib/Sonarr/Api/ISonarrApi.cs | 23 +- ...onarrReleaseProfileCompatibilityHandler.cs | 11 +- .../Mappings/SonarrApiObjectMappingProfile.cs | 31 +- .../Objects/SonarrQualityDefinitionItem.cs | 37 +- .../Api/Objects/SonarrReleaseProfile.cs | 85 ++- src/TrashLib/Sonarr/Api/Objects/SonarrTag.cs | 11 +- .../Api/Schemas/SonarrReleaseProfileSchema.cs | 11 +- src/TrashLib/Sonarr/Api/SonarrApi.cs | 137 ++-- ...onarrReleaseProfileCompatibilityHandler.cs | 75 ++- .../Config/ISonarrValidationMessages.cs | 13 +- .../Sonarr/Config/SonarrConfiguration.cs | 41 +- .../Config/SonarrConfigurationValidator.cs | 33 +- .../Sonarr/Config/SonarrValidationMessages.cs | 21 +- src/TrashLib/Sonarr/ISonarrCompatibility.cs | 11 +- .../ISonarrQualityDefinitionGuideParser.cs | 11 +- .../ISonarrQualityDefinitionUpdater.cs | 9 +- .../QualityDefinition/SonarrQualityData.cs | 51 +- .../SonarrQualityDefinitionGuideParser.cs | 97 ++- .../SonarrQualityDefinitionType.cs | 13 +- .../SonarrQualityDefinitionUpdater.cs | 215 +++---- .../ReleaseProfile/FilteredProfileData.cs | 47 +- .../IReleaseProfileGuideParser.cs | 11 +- .../ReleaseProfile/IReleaseProfileUpdater.cs | 9 +- .../Sonarr/ReleaseProfile/ParserState.cs | 109 ++-- .../Sonarr/ReleaseProfile/ProfileData.cs | 37 +- .../ReleaseProfileGuideParser.cs | 487 +++++++------- .../ReleaseProfile/ReleaseProfileType.cs | 11 +- .../ReleaseProfile/ReleaseProfileUpdater.cs | 253 ++++---- .../Sonarr/ReleaseProfile/ScopedState.cs | 83 ++- src/TrashLib/Sonarr/ReleaseProfile/Utils.cs | 115 ++-- src/TrashLib/Sonarr/SonarrAutofacModule.cs | 35 +- src/TrashLib/Sonarr/SonarrCapabilities.cs | 35 +- src/TrashLib/Sonarr/SonarrCompatibility.cs | 49 +- src/TrashLib/Startup/AutoMapperConfig.cs | 21 +- 160 files changed, 5776 insertions(+), 5934 deletions(-) diff --git a/src/Common.Tests/Extensions/DictionaryExtensionsTest.cs b/src/Common.Tests/Extensions/DictionaryExtensionsTest.cs index d72252b7..b95a7d76 100644 --- a/src/Common.Tests/Extensions/DictionaryExtensionsTest.cs +++ b/src/Common.Tests/Extensions/DictionaryExtensionsTest.cs @@ -3,61 +3,60 @@ using Common.Extensions; using FluentAssertions; using NUnit.Framework; -namespace Common.Tests.Extensions +namespace Common.Tests.Extensions; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class DictionaryExtensionsTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class DictionaryExtensionsTest + private class MySampleValue { - private class MySampleValue - { - } - - [Test] - public void Create_item_if_none_exists() - { - var dict = new Dictionary(); - var theValue = dict.GetOrCreate(100); - dict.Should().HaveCount(1); - dict.Should().Contain(100, theValue); - } - - [Test] - public void Return_default_if_no_item_exists() - { - var sample = new MySampleValue(); - var dict = new Dictionary {{100, sample}}; - - var theValue = dict.GetOrDefault(200); - - dict.Should().HaveCount(1).And.Contain(100, sample); - theValue.Should().BeNull(); - } - - [Test] - public void Return_existing_item_if_exists_not_create() - { - var sample = new MySampleValue(); - var dict = new Dictionary {{100, sample}}; - - var theValue = dict.GetOrCreate(100); - dict.Should().HaveCount(1); - dict.Should().Contain(100, sample); - dict.Should().ContainValue(theValue); - theValue.Should().Be(sample); - } - - [Test] - public void Return_existing_item_if_it_exists_not_default() - { - var sample = new MySampleValue(); - var dict = new Dictionary {{100, sample}}; - - var theValue = dict.GetOrDefault(100); - - // Ensure the container hasn't been mutated - dict.Should().HaveCount(1).And.Contain(100, sample); - theValue.Should().Be(sample); - } + } + + [Test] + public void Create_item_if_none_exists() + { + var dict = new Dictionary(); + var theValue = dict.GetOrCreate(100); + dict.Should().HaveCount(1); + dict.Should().Contain(100, theValue); + } + + [Test] + public void Return_default_if_no_item_exists() + { + var sample = new MySampleValue(); + var dict = new Dictionary {{100, sample}}; + + var theValue = dict.GetOrDefault(200); + + dict.Should().HaveCount(1).And.Contain(100, sample); + theValue.Should().BeNull(); + } + + [Test] + public void Return_existing_item_if_exists_not_create() + { + var sample = new MySampleValue(); + var dict = new Dictionary {{100, sample}}; + + var theValue = dict.GetOrCreate(100); + dict.Should().HaveCount(1); + dict.Should().Contain(100, sample); + dict.Should().ContainValue(theValue); + theValue.Should().Be(sample); + } + + [Test] + public void Return_existing_item_if_it_exists_not_default() + { + var sample = new MySampleValue(); + var dict = new Dictionary {{100, sample}}; + + var theValue = dict.GetOrDefault(100); + + // Ensure the container hasn't been mutated + dict.Should().HaveCount(1).And.Contain(100, sample); + theValue.Should().Be(sample); } } diff --git a/src/Common.Tests/ResourceDataReaderTest.cs b/src/Common.Tests/ResourceDataReaderTest.cs index 8757762b..be6b3adc 100644 --- a/src/Common.Tests/ResourceDataReaderTest.cs +++ b/src/Common.Tests/ResourceDataReaderTest.cs @@ -2,37 +2,36 @@ using FluentAssertions; using NUnit.Framework; -namespace Common.Tests +namespace Common.Tests; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class ResourceDataReaderTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class ResourceDataReaderTest + [Test] + public void GetResourceData_DefaultDir_ReturnResourceData() { - [Test] - public void GetResourceData_DefaultDir_ReturnResourceData() - { - var testData = new ResourceDataReader(typeof(ResourceDataReaderTest)); - var data = testData.ReadData("DefaultDataFile.txt"); - data.Trim().Should().Be("DefaultDataFile"); - } + var testData = new ResourceDataReader(typeof(ResourceDataReaderTest)); + var data = testData.ReadData("DefaultDataFile.txt"); + data.Trim().Should().Be("DefaultDataFile"); + } - [Test] - public void GetResourceData_NonexistentFile_Throw() - { - var testData = new ResourceDataReader(typeof(ResourceDataReaderTest)); - Action act = () => testData.ReadData("DataFileWontBeFound.txt"); + [Test] + public void GetResourceData_NonexistentFile_Throw() + { + var testData = new ResourceDataReader(typeof(ResourceDataReaderTest)); + Action act = () => testData.ReadData("DataFileWontBeFound.txt"); - act.Should() - .Throw() - .WithMessage("Embedded resource not found*"); - } + act.Should() + .Throw() + .WithMessage("Embedded resource not found*"); + } - [Test] - public void ReadData_ExplicitSubDir_ReturnResourceData() - { - var testData = new ResourceDataReader(typeof(ResourceDataReaderTest), "TestData"); - var data = testData.ReadData("DataFile.txt"); - data.Trim().Should().Be("DataFile"); - } + [Test] + public void ReadData_ExplicitSubDir_ReturnResourceData() + { + var testData = new ResourceDataReader(typeof(ResourceDataReaderTest), "TestData"); + var data = testData.ReadData("DataFile.txt"); + data.Trim().Should().Be("DataFile"); } } diff --git a/src/Common/Extensions/CollectionExtensions.cs b/src/Common/Extensions/CollectionExtensions.cs index 7951e32d..7b7fda17 100644 --- a/src/Common/Extensions/CollectionExtensions.cs +++ b/src/Common/Extensions/CollectionExtensions.cs @@ -2,37 +2,36 @@ using System; using System.Collections; using System.Collections.Generic; -namespace Common.Extensions +namespace Common.Extensions; + +public static class CollectionExtensions { - public static class CollectionExtensions + // From: https://stackoverflow.com/a/34362585/157971 + public static IReadOnlyCollection AsReadOnly(this ICollection source) { - // From: https://stackoverflow.com/a/34362585/157971 - public static IReadOnlyCollection AsReadOnly(this ICollection source) + if (source is null) { - if (source is null) - { - throw new ArgumentNullException(nameof(source)); - } - - return source as IReadOnlyCollection ?? new ReadOnlyCollectionAdapter(source); + throw new ArgumentNullException(nameof(source)); } - // From: https://stackoverflow.com/a/34362585/157971 - private sealed class ReadOnlyCollectionAdapter : IReadOnlyCollection - { - private readonly ICollection _source; - public ReadOnlyCollectionAdapter(ICollection source) => _source = source; - public int Count => _source.Count; - public IEnumerator GetEnumerator() => _source.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - } + return source as IReadOnlyCollection ?? new ReadOnlyCollectionAdapter(source); + } - public static void AddRange(this ICollection destination, IEnumerable source) + // From: https://stackoverflow.com/a/34362585/157971 + private sealed class ReadOnlyCollectionAdapter : IReadOnlyCollection + { + private readonly ICollection _source; + public ReadOnlyCollectionAdapter(ICollection source) => _source = source; + public int Count => _source.Count; + public IEnumerator GetEnumerator() => _source.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + public static void AddRange(this ICollection destination, IEnumerable source) + { + foreach (var item in source) { - foreach (var item in source) - { - destination.Add(item); - } + destination.Add(item); } } } diff --git a/src/Common/Extensions/DictionaryExtensions.cs b/src/Common/Extensions/DictionaryExtensions.cs index d93d5458..fbdda156 100644 --- a/src/Common/Extensions/DictionaryExtensions.cs +++ b/src/Common/Extensions/DictionaryExtensions.cs @@ -1,24 +1,23 @@ using System.Collections.Generic; -namespace Common.Extensions +namespace Common.Extensions; + +public static class DictionaryExtensions { - public static class DictionaryExtensions + public static TValue GetOrCreate(this IDictionary dict, TKey key) + where TValue : new() { - public static TValue GetOrCreate(this IDictionary dict, TKey key) - where TValue : new() + if (!dict.TryGetValue(key, out var val)) { - if (!dict.TryGetValue(key, out var val)) - { - val = new TValue(); - dict.Add(key, val); - } - - return val; + val = new TValue(); + dict.Add(key, val); } - public static TValue? GetOrDefault(this IDictionary dict, TKey key) - { - return dict.TryGetValue(key, out var val) ? val : default; - } + return val; + } + + public static TValue? GetOrDefault(this IDictionary dict, TKey key) + { + return dict.TryGetValue(key, out var val) ? val : default; } } diff --git a/src/Common/Extensions/FluentValidationExtensions.cs b/src/Common/Extensions/FluentValidationExtensions.cs index 186a87a5..03d7d491 100644 --- a/src/Common/Extensions/FluentValidationExtensions.cs +++ b/src/Common/Extensions/FluentValidationExtensions.cs @@ -4,40 +4,39 @@ using System.Threading.Tasks; using FluentValidation; using FluentValidation.Validators; -namespace Common.Extensions +namespace Common.Extensions; + +public static class FluentValidationExtensions { - public static class FluentValidationExtensions + // From: https://github.com/FluentValidation/FluentValidation/issues/1648 + public static IRuleBuilderOptions SetNonNullableValidator( + this IRuleBuilder ruleBuilder, IValidator validator, params string[] ruleSets) { - // From: https://github.com/FluentValidation/FluentValidation/issues/1648 - public static IRuleBuilderOptions SetNonNullableValidator( - this IRuleBuilder ruleBuilder, IValidator validator, params string[] ruleSets) + var adapter = new NullableChildValidatorAdaptor(validator, validator.GetType()) { - var adapter = new NullableChildValidatorAdaptor(validator, validator.GetType()) - { - RuleSets = ruleSets - }; + RuleSets = ruleSets + }; - return ruleBuilder.SetAsyncValidator(adapter); - } + return ruleBuilder.SetAsyncValidator(adapter); + } - private sealed class NullableChildValidatorAdaptor : ChildValidatorAdaptor, - IPropertyValidator, IAsyncPropertyValidator + private sealed class NullableChildValidatorAdaptor : ChildValidatorAdaptor, + IPropertyValidator, IAsyncPropertyValidator + { + public NullableChildValidatorAdaptor(IValidator validator, Type validatorType) + : base(validator, validatorType) { - public NullableChildValidatorAdaptor(IValidator validator, Type validatorType) - : base(validator, validatorType) - { - } + } - public override Task IsValidAsync(ValidationContext context, TProperty? value, - CancellationToken cancellation) - { - return base.IsValidAsync(context, value!, cancellation); - } + public override Task IsValidAsync(ValidationContext context, TProperty? value, + CancellationToken cancellation) + { + return base.IsValidAsync(context, value!, cancellation); + } - public override bool IsValid(ValidationContext context, TProperty? value) - { - return base.IsValid(context, value!); - } + public override bool IsValid(ValidationContext context, TProperty? value) + { + return base.IsValid(context, value!); } } } diff --git a/src/Common/Extensions/RegexExtensions.cs b/src/Common/Extensions/RegexExtensions.cs index e4c3d1c2..8303814b 100644 --- a/src/Common/Extensions/RegexExtensions.cs +++ b/src/Common/Extensions/RegexExtensions.cs @@ -1,18 +1,17 @@ using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; -namespace Common.Extensions +namespace Common.Extensions; + +public static class RegexExtensions { - public static class RegexExtensions + [SuppressMessage("Design", "CA1021:Avoid out parameters", + Justification = + "The out param has a very specific design purpose. It's to allow regex match expressions " + + "to be executed inside an if condition while also providing match output variable.")] + public static bool Match(this Regex re, string strToCheck, out Match match) { - [SuppressMessage("Design", "CA1021:Avoid out parameters", - Justification = - "The out param has a very specific design purpose. It's to allow regex match expressions " + - "to be executed inside an if condition while also providing match output variable.")] - public static bool Match(this Regex re, string strToCheck, out Match match) - { - match = re.Match(strToCheck); - return match.Success; - } + match = re.Match(strToCheck); + return match.Success; } } diff --git a/src/Common/Extensions/RxExtensions.cs b/src/Common/Extensions/RxExtensions.cs index fe7ff1bf..d1a6f798 100644 --- a/src/Common/Extensions/RxExtensions.cs +++ b/src/Common/Extensions/RxExtensions.cs @@ -3,46 +3,45 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using Serilog; -namespace Common.Extensions +namespace Common.Extensions; + +public static class RxExtensions { - public static class RxExtensions + public static IObservable Spy(this IObservable source, ILogger log, string? opName = null) { - public static IObservable Spy(this IObservable source, ILogger log, string? opName = null) + opName ??= "IObservable"; + log.Debug("{OpName}: Observable obtained on Thread: {ThreadId}", + opName, + Environment.CurrentManagedThreadId); + + return Observable.Create(obs => { - opName ??= "IObservable"; - log.Debug("{OpName}: Observable obtained on Thread: {ThreadId}", + log.Debug("{OpName}: Subscribed to on Thread: {ThreadId}", opName, Environment.CurrentManagedThreadId); - return Observable.Create(obs => + try { - log.Debug("{OpName}: Subscribed to on Thread: {ThreadId}", - opName, - Environment.CurrentManagedThreadId); - - try - { - var subscription = source - .Do( - x => log.Debug("{OpName}: OnNext({Result}) on Thread: {ThreadId}", opName, x, - Environment.CurrentManagedThreadId), - ex => log.Debug("{OpName}: OnError({Result}) on Thread: {ThreadId}", opName, ex.Message, - Environment.CurrentManagedThreadId), - () => log.Debug("{OpName}: OnCompleted() on Thread: {ThreadId}", opName, - Environment.CurrentManagedThreadId)) - .Subscribe(obs); - return new CompositeDisposable( - subscription, - Disposable.Create(() => log.Debug( - "{OpName}: Cleaned up on Thread: {ThreadId}", - opName, - Environment.CurrentManagedThreadId))); - } - finally - { - log.Debug("{OpName}: Subscription completed", opName); - } - }); - } + var subscription = source + .Do( + x => log.Debug("{OpName}: OnNext({Result}) on Thread: {ThreadId}", opName, x, + Environment.CurrentManagedThreadId), + ex => log.Debug("{OpName}: OnError({Result}) on Thread: {ThreadId}", opName, ex.Message, + Environment.CurrentManagedThreadId), + () => log.Debug("{OpName}: OnCompleted() on Thread: {ThreadId}", opName, + Environment.CurrentManagedThreadId)) + .Subscribe(obs); + return new CompositeDisposable( + subscription, + Disposable.Create(() => log.Debug( + "{OpName}: Cleaned up on Thread: {ThreadId}", + opName, + Environment.CurrentManagedThreadId))); + } + finally + { + log.Debug("{OpName}: Subscription completed", opName); + } + }); } } diff --git a/src/Common/Extensions/StringExtensions.cs b/src/Common/Extensions/StringExtensions.cs index 4dca069d..3689a960 100644 --- a/src/Common/Extensions/StringExtensions.cs +++ b/src/Common/Extensions/StringExtensions.cs @@ -1,33 +1,32 @@ using System; using System.Globalization; -namespace Common.Extensions +namespace Common.Extensions; + +public static class StringExtensions { - public static class StringExtensions + public static bool ContainsIgnoreCase(this string value, string searchFor) { - public static bool ContainsIgnoreCase(this string value, string searchFor) - { - return value.Contains(searchFor, StringComparison.OrdinalIgnoreCase); - } + return value.Contains(searchFor, StringComparison.OrdinalIgnoreCase); + } - public static bool EqualsIgnoreCase(this string value, string? matchThis) - { - return value.Equals(matchThis, StringComparison.OrdinalIgnoreCase); - } + public static bool EqualsIgnoreCase(this string value, string? matchThis) + { + return value.Equals(matchThis, StringComparison.OrdinalIgnoreCase); + } - public static float ToFloat(this string value) - { - return float.Parse(value, NumberStyles.Any, CultureInfo.InvariantCulture.NumberFormat); - } + public static float ToFloat(this string value) + { + return float.Parse(value, NumberStyles.Any, CultureInfo.InvariantCulture.NumberFormat); + } - public static decimal ToDecimal(this string value) - { - return decimal.Parse(value, NumberStyles.Any, CultureInfo.InvariantCulture.NumberFormat); - } + public static decimal ToDecimal(this string value) + { + return decimal.Parse(value, NumberStyles.Any, CultureInfo.InvariantCulture.NumberFormat); + } - public static string FormatWith(this string value, params object[] args) - { - return string.Format(value, args); - } + public static string FormatWith(this string value, params object[] args) + { + return string.Format(value, args); } } diff --git a/src/Common/JsonNetExtensions.cs b/src/Common/JsonNetExtensions.cs index 4a19b8d5..1bd10374 100644 --- a/src/Common/JsonNetExtensions.cs +++ b/src/Common/JsonNetExtensions.cs @@ -1,26 +1,25 @@ using System; using Newtonsoft.Json.Linq; -namespace Common +namespace Common; + +public static class JsonNetExtensions { - public static class JsonNetExtensions + public static JEnumerable Children(this JToken token, string key) + where T : JToken { - public static JEnumerable Children(this JToken token, string key) - where T : JToken - { - return token[key]?.Children() ?? JEnumerable.Empty; - } + return token[key]?.Children() ?? JEnumerable.Empty; + } - public static T ValueOrThrow(this JToken token, string key) - where T : class + public static T ValueOrThrow(this JToken token, string key) + where T : class + { + var value = token.Value(key); + if (value is null) { - var value = token.Value(key); - if (value is null) - { - throw new ArgumentNullException(token.Path); - } - - return value; + throw new ArgumentNullException(token.Path); } + + return value; } } diff --git a/src/Common/ResourceDataReader.cs b/src/Common/ResourceDataReader.cs index e03d4d22..6b232ac0 100644 --- a/src/Common/ResourceDataReader.cs +++ b/src/Common/ResourceDataReader.cs @@ -3,41 +3,40 @@ using System.IO; using System.Reflection; using System.Text; -namespace Common +namespace Common; + +public class ResourceDataReader { - public class ResourceDataReader + private readonly Assembly? _assembly; + private readonly string? _namespace; + private readonly string _subdirectory; + + public ResourceDataReader(Type typeWithNamespaceToUse, string subdirectory = "") { - private readonly Assembly? _assembly; - private readonly string? _namespace; - private readonly string _subdirectory; + _subdirectory = subdirectory; + _namespace = typeWithNamespaceToUse.Namespace; + _assembly = Assembly.GetAssembly(typeWithNamespaceToUse); + } - public ResourceDataReader(Type typeWithNamespaceToUse, string subdirectory = "") + public string ReadData(string filename) + { + var nameBuilder = new StringBuilder(); + nameBuilder.Append(_namespace); + if (!string.IsNullOrEmpty(_subdirectory)) { - _subdirectory = subdirectory; - _namespace = typeWithNamespaceToUse.Namespace; - _assembly = Assembly.GetAssembly(typeWithNamespaceToUse); + nameBuilder.Append($".{_subdirectory}"); } - public string ReadData(string filename) - { - var nameBuilder = new StringBuilder(); - nameBuilder.Append(_namespace); - if (!string.IsNullOrEmpty(_subdirectory)) - { - nameBuilder.Append($".{_subdirectory}"); - } + nameBuilder.Append($".{filename}"); - nameBuilder.Append($".{filename}"); - - var resourceName = nameBuilder.ToString(); - using var stream = _assembly?.GetManifestResourceStream(resourceName); - if (stream == null) - { - throw new ArgumentException($"Embedded resource not found: {resourceName}"); - } - - using var reader = new StreamReader(stream); - return reader.ReadToEnd(); + var resourceName = nameBuilder.ToString(); + using var stream = _assembly?.GetManifestResourceStream(resourceName); + if (stream == null) + { + throw new ArgumentException($"Embedded resource not found: {resourceName}"); } + + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); } } diff --git a/src/Common/YamlDotNet/CannotBeEmptyAttribute.cs b/src/Common/YamlDotNet/CannotBeEmptyAttribute.cs index f47c75ef..af7ccf94 100644 --- a/src/Common/YamlDotNet/CannotBeEmptyAttribute.cs +++ b/src/Common/YamlDotNet/CannotBeEmptyAttribute.cs @@ -2,16 +2,15 @@ using System; using System.Collections; using System.ComponentModel.DataAnnotations; -namespace Common.YamlDotNet +namespace Common.YamlDotNet; + +[AttributeUsage(AttributeTargets.Property)] +public sealed class CannotBeEmptyAttribute : RequiredAttribute { - [AttributeUsage(AttributeTargets.Property)] - public sealed class CannotBeEmptyAttribute : RequiredAttribute + public override bool IsValid(object? value) { - public override bool IsValid(object? value) - { - return base.IsValid(value) && - value is IEnumerable list && - list.GetEnumerator().MoveNext(); - } + return base.IsValid(value) && + value is IEnumerable list && + list.GetEnumerator().MoveNext(); } } diff --git a/src/Common/YamlDotNet/ValidatingDeserializer.cs b/src/Common/YamlDotNet/ValidatingDeserializer.cs index c45755be..3bbf4e3f 100644 --- a/src/Common/YamlDotNet/ValidatingDeserializer.cs +++ b/src/Common/YamlDotNet/ValidatingDeserializer.cs @@ -3,43 +3,42 @@ using System.ComponentModel.DataAnnotations; using YamlDotNet.Core; using YamlDotNet.Serialization; -namespace Common.YamlDotNet +namespace Common.YamlDotNet; + +internal class ValidatingDeserializer : INodeDeserializer { - internal class ValidatingDeserializer : INodeDeserializer + private readonly INodeDeserializer _nodeDeserializer; + + public ValidatingDeserializer(INodeDeserializer nodeDeserializer) { - private readonly INodeDeserializer _nodeDeserializer; + _nodeDeserializer = nodeDeserializer; + } - public ValidatingDeserializer(INodeDeserializer nodeDeserializer) + public bool Deserialize(IParser reader, Type expectedType, + Func nestedObjectDeserializer, out object? value) + { + if (!_nodeDeserializer.Deserialize(reader, expectedType, nestedObjectDeserializer, out value) || + value == null) { - _nodeDeserializer = nodeDeserializer; + return false; } - public bool Deserialize(IParser reader, Type expectedType, - Func nestedObjectDeserializer, out object? value) - { - if (!_nodeDeserializer.Deserialize(reader, expectedType, nestedObjectDeserializer, out value) || - value == null) - { - return false; - } - - var context = new ValidationContext(value, null, null); + var context = new ValidationContext(value, null, null); - try - { - Validator.ValidateObject(value, context, true); - } - catch (ValidationException e) + try + { + Validator.ValidateObject(value, context, true); + } + catch (ValidationException e) + { + if (reader.Current == null) { - if (reader.Current == null) - { - throw; - } - - throw new YamlException(reader.Current.Start, reader.Current.End, e.Message); + throw; } - return true; + throw new YamlException(reader.Current.Start, reader.Current.End, e.Message); } + + return true; } } diff --git a/src/Common/YamlDotNet/YamlDotNetExtensions.cs b/src/Common/YamlDotNet/YamlDotNetExtensions.cs index f008c6ea..a44377ad 100644 --- a/src/Common/YamlDotNet/YamlDotNetExtensions.cs +++ b/src/Common/YamlDotNet/YamlDotNetExtensions.cs @@ -2,29 +2,28 @@ using YamlDotNet.Serialization; using YamlDotNet.Serialization.NodeDeserializers; -namespace Common.YamlDotNet +namespace Common.YamlDotNet; + +public static class YamlDotNetExtensions { - public static class YamlDotNetExtensions + public static T? DeserializeType(this IDeserializer deserializer, string data) + where T : class { - public static T? DeserializeType(this IDeserializer deserializer, string data) - where T : class - { - var extractor = deserializer.Deserialize>(data); - return extractor.RootObject; - } + var extractor = deserializer.Deserialize>(data); + return extractor.RootObject; + } - public static DeserializerBuilder WithRequiredPropertyValidation(this DeserializerBuilder builder) - { - return builder - .WithNodeDeserializer(inner => new ValidatingDeserializer(inner), - s => s.InsteadOf()); - } + public static DeserializerBuilder WithRequiredPropertyValidation(this DeserializerBuilder builder) + { + return builder + .WithNodeDeserializer(inner => new ValidatingDeserializer(inner), + s => s.InsteadOf()); + } - [UsedImplicitly(ImplicitUseTargetFlags.Members)] - private sealed class RootExtractor - where T : class - { - public T? RootObject { get; } - } + [UsedImplicitly(ImplicitUseTargetFlags.Members)] + private sealed class RootExtractor + where T : class + { + public T? RootObject { get; } } } diff --git a/src/Common/YamlDotNet/YamlNullableEnumTypeConverter.cs b/src/Common/YamlDotNet/YamlNullableEnumTypeConverter.cs index 7d89bc6d..506e512d 100644 --- a/src/Common/YamlDotNet/YamlNullableEnumTypeConverter.cs +++ b/src/Common/YamlDotNet/YamlNullableEnumTypeConverter.cs @@ -3,70 +3,69 @@ using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; -namespace Common.YamlDotNet +namespace Common.YamlDotNet; + +// A workaround for nullable enums in YamlDotNet taken from: +// https://github.com/aaubry/YamlDotNet/issues/544#issuecomment-778062351 +public class YamlNullableEnumTypeConverter : IYamlTypeConverter { - // A workaround for nullable enums in YamlDotNet taken from: - // https://github.com/aaubry/YamlDotNet/issues/544#issuecomment-778062351 - public class YamlNullableEnumTypeConverter : IYamlTypeConverter + public bool Accepts(Type type) { - public bool Accepts(Type type) + return Nullable.GetUnderlyingType(type)?.IsEnum ?? false; + } + + public object? ReadYaml(IParser parser, Type type) + { + type = Nullable.GetUnderlyingType(type) ?? + throw new ArgumentException("Expected nullable enum type for ReadYaml"); + + if (parser.Accept(out var @event) && NodeIsNull(@event)) { - return Nullable.GetUnderlyingType(type)?.IsEnum ?? false; + parser.SkipThisAndNestedEvents(); + return null; } - public object? ReadYaml(IParser parser, Type type) + var scalar = parser.Consume(); + try { - type = Nullable.GetUnderlyingType(type) ?? - throw new ArgumentException("Expected nullable enum type for ReadYaml"); - - if (parser.Accept(out var @event) && NodeIsNull(@event)) - { - parser.SkipThisAndNestedEvents(); - return null; - } - - var scalar = parser.Consume(); - try - { - return Enum.Parse(type, scalar.Value, true); - } - catch (Exception ex) - { - throw new YamlException($"Invalid value: \"{scalar.Value}\" for {type.Name}", ex); - } + return Enum.Parse(type, scalar.Value, true); } - - public void WriteYaml(IEmitter emitter, object? value, Type type) + catch (Exception ex) { - type = Nullable.GetUnderlyingType(type) ?? - throw new ArgumentException("Expected nullable enum type for WriteYaml"); + throw new YamlException($"Invalid value: \"{scalar.Value}\" for {type.Name}", ex); + } + } - if (value == null) - { - return; - } + public void WriteYaml(IEmitter emitter, object? value, Type type) + { + type = Nullable.GetUnderlyingType(type) ?? + throw new ArgumentException("Expected nullable enum type for WriteYaml"); - var toWrite = Enum.GetName(type, value) ?? - throw new InvalidOperationException($"Invalid value {value} for enum: {type}"); - emitter.Emit(new Scalar(null!, null!, toWrite, ScalarStyle.Any, true, false)); + if (value == null) + { + return; } - private static bool NodeIsNull(NodeEvent nodeEvent) - { - // http://yaml.org/type/null.html + var toWrite = Enum.GetName(type, value) ?? + throw new InvalidOperationException($"Invalid value {value} for enum: {type}"); + emitter.Emit(new Scalar(null!, null!, toWrite, ScalarStyle.Any, true, false)); + } - if (nodeEvent.Tag == "tag:yaml.org,2002:null") - { - return true; - } + private static bool NodeIsNull(NodeEvent nodeEvent) + { + // http://yaml.org/type/null.html - if (nodeEvent is not Scalar {Style: ScalarStyle.Plain} scalar) - { - return false; - } + if (nodeEvent.Tag == "tag:yaml.org,2002:null") + { + return true; + } - var value = scalar.Value; - return value is "" or "~" or "null" or "Null" or "NULL"; + if (nodeEvent is not Scalar {Style: ScalarStyle.Plain} scalar) + { + return false; } + + var value = scalar.Value; + return value is "" or "~" or "null" or "Null" or "NULL"; } } diff --git a/src/TestLibrary.Tests/StreamBuilderTest.cs b/src/TestLibrary.Tests/StreamBuilderTest.cs index d340ce13..0deaea0c 100644 --- a/src/TestLibrary.Tests/StreamBuilderTest.cs +++ b/src/TestLibrary.Tests/StreamBuilderTest.cs @@ -1,17 +1,16 @@ using FluentAssertions; using NUnit.Framework; -namespace TestLibrary.Tests +namespace TestLibrary.Tests; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class StreamBuilderTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class StreamBuilderTest + [Test] + public void FromString_UsingString_ShouldOutputSameString() { - [Test] - public void FromString_UsingString_ShouldOutputSameString() - { - var stream = StreamBuilder.FromString("test"); - stream.ReadToEnd().Should().Be("test"); - } + var stream = StreamBuilder.FromString("test"); + stream.ReadToEnd().Should().Be("test"); } } diff --git a/src/TestLibrary.Tests/StringUtilsTest.cs b/src/TestLibrary.Tests/StringUtilsTest.cs index b6216280..009c6468 100644 --- a/src/TestLibrary.Tests/StringUtilsTest.cs +++ b/src/TestLibrary.Tests/StringUtilsTest.cs @@ -1,17 +1,16 @@ using FluentAssertions; using NUnit.Framework; -namespace TestLibrary.Tests +namespace TestLibrary.Tests; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class StringUtilsTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class StringUtilsTest + [Test] + public void TrimmedString_Newlines_AreStripped() { - [Test] - public void TrimmedString_Newlines_AreStripped() - { - var testStr = "\r\ntest\r\n"; - StringUtils.TrimmedString(testStr).Should().Be("test"); - } + var testStr = "\r\ntest\r\n"; + StringUtils.TrimmedString(testStr).Should().Be("test"); } } diff --git a/src/TestLibrary/FluentAssertions/JsonEquivalencyStep.cs b/src/TestLibrary/FluentAssertions/JsonEquivalencyStep.cs index 99dab8e4..51e7d070 100644 --- a/src/TestLibrary/FluentAssertions/JsonEquivalencyStep.cs +++ b/src/TestLibrary/FluentAssertions/JsonEquivalencyStep.cs @@ -2,23 +2,22 @@ using FluentAssertions.Equivalency; using FluentAssertions.Json; using Newtonsoft.Json.Linq; -namespace TestLibrary.FluentAssertions +namespace TestLibrary.FluentAssertions; + +public class JsonEquivalencyStep : IEquivalencyStep { - public class JsonEquivalencyStep : IEquivalencyStep + public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationContext context, + IEquivalencyValidator nestedValidator) { - public EquivalencyResult Handle(Comparands comparands, IEquivalencyValidationContext context, - IEquivalencyValidator nestedValidator) + var canHandle = comparands.Subject?.GetType().IsAssignableTo(typeof(JToken)) ?? false; + if (!canHandle) { - var canHandle = comparands.Subject?.GetType().IsAssignableTo(typeof(JToken)) ?? false; - if (!canHandle) - { - return EquivalencyResult.ContinueWithNext; - } + return EquivalencyResult.ContinueWithNext; + } - ((JToken) comparands.Subject!).Should().BeEquivalentTo( - (JToken) comparands.Expectation, context.Reason.FormattedMessage, context.Reason.Arguments); + ((JToken) comparands.Subject!).Should().BeEquivalentTo( + (JToken) comparands.Expectation, context.Reason.FormattedMessage, context.Reason.Arguments); - return EquivalencyResult.AssertionCompleted; - } + return EquivalencyResult.AssertionCompleted; } } diff --git a/src/TestLibrary/NSubstitute/Verify.cs b/src/TestLibrary/NSubstitute/Verify.cs index 3f00a12c..c5fc045b 100644 --- a/src/TestLibrary/NSubstitute/Verify.cs +++ b/src/TestLibrary/NSubstitute/Verify.cs @@ -4,38 +4,37 @@ using System.Linq; using FluentAssertions.Execution; using NSubstitute.Core.Arguments; -namespace TestLibrary.NSubstitute +namespace TestLibrary.NSubstitute; + +public static class Verify { - public static class Verify + public static T That(Action action) { - public static T That(Action action) + return ArgumentMatcher.Enqueue(new AssertionMatcher(action)); + } + + private class AssertionMatcher : IArgumentMatcher + { + private readonly Action _assertion; + + public AssertionMatcher(Action assertion) { - return ArgumentMatcher.Enqueue(new AssertionMatcher(action)); + _assertion = assertion; } - private class AssertionMatcher : IArgumentMatcher + public bool IsSatisfiedBy(T argument) { - private readonly Action _assertion; + using var scope = new AssertionScope(); + _assertion(argument); - public AssertionMatcher(Action assertion) + var failures = scope.Discard().ToList(); + if (failures.Count == 0) { - _assertion = assertion; + return true; } - public bool IsSatisfiedBy(T argument) - { - using var scope = new AssertionScope(); - _assertion(argument); - - var failures = scope.Discard().ToList(); - if (failures.Count == 0) - { - return true; - } - - failures.ForEach(x => Trace.WriteLine(x)); - return false; - } + failures.ForEach(x => Trace.WriteLine(x)); + return false; } } } diff --git a/src/TestLibrary/StreamBuilder.cs b/src/TestLibrary/StreamBuilder.cs index d0bc7338..0e78d2b1 100644 --- a/src/TestLibrary/StreamBuilder.cs +++ b/src/TestLibrary/StreamBuilder.cs @@ -1,14 +1,13 @@ using System.IO; using System.Text; -namespace TestLibrary +namespace TestLibrary; + +public static class StreamBuilder { - public static class StreamBuilder + public static StreamReader FromString(string data) { - public static StreamReader FromString(string data) - { - var stream = new MemoryStream(Encoding.UTF8.GetBytes(data)); - return new StreamReader(stream); - } + var stream = new MemoryStream(Encoding.UTF8.GetBytes(data)); + return new StreamReader(stream); } } diff --git a/src/TestLibrary/StringUtils.cs b/src/TestLibrary/StringUtils.cs index 881723f5..7c22f939 100644 --- a/src/TestLibrary/StringUtils.cs +++ b/src/TestLibrary/StringUtils.cs @@ -1,7 +1,6 @@ -namespace TestLibrary +namespace TestLibrary; + +public static class StringUtils { - public static class StringUtils - { - public static string TrimmedString(string value) => value.Trim('\r', '\n'); - } + public static string TrimmedString(string value) => value.Trim('\r', '\n'); } diff --git a/src/Trash.Tests/Command/CreateConfigCommandTest.cs b/src/Trash.Tests/Command/CreateConfigCommandTest.cs index 01e65823..2d288c09 100644 --- a/src/Trash.Tests/Command/CreateConfigCommandTest.cs +++ b/src/Trash.Tests/Command/CreateConfigCommandTest.cs @@ -8,39 +8,38 @@ using Trash.Command; // ReSharper disable MethodHasAsyncOverload -namespace Trash.Tests.Command +namespace Trash.Tests.Command; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class CreateConfigCommandTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class CreateConfigCommandTest + [Test] + public async Task CreateConfig_DefaultPath_FileIsCreated() { - [Test] - public async Task CreateConfig_DefaultPath_FileIsCreated() - { - var logger = Substitute.For(); - var filesystem = Substitute.For(); - var cmd = new CreateConfigCommand(logger, filesystem); + var logger = Substitute.For(); + var filesystem = Substitute.For(); + var cmd = new CreateConfigCommand(logger, filesystem); - await cmd.ExecuteAsync(Substitute.For()).ConfigureAwait(false); + await cmd.ExecuteAsync(Substitute.For()).ConfigureAwait(false); - filesystem.File.Received().Exists(Arg.Is(s => s.EndsWith("trash.yml"))); - filesystem.File.Received().WriteAllText(Arg.Is(s => s.EndsWith("trash.yml")), Arg.Any()); - } + filesystem.File.Received().Exists(Arg.Is(s => s.EndsWith("trash.yml"))); + filesystem.File.Received().WriteAllText(Arg.Is(s => s.EndsWith("trash.yml")), Arg.Any()); + } - [Test] - public async Task CreateConfig_SpecifyPath_FileIsCreated() + [Test] + public async Task CreateConfig_SpecifyPath_FileIsCreated() + { + var logger = Substitute.For(); + var filesystem = Substitute.For(); + var cmd = new CreateConfigCommand(logger, filesystem) { - var logger = Substitute.For(); - var filesystem = Substitute.For(); - var cmd = new CreateConfigCommand(logger, filesystem) - { - Path = "some/other/path.yml" - }; - - await cmd.ExecuteAsync(Substitute.For()).ConfigureAwait(false); - - filesystem.File.Received().Exists(Arg.Is("some/other/path.yml")); - filesystem.File.Received().WriteAllText(Arg.Is("some/other/path.yml"), Arg.Any()); - } + Path = "some/other/path.yml" + }; + + await cmd.ExecuteAsync(Substitute.For()).ConfigureAwait(false); + + filesystem.File.Received().Exists(Arg.Is("some/other/path.yml")); + filesystem.File.Received().WriteAllText(Arg.Is("some/other/path.yml"), Arg.Any()); } } diff --git a/src/Trash.Tests/Command/Helpers/CliTypeActivatorTest.cs b/src/Trash.Tests/Command/Helpers/CliTypeActivatorTest.cs index ebfaddbe..6275fc64 100644 --- a/src/Trash.Tests/Command/Helpers/CliTypeActivatorTest.cs +++ b/src/Trash.Tests/Command/Helpers/CliTypeActivatorTest.cs @@ -5,53 +5,52 @@ using FluentAssertions; using NUnit.Framework; using Trash.Command.Helpers; -namespace Trash.Tests.Command.Helpers +namespace Trash.Tests.Command.Helpers; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class CliTypeActivatorTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class CliTypeActivatorTest + private class NonServiceCommandType + { + } + + private class StubCommand : IServiceCommand { - private class NonServiceCommandType - { - } - - private class StubCommand : IServiceCommand - { - public bool Preview => false; - public bool Debug => false; - public ICollection? Config => null; - public string CacheStoragePath => ""; - } - - [Test] - public void Resolve_NonServiceCommandType_NoActiveCommandSet() - { - var builder = new ContainerBuilder(); - builder.RegisterType(); - var container = CompositionRoot.Setup(builder); - - var createdType = CliTypeActivator.ResolveType(container, typeof(NonServiceCommandType)); - - Action act = () => _ = container.Resolve().ActiveCommand; - - createdType.Should().BeOfType(); - act.Should() - .Throw() - .WithMessage("The active command has not yet been determined"); - } - - [Test] - public void Resolve_ServiceCommandType_ActiveCommandSet() - { - var builder = new ContainerBuilder(); - builder.RegisterType(); - var container = CompositionRoot.Setup(builder); - - var createdType = CliTypeActivator.ResolveType(container, typeof(StubCommand)); - var activeCommand = container.Resolve().ActiveCommand; - - activeCommand.Should().BeSameAs(createdType); - activeCommand.Should().BeOfType(); - } + public bool Preview => false; + public bool Debug => false; + public ICollection? Config => null; + public string CacheStoragePath => ""; + } + + [Test] + public void Resolve_NonServiceCommandType_NoActiveCommandSet() + { + var builder = new ContainerBuilder(); + builder.RegisterType(); + var container = CompositionRoot.Setup(builder); + + var createdType = CliTypeActivator.ResolveType(container, typeof(NonServiceCommandType)); + + Action act = () => _ = container.Resolve().ActiveCommand; + + createdType.Should().BeOfType(); + act.Should() + .Throw() + .WithMessage("The active command has not yet been determined"); + } + + [Test] + public void Resolve_ServiceCommandType_ActiveCommandSet() + { + var builder = new ContainerBuilder(); + builder.RegisterType(); + var container = CompositionRoot.Setup(builder); + + var createdType = CliTypeActivator.ResolveType(container, typeof(StubCommand)); + var activeCommand = container.Resolve().ActiveCommand; + + activeCommand.Should().BeSameAs(createdType); + activeCommand.Should().BeOfType(); } } diff --git a/src/Trash.Tests/CompositionRootTest.cs b/src/Trash.Tests/CompositionRootTest.cs index 8c6149d3..3f6fcb9b 100644 --- a/src/Trash.Tests/CompositionRootTest.cs +++ b/src/Trash.Tests/CompositionRootTest.cs @@ -5,37 +5,36 @@ using Autofac.Core; using FluentAssertions; using NUnit.Framework; -namespace Trash.Tests +namespace Trash.Tests; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class CompositionRootTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class CompositionRootTest + private sealed class ConcreteTypeEnumerator : IEnumerable { - private sealed class ConcreteTypeEnumerator : IEnumerable - { - private readonly IContainer _container; + private readonly IContainer _container; - public ConcreteTypeEnumerator() - { - _container = CompositionRoot.Setup(); - } - - public IEnumerator GetEnumerator() - { - return _container.ComponentRegistry.Registrations - .SelectMany(x => x.Services) - .OfType() - .GetEnumerator(); - } + public ConcreteTypeEnumerator() + { + _container = CompositionRoot.Setup(); } - [TestCaseSource(typeof(ConcreteTypeEnumerator))] - public void Resolve_ICommandConcreteClasses(Service service) + public IEnumerator GetEnumerator() { - using var container = CompositionRoot.Setup(); - container.Invoking(c => c.ResolveService(service)) - .Should().NotThrow() - .And.NotBeNull(); + return _container.ComponentRegistry.Registrations + .SelectMany(x => x.Services) + .OfType() + .GetEnumerator(); } } + + [TestCaseSource(typeof(ConcreteTypeEnumerator))] + public void Resolve_ICommandConcreteClasses(Service service) + { + using var container = CompositionRoot.Setup(); + container.Invoking(c => c.ResolveService(service)) + .Should().NotThrow() + .And.NotBeNull(); + } } diff --git a/src/Trash.Tests/Config/ConfigurationLoaderTest.cs b/src/Trash.Tests/Config/ConfigurationLoaderTest.cs index 6340a2c6..7e016f72 100644 --- a/src/Trash.Tests/Config/ConfigurationLoaderTest.cs +++ b/src/Trash.Tests/Config/ConfigurationLoaderTest.cs @@ -19,157 +19,156 @@ using TrashLib.Sonarr.Config; using TrashLib.Sonarr.ReleaseProfile; using YamlDotNet.Serialization.ObjectFactories; -namespace Trash.Tests.Config +namespace Trash.Tests.Config; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class ConfigurationLoaderTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class ConfigurationLoaderTest + private static TextReader GetResourceData(string file) { - private static TextReader GetResourceData(string file) - { - var testData = new ResourceDataReader(typeof(ConfigurationLoaderTest), "Data"); - return new StringReader(testData.ReadData(file)); - } + var testData = new ResourceDataReader(typeof(ConfigurationLoaderTest), "Data"); + return new StringReader(testData.ReadData(file)); + } - [SuppressMessage("Microsoft.Design", "CA1034", - Justification = "YamlDotNet requires this type to be public so it may access it")] - public class TestConfig : IServiceConfiguration + [SuppressMessage("Microsoft.Design", "CA1034", + Justification = "YamlDotNet requires this type to be public so it may access it")] + public class TestConfig : IServiceConfiguration + { + public string BaseUrl => ""; + public string ApiKey => ""; + } + + [Test] + public void Load_many_iterations_of_config() + { + static StreamReader MockYaml(params object[] args) { - public string BaseUrl => ""; - public string ApiKey => ""; + var str = new StringBuilder("sonarr:"); + const string templateYaml = "\n - base_url: {0}\n api_key: abc"; + str.Append(args.Aggregate("", (current, p) => current + templateYaml.FormatWith(p))); + return StreamBuilder.FromString(str.ToString()); } - [Test] - public void Load_many_iterations_of_config() - { - static StreamReader MockYaml(params object[] args) - { - var str = new StringBuilder("sonarr:"); - const string templateYaml = "\n - base_url: {0}\n api_key: abc"; - str.Append(args.Aggregate("", (current, p) => current + templateYaml.FormatWith(p))); - return StreamBuilder.FromString(str.ToString()); - } - - var fs = Substitute.For(); - fs.File.OpenText(Arg.Any()) - .Returns(MockYaml(1, 2), MockYaml(3)); - - var provider = Substitute.For(); - // var objectFactory = Substitute.For(); - // objectFactory.Create(Arg.Any()) - // .Returns(t => Substitute.For(new[] {(Type)t[0]}, Array.Empty())); - - var actualActiveConfigs = new List(); + var fs = Substitute.For(); + fs.File.OpenText(Arg.Any()) + .Returns(MockYaml(1, 2), MockYaml(3)); + + var provider = Substitute.For(); + // var objectFactory = Substitute.For(); + // objectFactory.Create(Arg.Any()) + // .Returns(t => Substitute.For(new[] {(Type)t[0]}, Array.Empty())); + + var actualActiveConfigs = new List(); #pragma warning disable NS1004 - provider.ActiveConfiguration = Arg.Do(a => actualActiveConfigs.Add(a)); + provider.ActiveConfiguration = Arg.Do(a => actualActiveConfigs.Add(a)); #pragma warning restore NS1004 - var validator = Substitute.For>(); - var loader = - new ConfigurationLoader(provider, fs, new DefaultObjectFactory(), validator); + var validator = Substitute.For>(); + var loader = + new ConfigurationLoader(provider, fs, new DefaultObjectFactory(), validator); - var fakeFiles = new List - { - "config1.yml", - "config2.yml" - }; + var fakeFiles = new List + { + "config1.yml", + "config2.yml" + }; - var expected = new List - { - new() {ApiKey = "abc", BaseUrl = "1"}, - new() {ApiKey = "abc", BaseUrl = "2"}, - new() {ApiKey = "abc", BaseUrl = "3"} - }; + var expected = new List + { + new() {ApiKey = "abc", BaseUrl = "1"}, + new() {ApiKey = "abc", BaseUrl = "2"}, + new() {ApiKey = "abc", BaseUrl = "3"} + }; - var actual = loader.LoadMany(fakeFiles, "sonarr").ToList(); + var actual = loader.LoadMany(fakeFiles, "sonarr").ToList(); - actual.Should().BeEquivalentTo(expected); - actualActiveConfigs.Should().BeEquivalentTo(expected, op => op.WithoutStrictOrdering()); - } + actual.Should().BeEquivalentTo(expected); + actualActiveConfigs.Should().BeEquivalentTo(expected, op => op.WithoutStrictOrdering()); + } - [Test] - public void Parse_using_stream() - { - var validator = Substitute.For>(); - var configLoader = new ConfigurationLoader( - Substitute.For(), - Substitute.For(), - new DefaultObjectFactory(), - validator); + [Test] + public void Parse_using_stream() + { + var validator = Substitute.For>(); + var configLoader = new ConfigurationLoader( + Substitute.For(), + Substitute.For(), + new DefaultObjectFactory(), + validator); - var configs = configLoader.LoadFromStream(GetResourceData("Load_UsingStream_CorrectParsing.yml"), "sonarr"); + var configs = configLoader.LoadFromStream(GetResourceData("Load_UsingStream_CorrectParsing.yml"), "sonarr"); - configs.Should() - .BeEquivalentTo(new List + configs.Should() + .BeEquivalentTo(new List + { + new() { - new() + ApiKey = "95283e6b156c42f3af8a9b16173f876b", + BaseUrl = "http://localhost:8989", + ReleaseProfiles = new List { - ApiKey = "95283e6b156c42f3af8a9b16173f876b", - BaseUrl = "http://localhost:8989", - ReleaseProfiles = new List + new() { - new() - { - Type = ReleaseProfileType.Anime, - StrictNegativeScores = true, - Tags = new List {"anime"} - }, - new() + Type = ReleaseProfileType.Anime, + StrictNegativeScores = true, + Tags = new List {"anime"} + }, + new() + { + Type = ReleaseProfileType.Series, + StrictNegativeScores = false, + Tags = new List { - Type = ReleaseProfileType.Series, - StrictNegativeScores = false, - Tags = new List - { - "tv", - "series" - } + "tv", + "series" } } } - }); - } + } + }); + } - [Test] - public void Throw_when_validation_fails() + [Test] + public void Throw_when_validation_fails() + { + var validator = Substitute.For>(); + var configLoader = new ConfigurationLoader( + Substitute.For(), + Substitute.For(), + new DefaultObjectFactory(), + validator); + + // force the validator to return a validation error + validator.Validate(Arg.Any()).Returns(new ValidationResult { - var validator = Substitute.For>(); - var configLoader = new ConfigurationLoader( - Substitute.For(), - Substitute.For(), - new DefaultObjectFactory(), - validator); - - // force the validator to return a validation error - validator.Validate(Arg.Any()).Returns(new ValidationResult - { - Errors = {new ValidationFailure("PropertyName", "Test Validation Failure")} - }); + Errors = {new ValidationFailure("PropertyName", "Test Validation Failure")} + }); - var testYml = @" + var testYml = @" fubar: - api_key: abc "; - Action act = () => configLoader.LoadFromStream(new StringReader(testYml), "fubar"); + Action act = () => configLoader.LoadFromStream(new StringReader(testYml), "fubar"); - act.Should().Throw(); - } + act.Should().Throw(); + } - [Test] - public void Validation_success_does_not_throw() - { - var validator = Substitute.For>(); - var configLoader = new ConfigurationLoader( - Substitute.For(), - Substitute.For(), - new DefaultObjectFactory(), - validator); - - var testYml = @" + [Test] + public void Validation_success_does_not_throw() + { + var validator = Substitute.For>(); + var configLoader = new ConfigurationLoader( + Substitute.For(), + Substitute.For(), + new DefaultObjectFactory(), + validator); + + var testYml = @" fubar: - api_key: abc "; - Action act = () => configLoader.LoadFromStream(new StringReader(testYml), "fubar"); - act.Should().NotThrow(); - } + Action act = () => configLoader.LoadFromStream(new StringReader(testYml), "fubar"); + act.Should().NotThrow(); } } diff --git a/src/Trash.Tests/LogJanitorTest.cs b/src/Trash.Tests/LogJanitorTest.cs index c72957a1..f0f4d335 100644 --- a/src/Trash.Tests/LogJanitorTest.cs +++ b/src/Trash.Tests/LogJanitorTest.cs @@ -2,40 +2,39 @@ using System.IO.Abstractions; using NSubstitute; using NUnit.Framework; -namespace Trash.Tests +namespace Trash.Tests; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class LogJanitorTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class LogJanitorTest + [Test] + public void Keep_correct_number_of_newest_log_files() { - [Test] - public void Keep_correct_number_of_newest_log_files() - { - var fs = Substitute.For(); - var janitor = new LogJanitor(fs); + var fs = Substitute.For(); + var janitor = new LogJanitor(fs); - var testFileInfoList = new[] - { - Substitute.For(), - Substitute.For(), - Substitute.For(), - Substitute.For() - }; + var testFileInfoList = new[] + { + Substitute.For(), + Substitute.For(), + Substitute.For(), + Substitute.For() + }; - testFileInfoList[0].Name.Returns("trash_2021-05-15_19-00-00"); - testFileInfoList[1].Name.Returns("trash_2021-05-15_20-00-00"); - testFileInfoList[2].Name.Returns("trash_2021-05-15_21-00-00"); - testFileInfoList[3].Name.Returns("trash_2021-05-15_22-00-00"); + testFileInfoList[0].Name.Returns("trash_2021-05-15_19-00-00"); + testFileInfoList[1].Name.Returns("trash_2021-05-15_20-00-00"); + testFileInfoList[2].Name.Returns("trash_2021-05-15_21-00-00"); + testFileInfoList[3].Name.Returns("trash_2021-05-15_22-00-00"); - fs.DirectoryInfo.FromDirectoryName(Arg.Any()).GetFiles() - .Returns(testFileInfoList); + fs.DirectoryInfo.FromDirectoryName(Arg.Any()).GetFiles() + .Returns(testFileInfoList); - janitor.DeleteOldestLogFiles(2); + janitor.DeleteOldestLogFiles(2); - testFileInfoList[0].Received().Delete(); - testFileInfoList[1].Received().Delete(); - testFileInfoList[2].DidNotReceive().Delete(); - testFileInfoList[3].DidNotReceive().Delete(); - } + testFileInfoList[0].Received().Delete(); + testFileInfoList[1].Received().Delete(); + testFileInfoList[2].DidNotReceive().Delete(); + testFileInfoList[3].DidNotReceive().Delete(); } } diff --git a/src/Trash/AppPaths.cs b/src/Trash/AppPaths.cs index b65001ed..abd4be7d 100644 --- a/src/Trash/AppPaths.cs +++ b/src/Trash/AppPaths.cs @@ -1,17 +1,16 @@ using System; using System.IO; -namespace Trash +namespace Trash; + +internal static class AppPaths { - internal static class AppPaths - { - public static string AppDataPath { get; } = - Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "trash-updater"); + public static string AppDataPath { get; } = + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "trash-updater"); - public static string DefaultConfigPath { get; } = Path.Combine(AppContext.BaseDirectory, "trash.yml"); + public static string DefaultConfigPath { get; } = Path.Combine(AppContext.BaseDirectory, "trash.yml"); - public static string LogDirectory { get; } = Path.Combine(AppDataPath, "logs"); + public static string LogDirectory { get; } = Path.Combine(AppDataPath, "logs"); - public static string RepoDirectory { get; } = Path.Combine(AppDataPath, "repo"); - } + public static string RepoDirectory { get; } = Path.Combine(AppDataPath, "repo"); } diff --git a/src/Trash/Command/CreateConfigCommand.cs b/src/Trash/Command/CreateConfigCommand.cs index b67c53fb..7d97115c 100644 --- a/src/Trash/Command/CreateConfigCommand.cs +++ b/src/Trash/Command/CreateConfigCommand.cs @@ -8,42 +8,41 @@ using Common; using JetBrains.Annotations; using Serilog; -namespace Trash.Command +namespace Trash.Command; + +[Command("create-config", Description = "Create a starter YAML configuration file")] +[UsedImplicitly] +public class CreateConfigCommand : ICommand { - [Command("create-config", Description = "Create a starter YAML configuration file")] - [UsedImplicitly] - public class CreateConfigCommand : ICommand + private readonly IFileSystem _fileSystem; + + public CreateConfigCommand(ILogger logger, IFileSystem fileSystem) { - private readonly IFileSystem _fileSystem; + Log = logger; + _fileSystem = fileSystem; + } - public CreateConfigCommand(ILogger logger, IFileSystem fileSystem) - { - Log = logger; - _fileSystem = fileSystem; - } + private ILogger Log { get; } - private ILogger Log { get; } + [CommandOption("path", 'p', Description = + "Path where the new YAML file should be created. Must include the filename (e.g. path/to/config.yml). " + + "File must not already exist. If not specified, uses the default path of `trash.yml` right next to the " + + "executable.")] + public string Path { get; [UsedImplicitly] set; } = AppPaths.DefaultConfigPath; - [CommandOption("path", 'p', Description = - "Path where the new YAML file should be created. Must include the filename (e.g. path/to/config.yml). " + - "File must not already exist. If not specified, uses the default path of `trash.yml` right next to the " + - "executable.")] - public string Path { get; [UsedImplicitly] set; } = AppPaths.DefaultConfigPath; + public ValueTask ExecuteAsync(IConsole console) + { + var reader = new ResourceDataReader(typeof(Program)); + var ymlData = reader.ReadData("trash-config-template.yml"); - public ValueTask ExecuteAsync(IConsole console) + if (_fileSystem.File.Exists(Path)) { - var reader = new ResourceDataReader(typeof(Program)); - var ymlData = reader.ReadData("trash-config-template.yml"); - - if (_fileSystem.File.Exists(Path)) - { - throw new CommandException($"The file {Path} already exists. Please choose another path or " + - "delete/move the existing file and run this command again."); - } - - _fileSystem.File.WriteAllText(Path, ymlData); - Log.Information("Created configuration at: {Path}", Path); - return default; + throw new CommandException($"The file {Path} already exists. Please choose another path or " + + "delete/move the existing file and run this command again."); } + + _fileSystem.File.WriteAllText(Path, ymlData); + Log.Information("Created configuration at: {Path}", Path); + return default; } } diff --git a/src/Trash/Command/Helpers/ActiveServiceCommandProvider.cs b/src/Trash/Command/Helpers/ActiveServiceCommandProvider.cs index d42a470d..40101b77 100644 --- a/src/Trash/Command/Helpers/ActiveServiceCommandProvider.cs +++ b/src/Trash/Command/Helpers/ActiveServiceCommandProvider.cs @@ -1,16 +1,15 @@ using System; -namespace Trash.Command.Helpers +namespace Trash.Command.Helpers; + +public class ActiveServiceCommandProvider : IActiveServiceCommandProvider { - public class ActiveServiceCommandProvider : IActiveServiceCommandProvider - { - private IServiceCommand? _activeCommand; + private IServiceCommand? _activeCommand; - public IServiceCommand ActiveCommand - { - get => _activeCommand ?? - throw new InvalidOperationException("The active command has not yet been determined"); - set => _activeCommand = value; - } + public IServiceCommand ActiveCommand + { + get => _activeCommand ?? + throw new InvalidOperationException("The active command has not yet been determined"); + set => _activeCommand = value; } } diff --git a/src/Trash/Command/Helpers/CacheStoragePath.cs b/src/Trash/Command/Helpers/CacheStoragePath.cs index dce23aa8..d5e9705f 100644 --- a/src/Trash/Command/Helpers/CacheStoragePath.cs +++ b/src/Trash/Command/Helpers/CacheStoragePath.cs @@ -1,16 +1,15 @@ using TrashLib.Cache; -namespace Trash.Command.Helpers -{ - public class CacheStoragePath : ICacheStoragePath - { - private readonly IActiveServiceCommandProvider _serviceCommandProvider; +namespace Trash.Command.Helpers; - public CacheStoragePath(IActiveServiceCommandProvider serviceCommandProvider) - { - _serviceCommandProvider = serviceCommandProvider; - } +public class CacheStoragePath : ICacheStoragePath +{ + private readonly IActiveServiceCommandProvider _serviceCommandProvider; - public string Path => _serviceCommandProvider.ActiveCommand.CacheStoragePath; + public CacheStoragePath(IActiveServiceCommandProvider serviceCommandProvider) + { + _serviceCommandProvider = serviceCommandProvider; } + + public string Path => _serviceCommandProvider.ActiveCommand.CacheStoragePath; } diff --git a/src/Trash/Command/Helpers/CliTypeActivator.cs b/src/Trash/Command/Helpers/CliTypeActivator.cs index 939e1e62..7afd2bd4 100644 --- a/src/Trash/Command/Helpers/CliTypeActivator.cs +++ b/src/Trash/Command/Helpers/CliTypeActivator.cs @@ -1,20 +1,19 @@ using System; using Autofac; -namespace Trash.Command.Helpers +namespace Trash.Command.Helpers; + +internal static class CliTypeActivator { - internal static class CliTypeActivator + public static object ResolveType(IContainer container, Type typeToResolve) { - public static object ResolveType(IContainer container, Type typeToResolve) + var instance = container.Resolve(typeToResolve); + if (instance.GetType().IsAssignableTo()) { - var instance = container.Resolve(typeToResolve); - if (instance.GetType().IsAssignableTo()) - { - var activeServiceProvider = container.Resolve(); - activeServiceProvider.ActiveCommand = (IServiceCommand) instance; - } - - return instance; + var activeServiceProvider = container.Resolve(); + activeServiceProvider.ActiveCommand = (IServiceCommand) instance; } + + return instance; } } diff --git a/src/Trash/Command/Helpers/ExitCode.cs b/src/Trash/Command/Helpers/ExitCode.cs index a1f1be28..3d29e44f 100644 --- a/src/Trash/Command/Helpers/ExitCode.cs +++ b/src/Trash/Command/Helpers/ExitCode.cs @@ -1,8 +1,7 @@ -namespace Trash.Command.Helpers +namespace Trash.Command.Helpers; + +public enum ExitCode { - public enum ExitCode - { - Success = 0, - Failure = 1 - } + Success = 0, + Failure = 1 } diff --git a/src/Trash/Command/Helpers/IActiveServiceCommandProvider.cs b/src/Trash/Command/Helpers/IActiveServiceCommandProvider.cs index f152ce58..7aa79fea 100644 --- a/src/Trash/Command/Helpers/IActiveServiceCommandProvider.cs +++ b/src/Trash/Command/Helpers/IActiveServiceCommandProvider.cs @@ -1,7 +1,6 @@ -namespace Trash.Command.Helpers +namespace Trash.Command.Helpers; + +public interface IActiveServiceCommandProvider { - public interface IActiveServiceCommandProvider - { - IServiceCommand ActiveCommand { get; set; } - } + IServiceCommand ActiveCommand { get; set; } } diff --git a/src/Trash/Command/Helpers/IServiceCommand.cs b/src/Trash/Command/Helpers/IServiceCommand.cs index 4d217c49..7b442331 100644 --- a/src/Trash/Command/Helpers/IServiceCommand.cs +++ b/src/Trash/Command/Helpers/IServiceCommand.cs @@ -1,12 +1,11 @@ using System.Collections.Generic; -namespace Trash.Command.Helpers +namespace Trash.Command.Helpers; + +public interface IServiceCommand { - public interface IServiceCommand - { - bool Preview { get; } - bool Debug { get; } - ICollection? Config { get; } - string CacheStoragePath { get; } - } + bool Preview { get; } + bool Debug { get; } + ICollection? Config { get; } + string CacheStoragePath { get; } } diff --git a/src/Trash/Command/Helpers/ServiceCommand.cs b/src/Trash/Command/Helpers/ServiceCommand.cs index 4534096c..e6fcd044 100644 --- a/src/Trash/Command/Helpers/ServiceCommand.cs +++ b/src/Trash/Command/Helpers/ServiceCommand.cs @@ -15,109 +15,108 @@ using Serilog.Events; using TrashLib.Extensions; using YamlDotNet.Core; -namespace Trash.Command.Helpers +namespace Trash.Command.Helpers; + +public abstract class ServiceCommand : ICommand, IServiceCommand { - public abstract class ServiceCommand : ICommand, IServiceCommand + private readonly ILogger _log; + private readonly LoggingLevelSwitch _loggingLevelSwitch; + private readonly ILogJanitor _logJanitor; + + protected ServiceCommand( + ILogger log, + LoggingLevelSwitch loggingLevelSwitch, + ILogJanitor logJanitor) { - private readonly ILogger _log; - private readonly LoggingLevelSwitch _loggingLevelSwitch; - private readonly ILogJanitor _logJanitor; - - protected ServiceCommand( - ILogger log, - LoggingLevelSwitch loggingLevelSwitch, - ILogJanitor logJanitor) + _loggingLevelSwitch = loggingLevelSwitch; + _logJanitor = logJanitor; + _log = log; + } + + public async ValueTask ExecuteAsync(IConsole console) + { + SetupLogging(); + SetupHttp(); + + try { - _loggingLevelSwitch = loggingLevelSwitch; - _logJanitor = logJanitor; - _log = log; + await Process(); } - - public async ValueTask ExecuteAsync(IConsole console) + catch (YamlException e) { - SetupLogging(); - SetupHttp(); - - try - { - await Process(); - } - catch (YamlException e) + var inner = e.InnerException; + if (inner == null) { - var inner = e.InnerException; - if (inner == null) - { - throw; - } - - _log.Error("Found Unrecognized YAML Property: {ErrorMsg}", inner.Message); - _log.Error("Please remove the property quoted in the above message from your YAML file"); - throw new CommandException("Exiting due to invalid configuration"); - } - catch (Exception e) when (e is not CommandException) - { - _log.Error(e, "Unrecoverable Exception"); - ExitDueToFailure(); - } - finally - { - CleanupOldLogFiles(); + throw; } + + _log.Error("Found Unrecognized YAML Property: {ErrorMsg}", inner.Message); + _log.Error("Please remove the property quoted in the above message from your YAML file"); + throw new CommandException("Exiting due to invalid configuration"); + } + catch (Exception e) when (e is not CommandException) + { + _log.Error(e, "Unrecoverable Exception"); + ExitDueToFailure(); } + finally + { + CleanupOldLogFiles(); + } + } - [CommandOption("preview", 'p', Description = - "Only display the processed markdown results without making any API calls.")] - public bool Preview { get; [UsedImplicitly] set; } = false; + [CommandOption("preview", 'p', Description = + "Only display the processed markdown results without making any API calls.")] + public bool Preview { get; [UsedImplicitly] set; } = false; - [CommandOption("debug", 'd', Description = - "Display additional logs useful for development/debug purposes.")] - public bool Debug { get; [UsedImplicitly] set; } = false; + [CommandOption("debug", 'd', Description = + "Display additional logs useful for development/debug purposes.")] + public bool Debug { get; [UsedImplicitly] set; } = false; - [CommandOption("config", 'c', Description = - "One or more YAML config files to use. All configs will be used and settings are additive. " + - "If not specified, the script will look for `trash.yml` in the same directory as the executable.")] - public ICollection Config { get; [UsedImplicitly] set; } = - new List {AppPaths.DefaultConfigPath}; + [CommandOption("config", 'c', Description = + "One or more YAML config files to use. All configs will be used and settings are additive. " + + "If not specified, the script will look for `trash.yml` in the same directory as the executable.")] + public ICollection Config { get; [UsedImplicitly] set; } = + new List {AppPaths.DefaultConfigPath}; - public abstract string CacheStoragePath { get; } + public abstract string CacheStoragePath { get; } - private void CleanupOldLogFiles() - { - _logJanitor.DeleteOldestLogFiles(20); - } + private void CleanupOldLogFiles() + { + _logJanitor.DeleteOldestLogFiles(20); + } - private void SetupLogging() - { - _loggingLevelSwitch.MinimumLevel = - Debug ? LogEventLevel.Debug : LogEventLevel.Information; - } + private void SetupLogging() + { + _loggingLevelSwitch.MinimumLevel = + Debug ? LogEventLevel.Debug : LogEventLevel.Information; + } - private void SetupHttp() + private void SetupHttp() + { + FlurlHttp.Configure(settings => { - FlurlHttp.Configure(settings => + var jsonSettings = new JsonSerializerSettings { - var jsonSettings = new JsonSerializerSettings - { - // This is important. If any DTOs are missing members, say, if Radarr or Sonarr adds one in a future - // version, this needs to fail to indicate that a software change is required. Otherwise, we lose - // state between when we request settings, and re-apply them again with a few properties modified. - MissingMemberHandling = MissingMemberHandling.Error, - - // This makes sure that null properties, such as maxSize and preferredSize in Radarr - // Quality Definitions, do not get written out to JSON request bodies. - NullValueHandling = NullValueHandling.Ignore - }; - - settings.JsonSerializer = new NewtonsoftJsonSerializer(jsonSettings); - FlurlLogging.SetupLogging(settings, _log); - }); - } + // This is important. If any DTOs are missing members, say, if Radarr or Sonarr adds one in a future + // version, this needs to fail to indicate that a software change is required. Otherwise, we lose + // state between when we request settings, and re-apply them again with a few properties modified. + MissingMemberHandling = MissingMemberHandling.Error, + + // This makes sure that null properties, such as maxSize and preferredSize in Radarr + // Quality Definitions, do not get written out to JSON request bodies. + NullValueHandling = NullValueHandling.Ignore + }; + + settings.JsonSerializer = new NewtonsoftJsonSerializer(jsonSettings); + FlurlLogging.SetupLogging(settings, _log); + }); + } - public abstract Task Process(); + public abstract Task Process(); - protected static void ExitDueToFailure() - { - throw new CommandException("Exiting due to previous exception"); - } + protected static void ExitDueToFailure() + { + throw new CommandException("Exiting due to previous exception"); } } diff --git a/src/Trash/Command/RadarrCommand.cs b/src/Trash/Command/RadarrCommand.cs index 076104d6..347351c5 100644 --- a/src/Trash/Command/RadarrCommand.cs +++ b/src/Trash/Command/RadarrCommand.cs @@ -12,57 +12,56 @@ using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat; using TrashLib.Radarr.QualityDefinition; -namespace Trash.Command +namespace Trash.Command; + +[Command("radarr", Description = "Perform operations on a Radarr instance")] +[UsedImplicitly] +public class RadarrCommand : ServiceCommand { - [Command("radarr", Description = "Perform operations on a Radarr instance")] - [UsedImplicitly] - public class RadarrCommand : ServiceCommand - { - private readonly IConfigurationLoader _configLoader; - private readonly Func _customFormatUpdaterFactory; - private readonly ILogger _log; - private readonly Func _qualityUpdaterFactory; + private readonly IConfigurationLoader _configLoader; + private readonly Func _customFormatUpdaterFactory; + private readonly ILogger _log; + private readonly Func _qualityUpdaterFactory; - public RadarrCommand( - ILogger log, - LoggingLevelSwitch loggingLevelSwitch, - ILogJanitor logJanitor, - IConfigurationLoader configLoader, - Func qualityUpdaterFactory, - Func customFormatUpdaterFactory) - : base(log, loggingLevelSwitch, logJanitor) - { - _log = log; - _configLoader = configLoader; - _qualityUpdaterFactory = qualityUpdaterFactory; - _customFormatUpdaterFactory = customFormatUpdaterFactory; - } + public RadarrCommand( + ILogger log, + LoggingLevelSwitch loggingLevelSwitch, + ILogJanitor logJanitor, + IConfigurationLoader configLoader, + Func qualityUpdaterFactory, + Func customFormatUpdaterFactory) + : base(log, loggingLevelSwitch, logJanitor) + { + _log = log; + _configLoader = configLoader; + _qualityUpdaterFactory = qualityUpdaterFactory; + _customFormatUpdaterFactory = customFormatUpdaterFactory; + } - public override string CacheStoragePath { get; } = - Path.Combine(AppPaths.AppDataPath, "cache", "radarr"); + public override string CacheStoragePath { get; } = + Path.Combine(AppPaths.AppDataPath, "cache", "radarr"); - public override async Task Process() + public override async Task Process() + { + try { - try + foreach (var config in _configLoader.LoadMany(Config, "radarr")) { - foreach (var config in _configLoader.LoadMany(Config, "radarr")) + if (config.QualityDefinition != null) { - if (config.QualityDefinition != null) - { - await _qualityUpdaterFactory().Process(Preview, config); - } + await _qualityUpdaterFactory().Process(Preview, config); + } - if (config.CustomFormats.Count > 0) - { - await _customFormatUpdaterFactory().Process(Preview, config); - } + if (config.CustomFormats.Count > 0) + { + await _customFormatUpdaterFactory().Process(Preview, config); } } - catch (FlurlHttpException e) - { - _log.Error(e, "HTTP error while communicating with Radarr"); - ExitDueToFailure(); - } + } + catch (FlurlHttpException e) + { + _log.Error(e, "HTTP error while communicating with Radarr"); + ExitDueToFailure(); } } } diff --git a/src/Trash/Command/SonarrCommand.cs b/src/Trash/Command/SonarrCommand.cs index f945d2f5..8806364e 100644 --- a/src/Trash/Command/SonarrCommand.cs +++ b/src/Trash/Command/SonarrCommand.cs @@ -12,57 +12,56 @@ using TrashLib.Sonarr.Config; using TrashLib.Sonarr.QualityDefinition; using TrashLib.Sonarr.ReleaseProfile; -namespace Trash.Command +namespace Trash.Command; + +[Command("sonarr", Description = "Perform operations on a Sonarr instance")] +[UsedImplicitly] +public class SonarrCommand : ServiceCommand { - [Command("sonarr", Description = "Perform operations on a Sonarr instance")] - [UsedImplicitly] - public class SonarrCommand : ServiceCommand - { - private readonly IConfigurationLoader _configLoader; - private readonly ILogger _log; - private readonly Func _profileUpdaterFactory; - private readonly Func _qualityUpdaterFactory; + private readonly IConfigurationLoader _configLoader; + private readonly ILogger _log; + private readonly Func _profileUpdaterFactory; + private readonly Func _qualityUpdaterFactory; - public SonarrCommand( - ILogger log, - LoggingLevelSwitch loggingLevelSwitch, - ILogJanitor logJanitor, - IConfigurationLoader configLoader, - Func profileUpdaterFactory, - Func qualityUpdaterFactory) - : base(log, loggingLevelSwitch, logJanitor) - { - _log = log; - _configLoader = configLoader; - _profileUpdaterFactory = profileUpdaterFactory; - _qualityUpdaterFactory = qualityUpdaterFactory; - } + public SonarrCommand( + ILogger log, + LoggingLevelSwitch loggingLevelSwitch, + ILogJanitor logJanitor, + IConfigurationLoader configLoader, + Func profileUpdaterFactory, + Func qualityUpdaterFactory) + : base(log, loggingLevelSwitch, logJanitor) + { + _log = log; + _configLoader = configLoader; + _profileUpdaterFactory = profileUpdaterFactory; + _qualityUpdaterFactory = qualityUpdaterFactory; + } - public override string CacheStoragePath { get; } = - Path.Combine(AppPaths.AppDataPath, "cache", "sonarr"); + public override string CacheStoragePath { get; } = + Path.Combine(AppPaths.AppDataPath, "cache", "sonarr"); - public override async Task Process() + public override async Task Process() + { + try { - try + foreach (var config in _configLoader.LoadMany(Config, "sonarr")) { - foreach (var config in _configLoader.LoadMany(Config, "sonarr")) + if (config.ReleaseProfiles.Count > 0) { - if (config.ReleaseProfiles.Count > 0) - { - await _profileUpdaterFactory().Process(Preview, config); - } + await _profileUpdaterFactory().Process(Preview, config); + } - if (config.QualityDefinition.HasValue) - { - await _qualityUpdaterFactory().Process(Preview, config); - } + if (config.QualityDefinition.HasValue) + { + await _qualityUpdaterFactory().Process(Preview, config); } } - catch (FlurlHttpException e) - { - _log.Error(e, "HTTP error while communicating with Sonarr"); - ExitDueToFailure(); - } + } + catch (FlurlHttpException e) + { + _log.Error(e, "HTTP error while communicating with Sonarr"); + ExitDueToFailure(); } } } diff --git a/src/Trash/CompositionRoot.cs b/src/Trash/CompositionRoot.cs index 5838eddb..ba7ec277 100644 --- a/src/Trash/CompositionRoot.cs +++ b/src/Trash/CompositionRoot.cs @@ -17,83 +17,82 @@ using TrashLib.Sonarr; using TrashLib.Startup; using YamlDotNet.Serialization; -namespace Trash +namespace Trash; + +public static class CompositionRoot { - public static class CompositionRoot + private static void SetupLogging(ContainerBuilder builder) + { + builder.RegisterType().As(); + builder.RegisterType().SingleInstance(); + builder.Register(c => + { + var logPath = Path.Combine(AppPaths.LogDirectory, + $"trash_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log"); + + const string consoleTemplate = "[{Level:u3}] {Message:lj}{NewLine}{Exception}"; + + return new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console(outputTemplate: consoleTemplate, levelSwitch: c.Resolve()) + .WriteTo.File(logPath) + .CreateLogger(); + }) + .As() + .SingleInstance(); + } + + private static void ConfigurationRegistrations(ContainerBuilder builder) + { + builder.RegisterModule(); + + builder.RegisterType().As(); + builder.RegisterType().As(); + + builder.RegisterGeneric(typeof(ConfigurationLoader<>)) + .WithProperty(new AutowiringParameter()) + .As(typeof(IConfigurationLoader<>)); + + // note: Do not allow consumers to resolve IServiceConfiguration directly; if this gets cached + // they end up using the wrong configuration when multiple instances are used. + // builder.Register(c => c.Resolve().ActiveConfiguration) + // .As(); + } + + private static void CommandRegistrations(ContainerBuilder builder) + { + // Register all types deriving from CliFx's ICommand. These are all of our supported subcommands. + builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()) + .Where(t => t.IsAssignableTo(typeof(ICommand))); + + // Used to access the chosen command class. This is assigned from CliTypeActivator + builder.RegisterType() + .As() + .SingleInstance(); + } + + public static IContainer Setup() + { + return Setup(new ContainerBuilder()); + } + + public static IContainer Setup(ContainerBuilder builder) { - private static void SetupLogging(ContainerBuilder builder) - { - builder.RegisterType().As(); - builder.RegisterType().SingleInstance(); - builder.Register(c => - { - var logPath = Path.Combine(AppPaths.LogDirectory, - $"trash_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.log"); - - const string consoleTemplate = "[{Level:u3}] {Message:lj}{NewLine}{Exception}"; - - return new LoggerConfiguration() - .MinimumLevel.Debug() - .WriteTo.Console(outputTemplate: consoleTemplate, levelSwitch: c.Resolve()) - .WriteTo.File(logPath) - .CreateLogger(); - }) - .As() - .SingleInstance(); - } - - private static void ConfigurationRegistrations(ContainerBuilder builder) - { - builder.RegisterModule(); - - builder.RegisterType().As(); - builder.RegisterType().As(); - - builder.RegisterGeneric(typeof(ConfigurationLoader<>)) - .WithProperty(new AutowiringParameter()) - .As(typeof(IConfigurationLoader<>)); - - // note: Do not allow consumers to resolve IServiceConfiguration directly; if this gets cached - // they end up using the wrong configuration when multiple instances are used. - // builder.Register(c => c.Resolve().ActiveConfiguration) - // .As(); - } - - private static void CommandRegistrations(ContainerBuilder builder) - { - // Register all types deriving from CliFx's ICommand. These are all of our supported subcommands. - builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()) - .Where(t => t.IsAssignableTo(typeof(ICommand))); - - // Used to access the chosen command class. This is assigned from CliTypeActivator - builder.RegisterType() - .As() - .SingleInstance(); - } - - public static IContainer Setup() - { - return Setup(new ContainerBuilder()); - } - - public static IContainer Setup(ContainerBuilder builder) - { - builder.RegisterType().As(); - - builder.RegisterModule(); - builder.RegisterType().As(); - - ConfigurationRegistrations(builder); - CommandRegistrations(builder); - - SetupLogging(builder); - - builder.RegisterModule(); - builder.RegisterModule(); - - builder.Register(_ => AutoMapperConfig.Setup()).SingleInstance(); - - return builder.Build(); - } + builder.RegisterType().As(); + + builder.RegisterModule(); + builder.RegisterType().As(); + + ConfigurationRegistrations(builder); + CommandRegistrations(builder); + + SetupLogging(builder); + + builder.RegisterModule(); + builder.RegisterModule(); + + builder.Register(_ => AutoMapperConfig.Setup()).SingleInstance(); + + return builder.Build(); } } diff --git a/src/Trash/Config/ConfigurationException.cs b/src/Trash/Config/ConfigurationException.cs index a14f1aa0..7c0a2cad 100644 --- a/src/Trash/Config/ConfigurationException.cs +++ b/src/Trash/Config/ConfigurationException.cs @@ -5,52 +5,51 @@ using System.Runtime.Serialization; using System.Text; using FluentValidation.Results; -namespace Trash.Config +namespace Trash.Config; + +[Serializable] +public class ConfigurationException : Exception { - [Serializable] - public class ConfigurationException : Exception + protected ConfigurationException(SerializationInfo info, StreamingContext context) + : base(info, context) { - protected ConfigurationException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } + } - private ConfigurationException(string propertyName, Type deserializableType, IEnumerable messages) - { - PropertyName = propertyName; - DeserializableType = deserializableType; - ErrorMessages = messages.ToList(); - } + private ConfigurationException(string propertyName, Type deserializableType, IEnumerable messages) + { + PropertyName = propertyName; + DeserializableType = deserializableType; + ErrorMessages = messages.ToList(); + } - public ConfigurationException(string propertyName, Type deserializableType, string message) - : this(propertyName, deserializableType, new[] {message}) - { - } + public ConfigurationException(string propertyName, Type deserializableType, string message) + : this(propertyName, deserializableType, new[] {message}) + { + } - public ConfigurationException(string propertyName, Type deserializableType, - IEnumerable validationFailures) - : this(propertyName, deserializableType, validationFailures.Select(e => e.ToString())) - { - } + public ConfigurationException(string propertyName, Type deserializableType, + IEnumerable validationFailures) + : this(propertyName, deserializableType, validationFailures.Select(e => e.ToString())) + { + } - public IReadOnlyCollection ErrorMessages { get; } = new List(); - public string PropertyName { get; } = ""; - public Type DeserializableType { get; } = default!; + public IReadOnlyCollection ErrorMessages { get; } = new List(); + public string PropertyName { get; } = ""; + public Type DeserializableType { get; } = default!; - public override string Message => BuildMessage(); + public override string Message => BuildMessage(); - private string BuildMessage() + private string BuildMessage() + { + const string delim = "\n - "; + var builder = new StringBuilder( + $"An exception occurred while deserializing type '{DeserializableType}' for YML property '{PropertyName}'"); + if (ErrorMessages.Count > 0) { - const string delim = "\n - "; - var builder = new StringBuilder( - $"An exception occurred while deserializing type '{DeserializableType}' for YML property '{PropertyName}'"); - if (ErrorMessages.Count > 0) - { - builder.Append(":" + delim); - builder.Append(string.Join(delim, ErrorMessages)); - } - - return builder.ToString(); + builder.Append(":" + delim); + builder.Append(string.Join(delim, ErrorMessages)); } + + return builder.ToString(); } } diff --git a/src/Trash/Config/ConfigurationLoader.cs b/src/Trash/Config/ConfigurationLoader.cs index 10b632cb..41973796 100644 --- a/src/Trash/Config/ConfigurationLoader.cs +++ b/src/Trash/Config/ConfigurationLoader.cs @@ -10,95 +10,94 @@ using YamlDotNet.Core.Events; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; -namespace Trash.Config +namespace Trash.Config; + +public class ConfigurationLoader : IConfigurationLoader + where T : IServiceConfiguration { - public class ConfigurationLoader : IConfigurationLoader - where T : IServiceConfiguration + private readonly IConfigurationProvider _configProvider; + private readonly IDeserializer _deserializer; + private readonly IFileSystem _fileSystem; + private readonly IValidator _validator; + + public ConfigurationLoader( + IConfigurationProvider configProvider, + IFileSystem fileSystem, + IObjectFactory objectFactory, + IValidator validator) { - private readonly IConfigurationProvider _configProvider; - private readonly IDeserializer _deserializer; - private readonly IFileSystem _fileSystem; - private readonly IValidator _validator; + _configProvider = configProvider; + _fileSystem = fileSystem; + _validator = validator; + _deserializer = new DeserializerBuilder() + .IgnoreUnmatchedProperties() + .WithNamingConvention(UnderscoredNamingConvention.Instance) + .WithTypeConverter(new YamlNullableEnumTypeConverter()) + .WithObjectFactory(objectFactory) + .Build(); + } - public ConfigurationLoader( - IConfigurationProvider configProvider, - IFileSystem fileSystem, - IObjectFactory objectFactory, - IValidator validator) - { - _configProvider = configProvider; - _fileSystem = fileSystem; - _validator = validator; - _deserializer = new DeserializerBuilder() - .IgnoreUnmatchedProperties() - .WithNamingConvention(UnderscoredNamingConvention.Instance) - .WithTypeConverter(new YamlNullableEnumTypeConverter()) - .WithObjectFactory(objectFactory) - .Build(); - } + public IEnumerable Load(string propertyName, string configSection) + { + using var stream = _fileSystem.File.OpenText(propertyName); + return LoadFromStream(stream, configSection); + } - public IEnumerable Load(string propertyName, string configSection) - { - using var stream = _fileSystem.File.OpenText(propertyName); - return LoadFromStream(stream, configSection); - } + public IEnumerable LoadFromStream(TextReader stream, string configSection) + { + var parser = new Parser(stream); + parser.Consume(); + parser.Consume(); + parser.Consume(); - public IEnumerable LoadFromStream(TextReader stream, string configSection) + var validConfigs = new List(); + while (parser.TryConsume(out var key)) { - var parser = new Parser(stream); - parser.Consume(); - parser.Consume(); - parser.Consume(); - - var validConfigs = new List(); - while (parser.TryConsume(out var key)) + if (key.Value != configSection) { - if (key.Value != configSection) - { - parser.SkipThisAndNestedEvents(); - continue; - } - - var configs = _deserializer.Deserialize?>(parser); - if (configs == null) - { - parser.SkipThisAndNestedEvents(); - continue; - } - - ValidateConfigs(configSection, configs, validConfigs); parser.SkipThisAndNestedEvents(); + continue; } - if (validConfigs.Count == 0) + var configs = _deserializer.Deserialize?>(parser); + if (configs == null) { - throw new ConfigurationException(configSection, typeof(T), "There are no configured instances defined"); + parser.SkipThisAndNestedEvents(); + continue; } - return validConfigs; + ValidateConfigs(configSection, configs, validConfigs); + parser.SkipThisAndNestedEvents(); } - private void ValidateConfigs(string configSection, IEnumerable configs, ICollection validConfigs) + if (validConfigs.Count == 0) { - foreach (var config in configs) - { - var result = _validator.Validate(config); - if (result is {IsValid: false}) - { - throw new ConfigurationException(configSection, typeof(T), result.Errors); - } - - validConfigs.Add(config); - } + throw new ConfigurationException(configSection, typeof(T), "There are no configured instances defined"); } - public IEnumerable LoadMany(IEnumerable configFiles, string configSection) + return validConfigs; + } + + private void ValidateConfigs(string configSection, IEnumerable configs, ICollection validConfigs) + { + foreach (var config in configs) { - foreach (var config in configFiles.SelectMany(file => Load(file, configSection))) + var result = _validator.Validate(config); + if (result is {IsValid: false}) { - _configProvider.ActiveConfiguration = config; - yield return config; + throw new ConfigurationException(configSection, typeof(T), result.Errors); } + + validConfigs.Add(config); + } + } + + public IEnumerable LoadMany(IEnumerable configFiles, string configSection) + { + foreach (var config in configFiles.SelectMany(file => Load(file, configSection))) + { + _configProvider.ActiveConfiguration = config; + yield return config; } } } diff --git a/src/Trash/Config/IConfigurationLoader.cs b/src/Trash/Config/IConfigurationLoader.cs index 15fe6ba5..a08af9b8 100644 --- a/src/Trash/Config/IConfigurationLoader.cs +++ b/src/Trash/Config/IConfigurationLoader.cs @@ -2,13 +2,12 @@ using System.IO; using TrashLib.Config; -namespace Trash.Config +namespace Trash.Config; + +public interface IConfigurationLoader + where T : IServiceConfiguration { - public interface IConfigurationLoader - where T : IServiceConfiguration - { - IEnumerable Load(string propertyName, string configSection); - IEnumerable LoadFromStream(TextReader stream, string configSection); - IEnumerable LoadMany(IEnumerable configFiles, string configSection); - } + IEnumerable Load(string propertyName, string configSection); + IEnumerable LoadFromStream(TextReader stream, string configSection); + IEnumerable LoadMany(IEnumerable configFiles, string configSection); } diff --git a/src/Trash/Config/ObjectFactory.cs b/src/Trash/Config/ObjectFactory.cs index 2d610b45..4aa196b3 100644 --- a/src/Trash/Config/ObjectFactory.cs +++ b/src/Trash/Config/ObjectFactory.cs @@ -3,21 +3,20 @@ using Autofac; using YamlDotNet.Serialization; using YamlDotNet.Serialization.ObjectFactories; -namespace Trash.Config +namespace Trash.Config; + +public class ObjectFactory : IObjectFactory { - public class ObjectFactory : IObjectFactory - { - private readonly ILifetimeScope _container; - private readonly DefaultObjectFactory _defaultFactory = new(); + private readonly ILifetimeScope _container; + private readonly DefaultObjectFactory _defaultFactory = new(); - public ObjectFactory(ILifetimeScope container) - { - _container = container; - } + public ObjectFactory(ILifetimeScope container) + { + _container = container; + } - public object Create(Type type) - { - return _container.IsRegistered(type) ? _container.Resolve(type) : _defaultFactory.Create(type); - } + public object Create(Type type) + { + return _container.IsRegistered(type) ? _container.Resolve(type) : _defaultFactory.Create(type); } } diff --git a/src/Trash/ILogJanitor.cs b/src/Trash/ILogJanitor.cs index 766d65c9..c5c30a21 100644 --- a/src/Trash/ILogJanitor.cs +++ b/src/Trash/ILogJanitor.cs @@ -1,7 +1,6 @@ -namespace Trash +namespace Trash; + +public interface ILogJanitor { - public interface ILogJanitor - { - void DeleteOldestLogFiles(int numberOfNewestToKeep); - } + void DeleteOldestLogFiles(int numberOfNewestToKeep); } diff --git a/src/Trash/LogJanitor.cs b/src/Trash/LogJanitor.cs index bd52351f..d068bcfc 100644 --- a/src/Trash/LogJanitor.cs +++ b/src/Trash/LogJanitor.cs @@ -1,25 +1,24 @@ using System.IO.Abstractions; using System.Linq; -namespace Trash +namespace Trash; + +public class LogJanitor : ILogJanitor { - public class LogJanitor : ILogJanitor - { - private readonly IFileSystem _fileSystem; + private readonly IFileSystem _fileSystem; - public LogJanitor(IFileSystem fileSystem) - { - _fileSystem = fileSystem; - } + public LogJanitor(IFileSystem fileSystem) + { + _fileSystem = fileSystem; + } - public void DeleteOldestLogFiles(int numberOfNewestToKeep) + public void DeleteOldestLogFiles(int numberOfNewestToKeep) + { + foreach (var file in _fileSystem.DirectoryInfo.FromDirectoryName(AppPaths.LogDirectory).GetFiles() + .OrderByDescending(f => f.Name) + .Skip(numberOfNewestToKeep)) { - foreach (var file in _fileSystem.DirectoryInfo.FromDirectoryName(AppPaths.LogDirectory).GetFiles() - .OrderByDescending(f => f.Name) - .Skip(numberOfNewestToKeep)) - { - file.Delete(); - } + file.Delete(); } } } diff --git a/src/Trash/Program.cs b/src/Trash/Program.cs index e262b622..5b97e26a 100644 --- a/src/Trash/Program.cs +++ b/src/Trash/Program.cs @@ -3,22 +3,21 @@ using Autofac; using CliFx; using Trash.Command.Helpers; -namespace Trash +namespace Trash; + +internal static class Program { - internal static class Program - { - private static IContainer? _container; + private static IContainer? _container; - public static async Task Main() - { - _container = CompositionRoot.Setup(); - return await new CliApplicationBuilder() - .AddCommandsFromThisAssembly() - .SetExecutableName(ThisAssembly.AssemblyName) - .SetVersion($"v{ThisAssembly.AssemblyInformationalVersion}") - .UseTypeActivator(type => CliTypeActivator.ResolveType(_container, type)) - .Build() - .RunAsync(); - } + public static async Task Main() + { + _container = CompositionRoot.Setup(); + return await new CliApplicationBuilder() + .AddCommandsFromThisAssembly() + .SetExecutableName(ThisAssembly.AssemblyName) + .SetVersion($"v{ThisAssembly.AssemblyInformationalVersion}") + .UseTypeActivator(type => CliTypeActivator.ResolveType(_container, type)) + .Build() + .RunAsync(); } } diff --git a/src/Trash/ResourcePaths.cs b/src/Trash/ResourcePaths.cs index 62e0f99f..9376dd85 100644 --- a/src/Trash/ResourcePaths.cs +++ b/src/Trash/ResourcePaths.cs @@ -1,9 +1,8 @@ using TrashLib.Radarr.Config; -namespace Trash +namespace Trash; + +public class ResourcePaths : IResourcePaths { - public class ResourcePaths : IResourcePaths - { - public string RepoPath => AppPaths.RepoDirectory; - } + public string RepoPath => AppPaths.RepoDirectory; } diff --git a/src/TrashLib.TestLibrary/CfTestUtils.cs b/src/TrashLib.TestLibrary/CfTestUtils.cs index f22c10ce..3ed7e1bc 100644 --- a/src/TrashLib.TestLibrary/CfTestUtils.cs +++ b/src/TrashLib.TestLibrary/CfTestUtils.cs @@ -1,14 +1,13 @@ using System.Linq; using TrashLib.Radarr.CustomFormat.Models; -namespace Trash.TestLibrary +namespace Trash.TestLibrary; + +public static class CfTestUtils { - public static class CfTestUtils - { - public static QualityProfileCustomFormatScoreMapping NewMapping(params FormatMappingEntry[] entries) - => new(false) {Mapping = entries.ToList()}; + public static QualityProfileCustomFormatScoreMapping NewMapping(params FormatMappingEntry[] entries) + => new(false) {Mapping = entries.ToList()}; - public static QualityProfileCustomFormatScoreMapping NewMappingWithReset(params FormatMappingEntry[] entries) - => new(true) {Mapping = entries.ToList()}; - } + public static QualityProfileCustomFormatScoreMapping NewMappingWithReset(params FormatMappingEntry[] entries) + => new(true) {Mapping = entries.ToList()}; } diff --git a/src/TrashLib.Tests/Cache/ServiceCacheTest.cs b/src/TrashLib.Tests/Cache/ServiceCacheTest.cs index 74e6a845..4ac54789 100644 --- a/src/TrashLib.Tests/Cache/ServiceCacheTest.cs +++ b/src/TrashLib.Tests/Cache/ServiceCacheTest.cs @@ -12,201 +12,200 @@ using TestLibrary.NSubstitute; using TrashLib.Cache; using TrashLib.Config; -namespace TrashLib.Tests.Cache +namespace TrashLib.Tests.Cache; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class ServiceCacheTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class ServiceCacheTest + private class Context { - private class Context + public Context(IFileSystem? fs = null) { - public Context(IFileSystem? fs = null) + Filesystem = fs ?? Substitute.For(); + StoragePath = Substitute.For(); + ConfigProvider = Substitute.For(); + JsonSettings = new JsonSerializerSettings { - Filesystem = fs ?? Substitute.For(); - StoragePath = Substitute.For(); - ConfigProvider = Substitute.For(); - JsonSettings = new JsonSerializerSettings + Formatting = Formatting.Indented, + ContractResolver = new DefaultContractResolver { - Formatting = Formatting.Indented, - ContractResolver = new DefaultContractResolver - { - NamingStrategy = new SnakeCaseNamingStrategy() - } - }; - - // Set up a default for the active config's base URL. This is used to generate part of the path - ConfigProvider.ActiveConfiguration = Substitute.For(); - ConfigProvider.ActiveConfiguration.BaseUrl.Returns("http://localhost:1234"); - - Cache = new ServiceCache(Filesystem, StoragePath, ConfigProvider, Substitute.For()); - } - - public JsonSerializerSettings JsonSettings { get; } - public ServiceCache Cache { get; } - public IConfigurationProvider ConfigProvider { get; } - public ICacheStoragePath StoragePath { get; } - public IFileSystem Filesystem { get; } - } + NamingStrategy = new SnakeCaseNamingStrategy() + } + }; - private class ObjectWithoutAttribute - { + // Set up a default for the active config's base URL. This is used to generate part of the path + ConfigProvider.ActiveConfiguration = Substitute.For(); + ConfigProvider.ActiveConfiguration.BaseUrl.Returns("http://localhost:1234"); + + Cache = new ServiceCache(Filesystem, StoragePath, ConfigProvider, Substitute.For()); } - private const string ValidObjectName = "azAZ_09"; + public JsonSerializerSettings JsonSettings { get; } + public ServiceCache Cache { get; } + public IConfigurationProvider ConfigProvider { get; } + public ICacheStoragePath StoragePath { get; } + public IFileSystem Filesystem { get; } + } - [CacheObjectName(ValidObjectName)] - private class ObjectWithAttribute - { - public string TestValue { get; init; } = ""; - } + private class ObjectWithoutAttribute + { + } - [CacheObjectName("invalid+name")] - private class ObjectWithAttributeInvalidChars - { - } + private const string ValidObjectName = "azAZ_09"; - [Test] - public void Load_returns_null_when_file_does_not_exist() - { - var ctx = new Context(); - ctx.Filesystem.File.Exists(Arg.Any()).Returns(false); + [CacheObjectName(ValidObjectName)] + private class ObjectWithAttribute + { + public string TestValue { get; init; } = ""; + } - var result = ctx.Cache.Load(); - result.Should().BeNull(); - } + [CacheObjectName("invalid+name")] + private class ObjectWithAttributeInvalidChars + { + } - [Test] - public void Loading_with_attribute_parses_correctly() - { - var ctx = new Context(); + [Test] + public void Load_returns_null_when_file_does_not_exist() + { + var ctx = new Context(); + ctx.Filesystem.File.Exists(Arg.Any()).Returns(false); + + var result = ctx.Cache.Load(); + result.Should().BeNull(); + } - ctx.StoragePath.Path.Returns("testpath"); + [Test] + public void Loading_with_attribute_parses_correctly() + { + var ctx = new Context(); - dynamic testJson = new {TestValue = "Foo"}; - ctx.Filesystem.File.Exists(Arg.Any()).Returns(true); - ctx.Filesystem.File.ReadAllText(Arg.Any()) - .Returns(_ => JsonConvert.SerializeObject(testJson)); + ctx.StoragePath.Path.Returns("testpath"); - var obj = ctx.Cache.Load(); + dynamic testJson = new {TestValue = "Foo"}; + ctx.Filesystem.File.Exists(Arg.Any()).Returns(true); + ctx.Filesystem.File.ReadAllText(Arg.Any()) + .Returns(_ => JsonConvert.SerializeObject(testJson)); - obj.Should().NotBeNull(); - obj!.TestValue.Should().Be("Foo"); - ctx.Filesystem.File.Received().ReadAllText(Path.Combine("testpath", "be8fbc8f", $"{ValidObjectName}.json")); - } + var obj = ctx.Cache.Load(); - [Test] - public void Loading_with_invalid_object_name_throws() - { - var ctx = new Context(); + obj.Should().NotBeNull(); + obj!.TestValue.Should().Be("Foo"); + ctx.Filesystem.File.Received().ReadAllText(Path.Combine("testpath", "be8fbc8f", $"{ValidObjectName}.json")); + } - Action act = () => ctx.Cache.Load(); + [Test] + public void Loading_with_invalid_object_name_throws() + { + var ctx = new Context(); - act.Should() - .Throw() - .WithMessage("*'invalid+name' has unacceptable characters*"); - } + Action act = () => ctx.Cache.Load(); - [Test] - public void Loading_without_attribute_throws() - { - var ctx = new Context(); + act.Should() + .Throw() + .WithMessage("*'invalid+name' has unacceptable characters*"); + } - Action act = () => ctx.Cache.Load(); + [Test] + public void Loading_without_attribute_throws() + { + var ctx = new Context(); - act.Should() - .Throw() - .WithMessage("CacheObjectNameAttribute is missing*"); - } + Action act = () => ctx.Cache.Load(); - [Test] - public void Properties_are_saved_using_snake_case() - { - var ctx = new Context(); - ctx.StoragePath.Path.Returns("testpath"); - ctx.Cache.Save(new ObjectWithAttribute {TestValue = "Foo"}); + act.Should() + .Throw() + .WithMessage("CacheObjectNameAttribute is missing*"); + } - ctx.Filesystem.File.Received() - .WriteAllText(Arg.Any(), Verify.That(json => json.Should().Contain("\"test_value\""))); - } + [Test] + public void Properties_are_saved_using_snake_case() + { + var ctx = new Context(); + ctx.StoragePath.Path.Returns("testpath"); + ctx.Cache.Save(new ObjectWithAttribute {TestValue = "Foo"}); - [Test] - public void Saving_with_attribute_parses_correctly() - { - var ctx = new Context(); + ctx.Filesystem.File.Received() + .WriteAllText(Arg.Any(), Verify.That(json => json.Should().Contain("\"test_value\""))); + } - ctx.StoragePath.Path.Returns("testpath"); + [Test] + public void Saving_with_attribute_parses_correctly() + { + var ctx = new Context(); - ctx.Cache.Save(new ObjectWithAttribute {TestValue = "Foo"}); + ctx.StoragePath.Path.Returns("testpath"); - var expectedParentDirectory = Path.Combine("testpath", "be8fbc8f"); - ctx.Filesystem.Directory.Received().CreateDirectory(expectedParentDirectory); + ctx.Cache.Save(new ObjectWithAttribute {TestValue = "Foo"}); - dynamic expectedJson = new {TestValue = "Foo"}; - var expectedPath = Path.Combine(expectedParentDirectory, $"{ValidObjectName}.json"); - ctx.Filesystem.File.Received() - .WriteAllText(expectedPath, JsonConvert.SerializeObject(expectedJson, ctx.JsonSettings)); - } + var expectedParentDirectory = Path.Combine("testpath", "be8fbc8f"); + ctx.Filesystem.Directory.Received().CreateDirectory(expectedParentDirectory); - [Test] - public void Saving_with_invalid_object_name_throws() - { - var ctx = new Context(); + dynamic expectedJson = new {TestValue = "Foo"}; + var expectedPath = Path.Combine(expectedParentDirectory, $"{ValidObjectName}.json"); + ctx.Filesystem.File.Received() + .WriteAllText(expectedPath, JsonConvert.SerializeObject(expectedJson, ctx.JsonSettings)); + } + + [Test] + public void Saving_with_invalid_object_name_throws() + { + var ctx = new Context(); - Action act = () => ctx.Cache.Save(new ObjectWithAttributeInvalidChars()); + var act = () => ctx.Cache.Save(new ObjectWithAttributeInvalidChars()); - act.Should() - .Throw() - .WithMessage("*'invalid+name' has unacceptable characters*"); - } + act.Should() + .Throw() + .WithMessage("*'invalid+name' has unacceptable characters*"); + } - [Test] - public void Saving_without_attribute_throws() - { - var ctx = new Context(); + [Test] + public void Saving_without_attribute_throws() + { + var ctx = new Context(); - Action act = () => ctx.Cache.Save(new ObjectWithoutAttribute()); + var act = () => ctx.Cache.Save(new ObjectWithoutAttribute()); - act.Should() - .Throw() - .WithMessage("CacheObjectNameAttribute is missing*"); - } + act.Should() + .Throw() + .WithMessage("CacheObjectNameAttribute is missing*"); + } - [Test] - public void Switching_config_and_base_url_should_yield_different_cache_paths() - { - var ctx = new Context(); - ctx.StoragePath.Path.Returns("testpath"); + [Test] + public void Switching_config_and_base_url_should_yield_different_cache_paths() + { + var ctx = new Context(); + ctx.StoragePath.Path.Returns("testpath"); - var actualPaths = new List(); + var actualPaths = new List(); - dynamic testJson = new {TestValue = "Foo"}; - ctx.Filesystem.File.Exists(Arg.Any()).Returns(true); - ctx.Filesystem.File.ReadAllText(Arg.Do(s => actualPaths.Add(s))) - .Returns(_ => JsonConvert.SerializeObject(testJson)); + dynamic testJson = new {TestValue = "Foo"}; + ctx.Filesystem.File.Exists(Arg.Any()).Returns(true); + ctx.Filesystem.File.ReadAllText(Arg.Do(s => actualPaths.Add(s))) + .Returns(_ => JsonConvert.SerializeObject(testJson)); - ctx.Cache.Load(); + ctx.Cache.Load(); - // Change the active config & base URL so we get a different path - ctx.ConfigProvider.ActiveConfiguration = Substitute.For(); - ctx.ConfigProvider.ActiveConfiguration.BaseUrl.Returns("http://localhost:5678"); + // Change the active config & base URL so we get a different path + ctx.ConfigProvider.ActiveConfiguration = Substitute.For(); + ctx.ConfigProvider.ActiveConfiguration.BaseUrl.Returns("http://localhost:5678"); - ctx.Cache.Load(); + ctx.Cache.Load(); - actualPaths.Count.Should().Be(2); - actualPaths.Should().OnlyHaveUniqueItems(); - } + actualPaths.Count.Should().Be(2); + actualPaths.Should().OnlyHaveUniqueItems(); + } - [Test] - public void When_cache_file_is_empty_do_not_throw() - { - var ctx = new Context(); - ctx.Filesystem.File.Exists(Arg.Any()).Returns(true); - ctx.Filesystem.File.ReadAllText(Arg.Any()) - .Returns(_ => ""); + [Test] + public void When_cache_file_is_empty_do_not_throw() + { + var ctx = new Context(); + ctx.Filesystem.File.Exists(Arg.Any()).Returns(true); + ctx.Filesystem.File.ReadAllText(Arg.Any()) + .Returns(_ => ""); - Action act = () => ctx.Cache.Load(); + Action act = () => ctx.Cache.Load(); - act.Should().NotThrow(); - } + act.Should().NotThrow(); } } diff --git a/src/TrashLib.Tests/Radarr/CustomFormat/CachePersisterTest.cs b/src/TrashLib.Tests/Radarr/CustomFormat/CachePersisterTest.cs index aac599da..73ee0c82 100644 --- a/src/TrashLib.Tests/Radarr/CustomFormat/CachePersisterTest.cs +++ b/src/TrashLib.Tests/Radarr/CustomFormat/CachePersisterTest.cs @@ -11,143 +11,142 @@ using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; using TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps; -namespace TrashLib.Tests.Radarr.CustomFormat +namespace TrashLib.Tests.Radarr.CustomFormat; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class CachePersisterTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class CachePersisterTest + private class Context { - private class Context + public Context() { - public Context() - { - var log = Substitute.For(); - ServiceCache = Substitute.For(); - Persister = new CachePersister(log, ServiceCache); - } - - public CachePersister Persister { get; } - public IServiceCache ServiceCache { get; } + var log = Substitute.For(); + ServiceCache = Substitute.For(); + Persister = new CachePersister(log, ServiceCache); } - private static ProcessedCustomFormatData QuickMakeCf(string cfName, string trashId, int cfId) - { - return new ProcessedCustomFormatData(cfName, trashId, new JObject()) - { - CacheEntry = new TrashIdMapping(trashId, cfName) {CustomFormatId = cfId} - }; - } + public CachePersister Persister { get; } + public IServiceCache ServiceCache { get; } + } - [TestCase(CustomFormatCache.LatestVersion - 1)] - [TestCase(CustomFormatCache.LatestVersion + 1)] - public void Set_loaded_cache_to_null_if_versions_mismatch(int versionToTest) + private static ProcessedCustomFormatData QuickMakeCf(string cfName, string trashId, int cfId) + { + return new ProcessedCustomFormatData(cfName, trashId, new JObject()) { - var ctx = new Context(); - - var testCfObj = new CustomFormatCache - { - Version = versionToTest, - TrashIdMappings = new Collection {new("", "", 5)} - }; - ctx.ServiceCache.Load().Returns(testCfObj); - ctx.Persister.Load(); - ctx.Persister.CfCache.Should().BeNull(); - } + CacheEntry = new TrashIdMapping(trashId, cfName) {CustomFormatId = cfId} + }; + } - [Test] - public void Accept_loaded_cache_when_versions_match() - { - var ctx = new Context(); - - var testCfObj = new CustomFormatCache - { - Version = CustomFormatCache.LatestVersion, - TrashIdMappings = new Collection {new("", "", 5)} - }; - ctx.ServiceCache.Load().Returns(testCfObj); - ctx.Persister.Load(); - ctx.Persister.CfCache.Should().NotBeNull(); - } + [TestCase(CustomFormatCache.LatestVersion - 1)] + [TestCase(CustomFormatCache.LatestVersion + 1)] + public void Set_loaded_cache_to_null_if_versions_mismatch(int versionToTest) + { + var ctx = new Context(); - [Test] - public void Cf_cache_is_valid_after_successful_load() + var testCfObj = new CustomFormatCache { - var ctx = new Context(); - var testCfObj = new CustomFormatCache(); - ctx.ServiceCache.Load().Returns(testCfObj); + Version = versionToTest, + TrashIdMappings = new Collection {new("", "", 5)} + }; + ctx.ServiceCache.Load().Returns(testCfObj); + ctx.Persister.Load(); + ctx.Persister.CfCache.Should().BeNull(); + } - ctx.Persister.Load(); - ctx.Persister.CfCache.Should().BeSameAs(testCfObj); - } + [Test] + public void Accept_loaded_cache_when_versions_match() + { + var ctx = new Context(); - [Test] - public void Cf_cache_returns_null_if_not_loaded() + var testCfObj = new CustomFormatCache { - var ctx = new Context(); - ctx.Persister.Load(); - ctx.Persister.CfCache.Should().BeNull(); - } + Version = CustomFormatCache.LatestVersion, + TrashIdMappings = new Collection {new("", "", 5)} + }; + ctx.ServiceCache.Load().Returns(testCfObj); + ctx.Persister.Load(); + ctx.Persister.CfCache.Should().NotBeNull(); + } - [Test] - public void Save_works_with_valid_cf_cache() - { - var ctx = new Context(); - var testCfObj = new CustomFormatCache(); - ctx.ServiceCache.Load().Returns(testCfObj); + [Test] + public void Cf_cache_is_valid_after_successful_load() + { + var ctx = new Context(); + var testCfObj = new CustomFormatCache(); + ctx.ServiceCache.Load().Returns(testCfObj); - ctx.Persister.Load(); - ctx.Persister.Save(); + ctx.Persister.Load(); + ctx.Persister.CfCache.Should().BeSameAs(testCfObj); + } - ctx.ServiceCache.Received().Save(Arg.Is(testCfObj)); - } + [Test] + public void Cf_cache_returns_null_if_not_loaded() + { + var ctx = new Context(); + ctx.Persister.Load(); + ctx.Persister.CfCache.Should().BeNull(); + } + + [Test] + public void Save_works_with_valid_cf_cache() + { + var ctx = new Context(); + var testCfObj = new CustomFormatCache(); + ctx.ServiceCache.Load().Returns(testCfObj); + + ctx.Persister.Load(); + ctx.Persister.Save(); + + ctx.ServiceCache.Received().Save(Arg.Is(testCfObj)); + } - [Test] - public void Saving_without_loading_does_nothing() + [Test] + public void Saving_without_loading_does_nothing() + { + var ctx = new Context(); + ctx.Persister.Save(); + ctx.ServiceCache.DidNotReceive().Save(Arg.Any()); + } + + [Test] + public void Updating_overwrites_previous_cf_cache_and_updates_cf_data() + { + var ctx = new Context(); + + // Load initial CfCache just to test that it gets replaced + var testCfObj = new CustomFormatCache { - var ctx = new Context(); - ctx.Persister.Save(); - ctx.ServiceCache.DidNotReceive().Save(Arg.Any()); - } + TrashIdMappings = new Collection {new("", "") {CustomFormatId = 5}} + }; + ctx.ServiceCache.Load().Returns(testCfObj); + ctx.Persister.Load(); + + // Update with new cached items + var results = new CustomFormatTransactionData(); + results.NewCustomFormats.Add(QuickMakeCf("cfname", "trashid", 10)); - [Test] - public void Updating_overwrites_previous_cf_cache_and_updates_cf_data() + var customFormatData = new List { - var ctx = new Context(); - - // Load initial CfCache just to test that it gets replaced - var testCfObj = new CustomFormatCache - { - TrashIdMappings = new Collection {new("", "") {CustomFormatId = 5}} - }; - ctx.ServiceCache.Load().Returns(testCfObj); - ctx.Persister.Load(); - - // Update with new cached items - var results = new CustomFormatTransactionData(); - results.NewCustomFormats.Add(QuickMakeCf("cfname", "trashid", 10)); - - var customFormatData = new List - { - new("", "trashid", new JObject()) {CacheEntry = new TrashIdMapping("trashid", "cfname", 10)} - }; - - ctx.Persister.Update(customFormatData); - ctx.Persister.CfCache.Should().BeEquivalentTo(new CustomFormatCache - { - TrashIdMappings = new Collection {customFormatData[0].CacheEntry!} - }); - - customFormatData.Should().ContainSingle() - .Which.CacheEntry.Should().BeEquivalentTo( - new TrashIdMapping("trashid", "cfname") {CustomFormatId = 10}); - } + new("", "trashid", new JObject()) {CacheEntry = new TrashIdMapping("trashid", "cfname", 10)} + }; - [Test] - public void Updating_sets_cf_cache_without_loading() + ctx.Persister.Update(customFormatData); + ctx.Persister.CfCache.Should().BeEquivalentTo(new CustomFormatCache { - var ctx = new Context(); - ctx.Persister.Update(new List()); - ctx.Persister.CfCache.Should().NotBeNull(); - } + TrashIdMappings = new Collection {customFormatData[0].CacheEntry!} + }); + + customFormatData.Should().ContainSingle() + .Which.CacheEntry.Should().BeEquivalentTo( + new TrashIdMapping("trashid", "cfname") {CustomFormatId = 10}); + } + + [Test] + public void Updating_sets_cf_cache_without_loading() + { + var ctx = new Context(); + ctx.Persister.Update(new List()); + ctx.Persister.CfCache.Should().NotBeNull(); } } diff --git a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideProcessorTest.cs b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideProcessorTest.cs index 05a8efda..0c8a97e2 100644 --- a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideProcessorTest.cs +++ b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideProcessorTest.cs @@ -15,147 +15,146 @@ using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Processors; using TrashLib.Radarr.CustomFormat.Processors.GuideSteps; -namespace TrashLib.Tests.Radarr.CustomFormat.Processors +namespace TrashLib.Tests.Radarr.CustomFormat.Processors; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class GuideProcessorTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class GuideProcessorTest + private class TestGuideProcessorSteps : IGuideProcessorSteps { - private class TestGuideProcessorSteps : IGuideProcessorSteps + public ICustomFormatStep CustomFormat { get; } = new CustomFormatStep(); + public IConfigStep Config { get; } = new ConfigStep(); + public IQualityProfileStep QualityProfile { get; } = new QualityProfileStep(); + } + + private class Context + { + public Context() { - public ICustomFormatStep CustomFormat { get; } = new CustomFormatStep(); - public IConfigStep Config { get; } = new ConfigStep(); - public IQualityProfileStep QualityProfile { get; } = new QualityProfileStep(); + Logger = new LoggerConfiguration() + .WriteTo.TestCorrelator() + .WriteTo.NUnitOutput() + .MinimumLevel.Debug() + .CreateLogger(); + + Data = new ResourceDataReader(typeof(GuideProcessorTest), "Data"); } - private class Context - { - public Context() - { - Logger = new LoggerConfiguration() - .WriteTo.TestCorrelator() - .WriteTo.NUnitOutput() - .MinimumLevel.Debug() - .CreateLogger(); + public ILogger Logger { get; } + public ResourceDataReader Data { get; } - Data = new ResourceDataReader(typeof(GuideProcessorTest), "Data"); - } + public string ReadText(string textFile) => Data.ReadData(textFile); + public JObject ReadJson(string jsonFile) => JObject.Parse(ReadText(jsonFile)); + } - public ILogger Logger { get; } - public ResourceDataReader Data { get; } + [Test] + [SuppressMessage("Maintainability", "CA1506", Justification = "Designed to be a high-level integration test")] + public async Task Guide_processor_behaves_as_expected_with_normal_guide_data() + { + var ctx = new Context(); + var guideService = Substitute.For(); + var guideProcessor = new GuideProcessor(ctx.Logger, guideService, () => new TestGuideProcessorSteps()); - public string ReadText(string textFile) => Data.ReadData(textFile); - public JObject ReadJson(string jsonFile) => JObject.Parse(ReadText(jsonFile)); - } + // simulate guide data + guideService.GetCustomFormatJsonAsync().Returns(new[] + { + ctx.ReadText("ImportableCustomFormat1.json"), + ctx.ReadText("ImportableCustomFormat2.json"), + ctx.ReadText("NoScore.json"), + ctx.ReadText("WontBeInConfig.json") + }); - [Test] - [SuppressMessage("Maintainability", "CA1506", Justification = "Designed to be a high-level integration test")] - public async Task Guide_processor_behaves_as_expected_with_normal_guide_data() + // Simulate user config in YAML + var config = new List { - var ctx = new Context(); - var guideService = Substitute.For(); - var guideProcessor = new GuideProcessor(ctx.Logger, guideService, () => new TestGuideProcessorSteps()); + new() + { + Names = new List {"Surround SOUND", "DTS-HD/DTS:X", "no score", "not in guide 1"}, + QualityProfiles = new List + { + new() {Name = "profile1"}, + new() {Name = "profile2", Score = -1234} + } + }, + new() + { + Names = new List {"no score", "not in guide 2"}, + QualityProfiles = new List + { + new() {Name = "profile3"}, + new() {Name = "profile4", Score = 5678} + } + } + }; - // simulate guide data - guideService.GetCustomFormatJsonAsync().Returns(new[] + await guideProcessor.BuildGuideDataAsync(config, null); + + var expectedProcessedCustomFormatData = new List + { + new("Surround Sound", "43bb5f09c79641e7a22e48d440bd8868", ctx.ReadJson( + "ImportableCustomFormat1_Processed.json")) { - ctx.ReadText("ImportableCustomFormat1.json"), - ctx.ReadText("ImportableCustomFormat2.json"), - ctx.ReadText("NoScore.json"), - ctx.ReadText("WontBeInConfig.json") - }); + Score = 500 + }, + new("DTS-HD/DTS:X", "4eb3c272d48db8ab43c2c85283b69744", ctx.ReadJson( + "ImportableCustomFormat2_Processed.json")) + { + Score = 480 + }, + new("No Score", "abc", JObject.FromObject(new {name = "No Score"})) + }; + + guideProcessor.ProcessedCustomFormats.Should() + .BeEquivalentTo(expectedProcessedCustomFormatData, op => op.Using(new JsonEquivalencyStep())); - // Simulate user config in YAML - var config = new List + guideProcessor.ConfigData.Should() + .BeEquivalentTo(new List { new() { - Names = new List {"Surround SOUND", "DTS-HD/DTS:X", "no score", "not in guide 1"}, - QualityProfiles = new List - { - new() {Name = "profile1"}, - new() {Name = "profile2", Score = -1234} - } + CustomFormats = expectedProcessedCustomFormatData, + QualityProfiles = config[0].QualityProfiles }, new() { - Names = new List {"no score", "not in guide 2"}, - QualityProfiles = new List - { - new() {Name = "profile3"}, - new() {Name = "profile4", Score = 5678} - } + CustomFormats = expectedProcessedCustomFormatData.GetRange(2, 1), + QualityProfiles = config[1].QualityProfiles } - }; + }, op => op.Using(new JsonEquivalencyStep())); - await guideProcessor.BuildGuideDataAsync(config, null); + guideProcessor.CustomFormatsWithoutScore.Should() + .Equal(new List<(string name, string trashId, string profileName)> + { + ("No Score", "abc", "profile1"), + ("No Score", "abc", "profile3") + }); - var expectedProcessedCustomFormatData = new List + guideProcessor.CustomFormatsNotInGuide.Should().Equal(new List + { + "not in guide 1", "not in guide 2" + }); + + guideProcessor.ProfileScores.Should() + .BeEquivalentTo(new Dictionary { - new("Surround Sound", "43bb5f09c79641e7a22e48d440bd8868", ctx.ReadJson( - "ImportableCustomFormat1_Processed.json")) { - Score = 500 + "profile1", CfTestUtils.NewMapping( + new FormatMappingEntry(expectedProcessedCustomFormatData[0], 500), + new FormatMappingEntry(expectedProcessedCustomFormatData[1], 480)) }, - new("DTS-HD/DTS:X", "4eb3c272d48db8ab43c2c85283b69744", ctx.ReadJson( - "ImportableCustomFormat2_Processed.json")) { - Score = 480 + "profile2", CfTestUtils.NewMapping( + new FormatMappingEntry(expectedProcessedCustomFormatData[0], -1234), + new FormatMappingEntry(expectedProcessedCustomFormatData[1], -1234), + new FormatMappingEntry(expectedProcessedCustomFormatData[2], -1234)) }, - new("No Score", "abc", JObject.FromObject(new {name = "No Score"})) - }; - - guideProcessor.ProcessedCustomFormats.Should() - .BeEquivalentTo(expectedProcessedCustomFormatData, op => op.Using(new JsonEquivalencyStep())); - - guideProcessor.ConfigData.Should() - .BeEquivalentTo(new List - { - new() - { - CustomFormats = expectedProcessedCustomFormatData, - QualityProfiles = config[0].QualityProfiles - }, - new() - { - CustomFormats = expectedProcessedCustomFormatData.GetRange(2, 1), - QualityProfiles = config[1].QualityProfiles - } - }, op => op.Using(new JsonEquivalencyStep())); - - guideProcessor.CustomFormatsWithoutScore.Should() - .Equal(new List<(string name, string trashId, string profileName)> { - ("No Score", "abc", "profile1"), - ("No Score", "abc", "profile3") - }); - - guideProcessor.CustomFormatsNotInGuide.Should().Equal(new List - { - "not in guide 1", "not in guide 2" - }); - - guideProcessor.ProfileScores.Should() - .BeEquivalentTo(new Dictionary - { - { - "profile1", CfTestUtils.NewMapping( - new FormatMappingEntry(expectedProcessedCustomFormatData[0], 500), - new FormatMappingEntry(expectedProcessedCustomFormatData[1], 480)) - }, - { - "profile2", CfTestUtils.NewMapping( - new FormatMappingEntry(expectedProcessedCustomFormatData[0], -1234), - new FormatMappingEntry(expectedProcessedCustomFormatData[1], -1234), - new FormatMappingEntry(expectedProcessedCustomFormatData[2], -1234)) - }, - { - "profile4", CfTestUtils.NewMapping( - new FormatMappingEntry(expectedProcessedCustomFormatData[2], 5678)) - } - }, op => op - .Using(new JsonEquivalencyStep()) - .ComparingByMembers()); - } + "profile4", CfTestUtils.NewMapping( + new FormatMappingEntry(expectedProcessedCustomFormatData[2], 5678)) + } + }, op => op + .Using(new JsonEquivalencyStep()) + .ComparingByMembers()); } } diff --git a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/ConfigStepTest.cs b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/ConfigStepTest.cs index fe844218..ba627a49 100644 --- a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/ConfigStepTest.cs +++ b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/ConfigStepTest.cs @@ -7,219 +7,218 @@ using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; using TrashLib.Radarr.CustomFormat.Processors.GuideSteps; -namespace TrashLib.Tests.Radarr.CustomFormat.Processors.GuideSteps +namespace TrashLib.Tests.Radarr.CustomFormat.Processors.GuideSteps; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class ConfigStepTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class ConfigStepTest + [Test] + public void Cache_names_are_used_instead_of_name_in_json_data() { - [Test] - public void Cache_names_are_used_instead_of_name_in_json_data() + var testProcessedCfs = new List { - var testProcessedCfs = new List + new("name1", "id1", JObject.FromObject(new {name = "name1"})) { - new("name1", "id1", JObject.FromObject(new {name = "name1"})) - { - Score = 100 - }, - new("name3", "id3", JObject.FromObject(new {name = "name3"})) - { - CacheEntry = new TrashIdMapping("id3", "name1") - } - }; + Score = 100 + }, + new("name3", "id3", JObject.FromObject(new {name = "name3"})) + { + CacheEntry = new TrashIdMapping("id3", "name1") + } + }; - var testConfig = new CustomFormatConfig[] + var testConfig = new CustomFormatConfig[] + { + new() { - new() - { - Names = new List {"name1"} - } - }; + Names = new List {"name1"} + } + }; - var processor = new ConfigStep(); - processor.Process(testProcessedCfs, testConfig); + var processor = new ConfigStep(); + processor.Process(testProcessedCfs, testConfig); - processor.CustomFormatsNotInGuide.Should().BeEmpty(); - processor.ConfigData.Should().BeEquivalentTo(new List - { - new() - { - CustomFormats = new List - {testProcessedCfs[1]} - } - }, op => op - .Using(jctx => jctx.Subject.Should().BeEquivalentTo(jctx.Expectation)) - .WhenTypeIs()); - } + processor.CustomFormatsNotInGuide.Should().BeEmpty(); + processor.ConfigData.Should().BeEquivalentTo(new List + { + new() + { + CustomFormats = new List + {testProcessedCfs[1]} + } + }, op => op + .Using(jctx => jctx.Subject.Should().BeEquivalentTo(jctx.Expectation)) + .WhenTypeIs()); + } - [Test] - public void Custom_formats_missing_from_config_are_skipped() + [Test] + public void Custom_formats_missing_from_config_are_skipped() + { + var testProcessedCfs = new List { - var testProcessedCfs = new List - { - new("name1", "", new JObject()), - new("name2", "", new JObject()) - }; + new("name1", "", new JObject()), + new("name2", "", new JObject()) + }; - var testConfig = new CustomFormatConfig[] + var testConfig = new CustomFormatConfig[] + { + new() { - new() - { - Names = new List {"name1"} - } - }; + Names = new List {"name1"} + } + }; - var processor = new ConfigStep(); - processor.Process(testProcessedCfs, testConfig); + var processor = new ConfigStep(); + processor.Process(testProcessedCfs, testConfig); - processor.CustomFormatsNotInGuide.Should().BeEmpty(); - processor.ConfigData.Should().BeEquivalentTo(new List + processor.CustomFormatsNotInGuide.Should().BeEmpty(); + processor.ConfigData.Should().BeEquivalentTo(new List + { + new() { - new() + CustomFormats = new List { - CustomFormats = new List - { - new("name1", "", new JObject()) - } + new("name1", "", new JObject()) } - }, op => op - .Using(jctx => jctx.Subject.Should().BeEquivalentTo(jctx.Expectation)) - .WhenTypeIs()); - } + } + }, op => op + .Using(jctx => jctx.Subject.Should().BeEquivalentTo(jctx.Expectation)) + .WhenTypeIs()); + } - [Test] - public void Custom_formats_missing_from_guide_are_added_to_not_in_guide_list() + [Test] + public void Custom_formats_missing_from_guide_are_added_to_not_in_guide_list() + { + var testProcessedCfs = new List { - var testProcessedCfs = new List - { - new("name1", "", new JObject()), - new("name2", "", new JObject()) - }; + new("name1", "", new JObject()), + new("name2", "", new JObject()) + }; - var testConfig = new CustomFormatConfig[] + var testConfig = new CustomFormatConfig[] + { + new() { - new() - { - Names = new List {"name1", "name3"} - } - }; + Names = new List {"name1", "name3"} + } + }; - var processor = new ConfigStep(); - processor.Process(testProcessedCfs, testConfig); + var processor = new ConfigStep(); + processor.Process(testProcessedCfs, testConfig); - processor.CustomFormatsNotInGuide.Should().BeEquivalentTo(new List {"name3"}, op => op - .Using(jctx => jctx.Subject.Should().BeEquivalentTo(jctx.Expectation)) - .WhenTypeIs()); - processor.ConfigData.Should().BeEquivalentTo(new List + processor.CustomFormatsNotInGuide.Should().BeEquivalentTo(new List {"name3"}, op => op + .Using(jctx => jctx.Subject.Should().BeEquivalentTo(jctx.Expectation)) + .WhenTypeIs()); + processor.ConfigData.Should().BeEquivalentTo(new List + { + new() { - new() + CustomFormats = new List { - CustomFormats = new List - { - new("name1", "", new JObject()) - } + new("name1", "", new JObject()) } - }, op => op - .Using(jctx => jctx.Subject.Should().BeEquivalentTo(jctx.Expectation)) - .WhenTypeIs()); - } + } + }, op => op + .Using(jctx => jctx.Subject.Should().BeEquivalentTo(jctx.Expectation)) + .WhenTypeIs()); + } - [Test] - public void Duplicate_config_name_and_id_are_ignored() + [Test] + public void Duplicate_config_name_and_id_are_ignored() + { + var testProcessedCfs = new List { - var testProcessedCfs = new List - { - new("name1", "id1", new JObject()) - }; + new("name1", "id1", new JObject()) + }; - var testConfig = new CustomFormatConfig[] + var testConfig = new CustomFormatConfig[] + { + new() { - new() - { - Names = new List {"name1"}, - TrashIds = new List {"id1"} - } - }; + Names = new List {"name1"}, + TrashIds = new List {"id1"} + } + }; - var processor = new ConfigStep(); - processor.Process(testProcessedCfs, testConfig); + var processor = new ConfigStep(); + processor.Process(testProcessedCfs, testConfig); - processor.CustomFormatsNotInGuide.Should().BeEmpty(); - processor.ConfigData.Should().BeEquivalentTo(new List + processor.CustomFormatsNotInGuide.Should().BeEmpty(); + processor.ConfigData.Should().BeEquivalentTo(new List + { + new() { - new() - { - CustomFormats = new List {testProcessedCfs[0]} - } - }); - } + CustomFormats = new List {testProcessedCfs[0]} + } + }); + } - [Test] - public void Duplicate_config_names_are_ignored() + [Test] + public void Duplicate_config_names_are_ignored() + { + var testProcessedCfs = new List { - var testProcessedCfs = new List - { - new("name1", "id1", new JObject()) - }; + new("name1", "id1", new JObject()) + }; - var testConfig = new CustomFormatConfig[] - { - new() {Names = new List {"name1", "name1"}} - }; + var testConfig = new CustomFormatConfig[] + { + new() {Names = new List {"name1", "name1"}} + }; - var processor = new ConfigStep(); - processor.Process(testProcessedCfs, testConfig); + var processor = new ConfigStep(); + processor.Process(testProcessedCfs, testConfig); - processor.CustomFormatsNotInGuide.Should().BeEmpty(); - processor.ConfigData.Should().BeEquivalentTo(new List + processor.CustomFormatsNotInGuide.Should().BeEmpty(); + processor.ConfigData.Should().BeEquivalentTo(new List + { + new() { - new() - { - CustomFormats = new List {testProcessedCfs[0]} - } - }); - } + CustomFormats = new List {testProcessedCfs[0]} + } + }); + } - [Test] - public void Find_custom_formats_by_name_and_trash_id() + [Test] + public void Find_custom_formats_by_name_and_trash_id() + { + var testProcessedCfs = new List { - var testProcessedCfs = new List + new("name1", "id1", JObject.FromObject(new {name = "name1"})) { - new("name1", "id1", JObject.FromObject(new {name = "name1"})) - { - Score = 100 - }, - new("name3", "id3", JObject.FromObject(new {name = "name3"})), - new("name4", "id4", new JObject()) - }; + Score = 100 + }, + new("name3", "id3", JObject.FromObject(new {name = "name3"})), + new("name4", "id4", new JObject()) + }; - var testConfig = new CustomFormatConfig[] + var testConfig = new CustomFormatConfig[] + { + new() { - new() + Names = new List {"name1", "name3"}, + TrashIds = new List {"id1", "id4"}, + QualityProfiles = new List { - Names = new List {"name1", "name3"}, - TrashIds = new List {"id1", "id4"}, - QualityProfiles = new List - { - new() {Name = "profile1", Score = 50} - } + new() {Name = "profile1", Score = 50} } - }; + } + }; - var processor = new ConfigStep(); - processor.Process(testProcessedCfs, testConfig); + var processor = new ConfigStep(); + processor.Process(testProcessedCfs, testConfig); - processor.CustomFormatsNotInGuide.Should().BeEmpty(); - processor.ConfigData.Should().BeEquivalentTo(new List - { - new() - { - CustomFormats = testProcessedCfs, - QualityProfiles = testConfig[0].QualityProfiles - } - }, op => op - .Using(jctx => jctx.Subject.Should().BeEquivalentTo(jctx.Expectation)) - .WhenTypeIs()); - } + processor.CustomFormatsNotInGuide.Should().BeEmpty(); + processor.ConfigData.Should().BeEquivalentTo(new List + { + new() + { + CustomFormats = testProcessedCfs, + QualityProfiles = testConfig[0].QualityProfiles + } + }, op => op + .Using(jctx => jctx.Subject.Should().BeEquivalentTo(jctx.Expectation)) + .WhenTypeIs()); } } diff --git a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStepTest.cs b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStepTest.cs index 6c6ab859..33cf417f 100644 --- a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStepTest.cs +++ b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStepTest.cs @@ -11,379 +11,378 @@ using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; using TrashLib.Radarr.CustomFormat.Processors.GuideSteps; -namespace TrashLib.Tests.Radarr.CustomFormat.Processors.GuideSteps +namespace TrashLib.Tests.Radarr.CustomFormat.Processors.GuideSteps; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class CustomFormatStepTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class CustomFormatStepTest + private class Context { - private class Context + public List TestGuideData { get; } = new() { - public List TestGuideData { get; } = new() + JsonConvert.SerializeObject(new { - JsonConvert.SerializeObject(new - { - trash_id = "id1", - name = "name1" - }, Formatting.Indented), - JsonConvert.SerializeObject(new - { - trash_id = "id2", - name = "name2" - }, Formatting.Indented), - JsonConvert.SerializeObject(new - { - trash_id = "id3", - name = "name3" - }, Formatting.Indented) - }; - } - - [TestCase("name1", 0)] - [TestCase("naME1", 0)] - [TestCase("DifferentName", 1)] - public void Match_cf_in_guide_with_different_name_with_cache_using_same_name_in_config(string variableCfName, - int outdatedCount) - { - var testConfig = new List + trash_id = "id1", + name = "name1" + }, Formatting.Indented), + JsonConvert.SerializeObject(new { - new() {Names = new List {"name1"}} - }; - - var testCache = new CustomFormatCache + trash_id = "id2", + name = "name2" + }, Formatting.Indented), + JsonConvert.SerializeObject(new { - TrashIdMappings = new Collection - { - new("id1", "name1") - } - }; + trash_id = "id3", + name = "name3" + }, Formatting.Indented) + }; + } - var testGuideData = new List - { - JsonConvert.SerializeObject(new - { - trash_id = "id1", - name = variableCfName - }, Formatting.Indented) - }; - - var processor = new CustomFormatStep(); - processor.Process(testGuideData, testConfig, testCache); - - processor.DuplicatedCustomFormats.Should().BeEmpty(); - processor.CustomFormatsWithOutdatedNames.Should().HaveCount(outdatedCount); - processor.DeletedCustomFormatsInCache.Should().BeEmpty(); - processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List - { - new(variableCfName, "id1", JObject.FromObject(new {name = variableCfName})) - { - CacheEntry = testCache.TrashIdMappings[0] - } - }, - op => op.Using(new JsonEquivalencyStep())); - } + [TestCase("name1", 0)] + [TestCase("naME1", 0)] + [TestCase("DifferentName", 1)] + public void Match_cf_in_guide_with_different_name_with_cache_using_same_name_in_config(string variableCfName, + int outdatedCount) + { + var testConfig = new List + { + new() {Names = new List {"name1"}} + }; - [Test] - public void Cache_entry_is_not_set_when_id_is_different() + var testCache = new CustomFormatCache { - var guideData = new List + TrashIdMappings = new Collection { - @"{'name': 'name1', 'trash_id': 'id1'}" - }; + new("id1", "name1") + } + }; - var testConfig = new List + var testGuideData = new List + { + JsonConvert.SerializeObject(new { - new() {Names = new List {"name1"}} - }; - - var testCache = new CustomFormatCache + trash_id = "id1", + name = variableCfName + }, Formatting.Indented) + }; + + var processor = new CustomFormatStep(); + processor.Process(testGuideData, testConfig, testCache); + + processor.DuplicatedCustomFormats.Should().BeEmpty(); + processor.CustomFormatsWithOutdatedNames.Should().HaveCount(outdatedCount); + processor.DeletedCustomFormatsInCache.Should().BeEmpty(); + processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List { - TrashIdMappings = new Collection + new(variableCfName, "id1", JObject.FromObject(new {name = variableCfName})) { - new("id1000", "name1") + CacheEntry = testCache.TrashIdMappings[0] } - }; + }, + op => op.Using(new JsonEquivalencyStep())); + } - var processor = new CustomFormatStep(); - processor.Process(guideData, testConfig, testCache); + [Test] + public void Cache_entry_is_not_set_when_id_is_different() + { + var guideData = new List + { + @"{'name': 'name1', 'trash_id': 'id1'}" + }; - processor.DuplicatedCustomFormats.Should().BeEmpty(); - processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); - processor.DeletedCustomFormatsInCache.Count.Should().Be(1); - processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List - { - new("name1", "id1", JObject.FromObject(new {name = "name1"})) - { - Score = null, - CacheEntry = null - } - }, - op => op.Using(new JsonEquivalencyStep())); - } + var testConfig = new List + { + new() {Names = new List {"name1"}} + }; - [Test] - public void Cfs_not_in_config_are_skipped() + var testCache = new CustomFormatCache { - var ctx = new Context(); - var testConfig = new List + TrashIdMappings = new Collection { - new() {Names = new List {"name1", "name3"}} - }; + new("id1000", "name1") + } + }; - var processor = new CustomFormatStep(); - processor.Process(ctx.TestGuideData, testConfig, new CustomFormatCache()); + var processor = new CustomFormatStep(); + processor.Process(guideData, testConfig, testCache); - processor.DuplicatedCustomFormats.Should().BeEmpty(); - processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); - processor.DeletedCustomFormatsInCache.Should().BeEmpty(); - processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List + processor.DuplicatedCustomFormats.Should().BeEmpty(); + processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); + processor.DeletedCustomFormatsInCache.Count.Should().Be(1); + processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List + { + new("name1", "id1", JObject.FromObject(new {name = "name1"})) { - new("name1", "id1", JObject.FromObject(new {name = "name1"})) {Score = null}, - new("name3", "id3", JObject.FromObject(new {name = "name3"})) {Score = null} - }, - op => op.Using(new JsonEquivalencyStep())); - } + Score = null, + CacheEntry = null + } + }, + op => op.Using(new JsonEquivalencyStep())); + } - [Test] - public void Config_cfs_in_different_sections_are_processed() + [Test] + public void Cfs_not_in_config_are_skipped() + { + var ctx = new Context(); + var testConfig = new List { - var ctx = new Context(); - var testConfig = new List - { - new() {Names = new List {"name1", "name3"}}, - new() {Names = new List {"name2"}} - }; + new() {Names = new List {"name1", "name3"}} + }; - var processor = new CustomFormatStep(); - processor.Process(ctx.TestGuideData, testConfig, new CustomFormatCache()); + var processor = new CustomFormatStep(); + processor.Process(ctx.TestGuideData, testConfig, new CustomFormatCache()); - processor.DuplicatedCustomFormats.Should().BeEmpty(); - processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); - processor.DeletedCustomFormatsInCache.Should().BeEmpty(); - processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List - { - new("name1", "id1", JObject.FromObject(new {name = "name1"})) {Score = null}, - new("name2", "id2", JObject.FromObject(new {name = "name2"})) {Score = null}, - new("name3", "id3", JObject.FromObject(new {name = "name3"})) {Score = null} - }, - op => op.Using(new JsonEquivalencyStep())); - } + processor.DuplicatedCustomFormats.Should().BeEmpty(); + processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); + processor.DeletedCustomFormatsInCache.Should().BeEmpty(); + processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List + { + new("name1", "id1", JObject.FromObject(new {name = "name1"})) {Score = null}, + new("name3", "id3", JObject.FromObject(new {name = "name3"})) {Score = null} + }, + op => op.Using(new JsonEquivalencyStep())); + } - [Test] - public void Custom_format_is_deleted_if_in_config_and_cache_but_not_in_guide() + [Test] + public void Config_cfs_in_different_sections_are_processed() + { + var ctx = new Context(); + var testConfig = new List { - var guideData = new List - { - @"{'name': 'name1', 'trash_id': 'id1'}" - }; + new() {Names = new List {"name1", "name3"}}, + new() {Names = new List {"name2"}} + }; - var testConfig = new List - { - new() {Names = new List {"name1"}} - }; + var processor = new CustomFormatStep(); + processor.Process(ctx.TestGuideData, testConfig, new CustomFormatCache()); - var testCache = new CustomFormatCache + processor.DuplicatedCustomFormats.Should().BeEmpty(); + processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); + processor.DeletedCustomFormatsInCache.Should().BeEmpty(); + processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List { - TrashIdMappings = new Collection {new("id1000", "name1")} - }; + new("name1", "id1", JObject.FromObject(new {name = "name1"})) {Score = null}, + new("name2", "id2", JObject.FromObject(new {name = "name2"})) {Score = null}, + new("name3", "id3", JObject.FromObject(new {name = "name3"})) {Score = null} + }, + op => op.Using(new JsonEquivalencyStep())); + } - var processor = new CustomFormatStep(); - processor.Process(guideData, testConfig, testCache); + [Test] + public void Custom_format_is_deleted_if_in_config_and_cache_but_not_in_guide() + { + var guideData = new List + { + @"{'name': 'name1', 'trash_id': 'id1'}" + }; - processor.DuplicatedCustomFormats.Should().BeEmpty(); - processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); - processor.DeletedCustomFormatsInCache.Should() - .BeEquivalentTo(new[] {new TrashIdMapping("id1000", "name1")}); - processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List - { - new("name1", "id1", JObject.Parse(@"{'name': 'name1'}")) - }, - op => op.Using(new JsonEquivalencyStep())); - } + var testConfig = new List + { + new() {Names = new List {"name1"}} + }; - [Test] - public void Custom_format_is_deleted_if_not_in_config_but_in_cache_and_in_guide() + var testCache = new CustomFormatCache { - var cache = new CustomFormatCache - { - TrashIdMappings = new Collection {new("id1", "3D", 9)} - }; + TrashIdMappings = new Collection {new("id1000", "name1")} + }; - var guideCfs = new List - { - "{'name': '3D', 'trash_id': 'id1'}" - }; + var processor = new CustomFormatStep(); + processor.Process(guideData, testConfig, testCache); - var processor = new CustomFormatStep(); - processor.Process(guideCfs, Array.Empty(), cache); + processor.DuplicatedCustomFormats.Should().BeEmpty(); + processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); + processor.DeletedCustomFormatsInCache.Should() + .BeEquivalentTo(new[] {new TrashIdMapping("id1000", "name1")}); + processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List + { + new("name1", "id1", JObject.Parse(@"{'name': 'name1'}")) + }, + op => op.Using(new JsonEquivalencyStep())); + } - processor.DuplicatedCustomFormats.Should().BeEmpty(); - processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); - processor.DeletedCustomFormatsInCache.Should().BeEquivalentTo(new[] {cache.TrashIdMappings[0]}); - processor.ProcessedCustomFormats.Should().BeEmpty(); - } + [Test] + public void Custom_format_is_deleted_if_not_in_config_but_in_cache_and_in_guide() + { + var cache = new CustomFormatCache + { + TrashIdMappings = new Collection {new("id1", "3D", 9)} + }; - [Test] - public void Custom_format_name_in_cache_is_updated_if_renamed_in_guide_and_config() + var guideCfs = new List { - var guideData = new List - { - @"{'name': 'name2', 'trash_id': 'id1'}" - }; + "{'name': '3D', 'trash_id': 'id1'}" + }; - var testConfig = new List - { - new() {Names = new List {"name2"}} - }; + var processor = new CustomFormatStep(); + processor.Process(guideCfs, Array.Empty(), cache); - var testCache = new CustomFormatCache - { - TrashIdMappings = new Collection {new("id1", "name1")} - }; - - var processor = new CustomFormatStep(); - processor.Process(guideData, testConfig, testCache); - - processor.DuplicatedCustomFormats.Should().BeEmpty(); - processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); - processor.DeletedCustomFormatsInCache.Should().BeEmpty(); - processor.ProcessedCustomFormats.Should() - .ContainSingle().Which.CacheEntry.Should() - .BeEquivalentTo(new TrashIdMapping("id1", "name2")); - } - - [Test] - public void Duplicates_are_recorded_and_removed_from_processed_custom_formats_list() + processor.DuplicatedCustomFormats.Should().BeEmpty(); + processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); + processor.DeletedCustomFormatsInCache.Should().BeEquivalentTo(new[] {cache.TrashIdMappings[0]}); + processor.ProcessedCustomFormats.Should().BeEmpty(); + } + + [Test] + public void Custom_format_name_in_cache_is_updated_if_renamed_in_guide_and_config() + { + var guideData = new List { - var guideData = new List - { - @"{'name': 'name1', 'trash_id': 'id1'}", - @"{'name': 'name1', 'trash_id': 'id2'}" - }; + @"{'name': 'name2', 'trash_id': 'id1'}" + }; - var testConfig = new List - { - new() {Names = new List {"name1"}} - }; + var testConfig = new List + { + new() {Names = new List {"name2"}} + }; - var processor = new CustomFormatStep(); - processor.Process(guideData, testConfig, null); + var testCache = new CustomFormatCache + { + TrashIdMappings = new Collection {new("id1", "name1")} + }; + + var processor = new CustomFormatStep(); + processor.Process(guideData, testConfig, testCache); + + processor.DuplicatedCustomFormats.Should().BeEmpty(); + processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); + processor.DeletedCustomFormatsInCache.Should().BeEmpty(); + processor.ProcessedCustomFormats.Should() + .ContainSingle().Which.CacheEntry.Should() + .BeEquivalentTo(new TrashIdMapping("id1", "name2")); + } - //Dictionary> - processor.DuplicatedCustomFormats.Should() - .ContainKey("name1").WhoseValue.Should() - .BeEquivalentTo(new List - { - new("name1", "id1", JObject.Parse(@"{'name': 'name1'}")), - new("name1", "id2", JObject.Parse(@"{'name': 'name1'}")) - }); - processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); - processor.DeletedCustomFormatsInCache.Should().BeEmpty(); - processor.ProcessedCustomFormats.Should().BeEmpty(); - } - - [Test] - public void Match_cf_names_regardless_of_case_in_config() + [Test] + public void Duplicates_are_recorded_and_removed_from_processed_custom_formats_list() + { + var guideData = new List { - var ctx = new Context(); - var testConfig = new List - { - new() {Names = new List {"name1", "NAME1"}} - }; + @"{'name': 'name1', 'trash_id': 'id1'}", + @"{'name': 'name1', 'trash_id': 'id2'}" + }; - var processor = new CustomFormatStep(); - processor.Process(ctx.TestGuideData, testConfig, new CustomFormatCache()); + var testConfig = new List + { + new() {Names = new List {"name1"}} + }; - processor.DuplicatedCustomFormats.Should().BeEmpty(); - processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); - processor.DeletedCustomFormatsInCache.Should().BeEmpty(); - processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List - { - new("name1", "id1", JObject.FromObject(new {name = "name1"})) - }, - op => op.Using(new JsonEquivalencyStep())); - } + var processor = new CustomFormatStep(); + processor.Process(guideData, testConfig, null); - [Test] - public void Match_custom_format_using_trash_id() - { - var guideData = new List + //Dictionary> + processor.DuplicatedCustomFormats.Should() + .ContainKey("name1").WhoseValue.Should() + .BeEquivalentTo(new List { - @"{'name': 'name1', 'trash_id': 'id1'}", - @"{'name': 'name2', 'trash_id': 'id2'}" - }; + new("name1", "id1", JObject.Parse(@"{'name': 'name1'}")), + new("name1", "id2", JObject.Parse(@"{'name': 'name1'}")) + }); + processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); + processor.DeletedCustomFormatsInCache.Should().BeEmpty(); + processor.ProcessedCustomFormats.Should().BeEmpty(); + } - var testConfig = new List - { - new() {TrashIds = new List {"id2"}} - }; + [Test] + public void Match_cf_names_regardless_of_case_in_config() + { + var ctx = new Context(); + var testConfig = new List + { + new() {Names = new List {"name1", "NAME1"}} + }; - var processor = new CustomFormatStep(); - processor.Process(guideData, testConfig, null); + var processor = new CustomFormatStep(); + processor.Process(ctx.TestGuideData, testConfig, new CustomFormatCache()); - processor.DuplicatedCustomFormats.Should().BeEmpty(); - processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); - processor.DeletedCustomFormatsInCache.Should().BeEmpty(); - processor.ProcessedCustomFormats.Should() - .BeEquivalentTo(new List - { - new("name2", "id2", JObject.FromObject(new {name = "name2"})) - }); - } + processor.DuplicatedCustomFormats.Should().BeEmpty(); + processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); + processor.DeletedCustomFormatsInCache.Should().BeEmpty(); + processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List + { + new("name1", "id1", JObject.FromObject(new {name = "name1"})) + }, + op => op.Using(new JsonEquivalencyStep())); + } - [Test] - public void Non_existent_cfs_in_config_are_skipped() + [Test] + public void Match_custom_format_using_trash_id() + { + var guideData = new List { - var ctx = new Context(); - var testConfig = new List + @"{'name': 'name1', 'trash_id': 'id1'}", + @"{'name': 'name2', 'trash_id': 'id2'}" + }; + + var testConfig = new List + { + new() {TrashIds = new List {"id2"}} + }; + + var processor = new CustomFormatStep(); + processor.Process(guideData, testConfig, null); + + processor.DuplicatedCustomFormats.Should().BeEmpty(); + processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); + processor.DeletedCustomFormatsInCache.Should().BeEmpty(); + processor.ProcessedCustomFormats.Should() + .BeEquivalentTo(new List { - new() {Names = new List {"doesnt_exist"}} - }; + new("name2", "id2", JObject.FromObject(new {name = "name2"})) + }); + } + + [Test] + public void Non_existent_cfs_in_config_are_skipped() + { + var ctx = new Context(); + var testConfig = new List + { + new() {Names = new List {"doesnt_exist"}} + }; - var processor = new CustomFormatStep(); - processor.Process(ctx.TestGuideData, testConfig, new CustomFormatCache()); + var processor = new CustomFormatStep(); + processor.Process(ctx.TestGuideData, testConfig, new CustomFormatCache()); - processor.DuplicatedCustomFormats.Should().BeEmpty(); - processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); - processor.DeletedCustomFormatsInCache.Should().BeEmpty(); - processor.ProcessedCustomFormats.Should().BeEmpty(); - } + processor.DuplicatedCustomFormats.Should().BeEmpty(); + processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); + processor.DeletedCustomFormatsInCache.Should().BeEmpty(); + processor.ProcessedCustomFormats.Should().BeEmpty(); + } - [Test] - public void Score_from_json_takes_precedence_over_score_from_guide() + [Test] + public void Score_from_json_takes_precedence_over_score_from_guide() + { + var guideData = new List { - var guideData = new List - { - @"{'name': 'name1', 'trash_id': 'id1', 'trash_score': 100}" - }; + @"{'name': 'name1', 'trash_id': 'id1', 'trash_score': 100}" + }; - var testConfig = new List + var testConfig = new List + { + new() { - new() + Names = new List {"name1"}, + QualityProfiles = new List { - Names = new List {"name1"}, - QualityProfiles = new List - { - new() {Name = "profile", Score = 200} - } + new() {Name = "profile", Score = 200} } - }; + } + }; - var processor = new CustomFormatStep(); - processor.Process(guideData, testConfig, null); + var processor = new CustomFormatStep(); + processor.Process(guideData, testConfig, null); - processor.DuplicatedCustomFormats.Should().BeEmpty(); - processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); - processor.DeletedCustomFormatsInCache.Should().BeEmpty(); - processor.ProcessedCustomFormats.Should() - .BeEquivalentTo(new List + processor.DuplicatedCustomFormats.Should().BeEmpty(); + processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); + processor.DeletedCustomFormatsInCache.Should().BeEmpty(); + processor.ProcessedCustomFormats.Should() + .BeEquivalentTo(new List + { + new("name1", "id1", JObject.FromObject(new {name = "name1"})) { - new("name1", "id1", JObject.FromObject(new {name = "name1"})) - { - Score = 100 - } - }, - op => op.Using(new JsonEquivalencyStep())); - } + Score = 100 + } + }, + op => op.Using(new JsonEquivalencyStep())); } } diff --git a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStepTest.cs b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStepTest.cs index f7dd89a9..d9675e39 100644 --- a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStepTest.cs +++ b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStepTest.cs @@ -8,127 +8,126 @@ using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Processors.GuideSteps; -namespace TrashLib.Tests.Radarr.CustomFormat.Processors.GuideSteps +namespace TrashLib.Tests.Radarr.CustomFormat.Processors.GuideSteps; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class QualityProfileStepTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class QualityProfileStepTest + [Test] + public void No_score_used_if_no_score_in_config_or_guide() { - [Test] - public void No_score_used_if_no_score_in_config_or_guide() + var testConfigData = new List { - var testConfigData = new List + new() { - new() + CustomFormats = new List + { + new("name1", "id1", new JObject()) {Score = null} + }, + QualityProfiles = new List { - CustomFormats = new List - { - new("name1", "id1", new JObject()) {Score = null} - }, - QualityProfiles = new List - { - new() {Name = "profile1"} - } + new() {Name = "profile1"} } - }; + } + }; - var processor = new QualityProfileStep(); - processor.Process(testConfigData); + var processor = new QualityProfileStep(); + processor.Process(testConfigData); - processor.ProfileScores.Should().BeEmpty(); - processor.CustomFormatsWithoutScore.Should().Equal(("name1", "id1", "profile1")); - } + processor.ProfileScores.Should().BeEmpty(); + processor.CustomFormatsWithoutScore.Should().Equal(("name1", "id1", "profile1")); + } - [Test] - public void Overwrite_score_from_guide_if_config_defines_score() + [Test] + public void Overwrite_score_from_guide_if_config_defines_score() + { + var testConfigData = new List { - var testConfigData = new List + new() { - new() + CustomFormats = new List { - CustomFormats = new List - { - new("", "id1", new JObject()) {Score = 100} - }, - QualityProfiles = new List - { - new() {Name = "profile1", Score = 50} - } + new("", "id1", new JObject()) {Score = 100} + }, + QualityProfiles = new List + { + new() {Name = "profile1", Score = 50} } - }; + } + }; - var processor = new QualityProfileStep(); - processor.Process(testConfigData); + var processor = new QualityProfileStep(); + processor.Process(testConfigData); - processor.ProfileScores.Should() - .ContainKey("profile1").WhoseValue.Should() - .BeEquivalentTo( - CfTestUtils.NewMapping(new FormatMappingEntry(testConfigData[0].CustomFormats.First(), 50))); + processor.ProfileScores.Should() + .ContainKey("profile1").WhoseValue.Should() + .BeEquivalentTo( + CfTestUtils.NewMapping(new FormatMappingEntry(testConfigData[0].CustomFormats.First(), 50))); - processor.CustomFormatsWithoutScore.Should().BeEmpty(); - } + processor.CustomFormatsWithoutScore.Should().BeEmpty(); + } - [Test] - public void Use_guide_score_if_no_score_in_config() + [Test] + public void Use_guide_score_if_no_score_in_config() + { + var testConfigData = new List { - var testConfigData = new List + new() { - new() + CustomFormats = new List { - CustomFormats = new List - { - new("", "id1", new JObject()) {Score = 100} - }, - QualityProfiles = new List - { - new() {Name = "profile1"}, - new() {Name = "profile2", Score = null} - } + new("", "id1", new JObject()) {Score = 100} + }, + QualityProfiles = new List + { + new() {Name = "profile1"}, + new() {Name = "profile2", Score = null} } - }; + } + }; - var processor = new QualityProfileStep(); - processor.Process(testConfigData); + var processor = new QualityProfileStep(); + processor.Process(testConfigData); - var expectedScoreEntries = - CfTestUtils.NewMapping(new FormatMappingEntry(testConfigData[0].CustomFormats.First(), 100)); + var expectedScoreEntries = + CfTestUtils.NewMapping(new FormatMappingEntry(testConfigData[0].CustomFormats.First(), 100)); - processor.ProfileScores.Should().BeEquivalentTo( - new Dictionary - { - {"profile1", expectedScoreEntries}, - {"profile2", expectedScoreEntries} - }); + processor.ProfileScores.Should().BeEquivalentTo( + new Dictionary + { + {"profile1", expectedScoreEntries}, + {"profile2", expectedScoreEntries} + }); - processor.CustomFormatsWithoutScore.Should().BeEmpty(); - } + processor.CustomFormatsWithoutScore.Should().BeEmpty(); + } - [Test] - public void Zero_score_is_not_ignored() + [Test] + public void Zero_score_is_not_ignored() + { + var testConfigData = new List { - var testConfigData = new List + new() { - new() + CustomFormats = new List + { + new("name1", "id1", new JObject()) {Score = 0} + }, + QualityProfiles = new List { - CustomFormats = new List - { - new("name1", "id1", new JObject()) {Score = 0} - }, - QualityProfiles = new List - { - new() {Name = "profile1"} - } + new() {Name = "profile1"} } - }; + } + }; - var processor = new QualityProfileStep(); - processor.Process(testConfigData); + var processor = new QualityProfileStep(); + processor.Process(testConfigData); - processor.ProfileScores.Should() - .ContainKey("profile1").WhoseValue.Should() - .BeEquivalentTo(CfTestUtils.NewMapping(new FormatMappingEntry(testConfigData[0].CustomFormats.First(), 0))); + processor.ProfileScores.Should() + .ContainKey("profile1").WhoseValue.Should() + .BeEquivalentTo(CfTestUtils.NewMapping(new FormatMappingEntry(testConfigData[0].CustomFormats.First(), 0))); - processor.CustomFormatsWithoutScore.Should().BeEmpty(); - } + processor.CustomFormatsWithoutScore.Should().BeEmpty(); } } diff --git a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceProcessorTest.cs b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceProcessorTest.cs index 4db56160..284b2127 100644 --- a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceProcessorTest.cs +++ b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceProcessorTest.cs @@ -11,75 +11,74 @@ using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; using TrashLib.Radarr.CustomFormat.Processors; -namespace TrashLib.Tests.Radarr.CustomFormat.Processors +namespace TrashLib.Tests.Radarr.CustomFormat.Processors; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class PersistenceProcessorTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class PersistenceProcessorTest + [Test] + public void Custom_formats_are_deleted_if_deletion_option_is_enabled_in_config() + { + var steps = Substitute.For(); + var cfApi = Substitute.For(); + var qpApi = Substitute.For(); + + var configProvider = Substitute.For(); + configProvider.ActiveConfiguration = new RadarrConfiguration {DeleteOldCustomFormats = true}; + + var guideCfs = Array.Empty(); + var deletedCfsInCache = new Collection(); + var profileScores = new Dictionary(); + + var processor = new PersistenceProcessor(cfApi, qpApi, configProvider, () => steps); + processor.PersistCustomFormats(guideCfs, deletedCfsInCache, profileScores); + + steps.JsonTransactionStep.Received().RecordDeletions(Arg.Is(deletedCfsInCache), Arg.Any>()); + } + + [Test] + public void Custom_formats_are_not_deleted_if_deletion_option_is_disabled_in_config() + { + var steps = Substitute.For(); + var cfApi = Substitute.For(); + var qpApi = Substitute.For(); + + var configProvider = Substitute.For(); + configProvider.ActiveConfiguration = new RadarrConfiguration {DeleteOldCustomFormats = false}; + + var guideCfs = Array.Empty(); + var deletedCfsInCache = Array.Empty(); + var profileScores = new Dictionary(); + + var processor = new PersistenceProcessor(cfApi, qpApi, configProvider, () => steps); + processor.PersistCustomFormats(guideCfs, deletedCfsInCache, profileScores); + + steps.JsonTransactionStep.DidNotReceive() + .RecordDeletions(Arg.Any>(), Arg.Any>()); + } + + [Test] + public void Different_active_configuration_is_properly_used() { - [Test] - public void Custom_formats_are_deleted_if_deletion_option_is_enabled_in_config() - { - var steps = Substitute.For(); - var cfApi = Substitute.For(); - var qpApi = Substitute.For(); - - var configProvider = Substitute.For(); - configProvider.ActiveConfiguration = new RadarrConfiguration {DeleteOldCustomFormats = true}; - - var guideCfs = Array.Empty(); - var deletedCfsInCache = new Collection(); - var profileScores = new Dictionary(); - - var processor = new PersistenceProcessor(cfApi, qpApi, configProvider, () => steps); - processor.PersistCustomFormats(guideCfs, deletedCfsInCache, profileScores); - - steps.JsonTransactionStep.Received().RecordDeletions(Arg.Is(deletedCfsInCache), Arg.Any>()); - } - - [Test] - public void Custom_formats_are_not_deleted_if_deletion_option_is_disabled_in_config() - { - var steps = Substitute.For(); - var cfApi = Substitute.For(); - var qpApi = Substitute.For(); - - var configProvider = Substitute.For(); - configProvider.ActiveConfiguration = new RadarrConfiguration {DeleteOldCustomFormats = false}; - - var guideCfs = Array.Empty(); - var deletedCfsInCache = Array.Empty(); - var profileScores = new Dictionary(); - - var processor = new PersistenceProcessor(cfApi, qpApi, configProvider, () => steps); - processor.PersistCustomFormats(guideCfs, deletedCfsInCache, profileScores); - - steps.JsonTransactionStep.DidNotReceive() - .RecordDeletions(Arg.Any>(), Arg.Any>()); - } - - [Test] - public void Different_active_configuration_is_properly_used() - { - var steps = Substitute.For(); - var cfApi = Substitute.For(); - var qpApi = Substitute.For(); - var configProvider = Substitute.For(); - - var guideCfs = Array.Empty(); - var deletedCfsInCache = Array.Empty(); - var profileScores = new Dictionary(); - - var processor = new PersistenceProcessor(cfApi, qpApi, configProvider, () => steps); - - configProvider.ActiveConfiguration = new RadarrConfiguration {DeleteOldCustomFormats = false}; - processor.PersistCustomFormats(guideCfs, deletedCfsInCache, profileScores); - - configProvider.ActiveConfiguration = new RadarrConfiguration {DeleteOldCustomFormats = true}; - processor.PersistCustomFormats(guideCfs, deletedCfsInCache, profileScores); - - steps.JsonTransactionStep.Received(1) - .RecordDeletions(Arg.Any>(), Arg.Any>()); - } + var steps = Substitute.For(); + var cfApi = Substitute.For(); + var qpApi = Substitute.For(); + var configProvider = Substitute.For(); + + var guideCfs = Array.Empty(); + var deletedCfsInCache = Array.Empty(); + var profileScores = new Dictionary(); + + var processor = new PersistenceProcessor(cfApi, qpApi, configProvider, () => steps); + + configProvider.ActiveConfiguration = new RadarrConfiguration {DeleteOldCustomFormats = false}; + processor.PersistCustomFormats(guideCfs, deletedCfsInCache, profileScores); + + configProvider.ActiveConfiguration = new RadarrConfiguration {DeleteOldCustomFormats = true}; + processor.PersistCustomFormats(guideCfs, deletedCfsInCache, profileScores); + + steps.JsonTransactionStep.Received(1) + .RecordDeletions(Arg.Any>(), Arg.Any>()); } } diff --git a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/CustomFormatApiPersistenceStepTest.cs b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/CustomFormatApiPersistenceStepTest.cs index 5ac82bf5..a585df2f 100644 --- a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/CustomFormatApiPersistenceStepTest.cs +++ b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/CustomFormatApiPersistenceStepTest.cs @@ -8,40 +8,39 @@ using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; using TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps; -namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps +namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class CustomFormatApiPersistenceStepTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class CustomFormatApiPersistenceStepTest + private static ProcessedCustomFormatData QuickMakeCf(string cfName, string trashId, int cfId) { - private static ProcessedCustomFormatData QuickMakeCf(string cfName, string trashId, int cfId) + return new ProcessedCustomFormatData(cfName, trashId, new JObject()) { - return new ProcessedCustomFormatData(cfName, trashId, new JObject()) - { - CacheEntry = new TrashIdMapping(trashId, cfName) {CustomFormatId = cfId} - }; - } + CacheEntry = new TrashIdMapping(trashId, cfName) {CustomFormatId = cfId} + }; + } - [Test] - public async Task All_api_operations_behave_normally() - { - var transactions = new CustomFormatTransactionData(); - transactions.NewCustomFormats.Add(QuickMakeCf("cfname1", "trashid1", 1)); - transactions.UpdatedCustomFormats.Add(QuickMakeCf("cfname2", "trashid2", 2)); - transactions.UnchangedCustomFormats.Add(QuickMakeCf("cfname3", "trashid3", 3)); - transactions.DeletedCustomFormatIds.Add(new TrashIdMapping("trashid4", "cfname4") {CustomFormatId = 4}); + [Test] + public async Task All_api_operations_behave_normally() + { + var transactions = new CustomFormatTransactionData(); + transactions.NewCustomFormats.Add(QuickMakeCf("cfname1", "trashid1", 1)); + transactions.UpdatedCustomFormats.Add(QuickMakeCf("cfname2", "trashid2", 2)); + transactions.UnchangedCustomFormats.Add(QuickMakeCf("cfname3", "trashid3", 3)); + transactions.DeletedCustomFormatIds.Add(new TrashIdMapping("trashid4", "cfname4") {CustomFormatId = 4}); - var api = Substitute.For(); + var api = Substitute.For(); - var processor = new CustomFormatApiPersistenceStep(); - await processor.Process(api, transactions); + var processor = new CustomFormatApiPersistenceStep(); + await processor.Process(api, transactions); - Received.InOrder(() => - { - api.CreateCustomFormat(transactions.NewCustomFormats.First()); - api.UpdateCustomFormat(transactions.UpdatedCustomFormats.First()); - api.DeleteCustomFormat(4); - }); - } + Received.InOrder(() => + { + api.CreateCustomFormat(transactions.NewCustomFormats.First()); + api.UpdateCustomFormat(transactions.UpdatedCustomFormats.First()); + api.DeleteCustomFormat(4); + }); } } diff --git a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/JsonTransactionStepTest.cs b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/JsonTransactionStepTest.cs index 26125fce..36214825 100644 --- a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/JsonTransactionStepTest.cs +++ b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/JsonTransactionStepTest.cs @@ -37,18 +37,18 @@ using TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps; } */ -namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps +namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class JsonTransactionStepTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class JsonTransactionStepTest + [TestCase(1, "cf2")] + [TestCase(2, "cf1")] + [TestCase(null, "cf1")] + public void Updates_using_combination_of_id_and_name(int? id, string guideCfName) { - [TestCase(1, "cf2")] - [TestCase(2, "cf1")] - [TestCase(null, "cf1")] - public void Updates_using_combination_of_id_and_name(int? id, string guideCfName) - { - const string radarrCfData = @"{ + const string radarrCfData = @"{ 'id': 1, 'name': 'cf1', 'specifications': [{ @@ -59,7 +59,7 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps }] }] }"; - var guideCfData = JObject.Parse(@"{ + var guideCfData = JObject.Parse(@"{ 'name': 'cf1', 'specifications': [{ 'name': 'spec1', @@ -69,21 +69,21 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps } }] }"); - var cacheEntry = id != null ? new TrashIdMapping("", "") {CustomFormatId = id.Value} : null; + var cacheEntry = id != null ? new TrashIdMapping("", "") {CustomFormatId = id.Value} : null; - var guideCfs = new List - { - new(guideCfName, "", guideCfData) {CacheEntry = cacheEntry} - }; + var guideCfs = new List + { + new(guideCfName, "", guideCfData) {CacheEntry = cacheEntry} + }; - var processor = new JsonTransactionStep(); - processor.Process(guideCfs, new[] {JObject.Parse(radarrCfData)}); + var processor = new JsonTransactionStep(); + processor.Process(guideCfs, new[] {JObject.Parse(radarrCfData)}); - var expectedTransactions = new CustomFormatTransactionData(); - expectedTransactions.UpdatedCustomFormats.Add(guideCfs[0]); - processor.Transactions.Should().BeEquivalentTo(expectedTransactions); + var expectedTransactions = new CustomFormatTransactionData(); + expectedTransactions.UpdatedCustomFormats.Add(guideCfs[0]); + processor.Transactions.Should().BeEquivalentTo(expectedTransactions); - const string expectedJsonData = @"{ + const string expectedJsonData = @"{ 'id': 1, 'name': 'cf1', 'specifications': [{ @@ -95,14 +95,14 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps }] }] }"; - processor.Transactions.UpdatedCustomFormats.First().Json.Should() - .BeEquivalentTo(JObject.Parse(expectedJsonData), op => op.Using(new JsonEquivalencyStep())); - } + processor.Transactions.UpdatedCustomFormats.First().Json.Should() + .BeEquivalentTo(JObject.Parse(expectedJsonData), op => op.Using(new JsonEquivalencyStep())); + } - [Test] - public void Combination_of_create_update_and_unchanged_and_verify_proper_json_merging() - { - const string radarrCfData = @"[{ + [Test] + public void Combination_of_create_update_and_unchanged_and_verify_proper_json_merging() + { + const string radarrCfData = @"[{ 'id': 1, 'name': 'user_defined', 'specifications': [{ @@ -137,7 +137,7 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps }] }] }]"; - var guideCfData = JsonConvert.DeserializeObject>(@"[{ + var guideCfData = JsonConvert.DeserializeObject>(@"[{ 'name': 'created', 'specifications': [{ 'name': 'spec5', @@ -172,23 +172,23 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps }] }]"); - var radarrCfs = JsonConvert.DeserializeObject>(radarrCfData); - var guideCfs = new List + var radarrCfs = JsonConvert.DeserializeObject>(radarrCfData); + var guideCfs = new List + { + new("created", "", guideCfData![0]), + new("updated_different_name", "", guideCfData[1]) { - new("created", "", guideCfData![0]), - new("updated_different_name", "", guideCfData[1]) - { - CacheEntry = new TrashIdMapping("", "", 2) - }, - new("no_change", "", guideCfData[2]) - }; + CacheEntry = new TrashIdMapping("", "", 2) + }, + new("no_change", "", guideCfData[2]) + }; - var processor = new JsonTransactionStep(); - processor.Process(guideCfs, radarrCfs!); + var processor = new JsonTransactionStep(); + processor.Process(guideCfs, radarrCfs!); - var expectedJson = new[] - { - @"{ + var expectedJson = new[] + { + @"{ 'name': 'created', 'specifications': [{ 'name': 'spec5', @@ -198,7 +198,7 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps }] }] }", - @"{ + @"{ 'id': 2, 'name': 'updated_different_name', 'specifications': [{ @@ -219,7 +219,7 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps }] }] }", - @"{ + @"{ 'id': 3, 'name': 'no_change', 'specifications': [{ @@ -231,28 +231,28 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps }] }] }" - }; + }; - var expectedTransactions = new CustomFormatTransactionData(); - expectedTransactions.NewCustomFormats.Add(guideCfs[0]); - expectedTransactions.UpdatedCustomFormats.Add(guideCfs[1]); - expectedTransactions.UnchangedCustomFormats.Add(guideCfs[2]); - processor.Transactions.Should().BeEquivalentTo(expectedTransactions); + var expectedTransactions = new CustomFormatTransactionData(); + expectedTransactions.NewCustomFormats.Add(guideCfs[0]); + expectedTransactions.UpdatedCustomFormats.Add(guideCfs[1]); + expectedTransactions.UnchangedCustomFormats.Add(guideCfs[2]); + processor.Transactions.Should().BeEquivalentTo(expectedTransactions); - processor.Transactions.NewCustomFormats.First().Json.Should() - .BeEquivalentTo(JObject.Parse(expectedJson[0]), op => op.Using(new JsonEquivalencyStep())); + processor.Transactions.NewCustomFormats.First().Json.Should() + .BeEquivalentTo(JObject.Parse(expectedJson[0]), op => op.Using(new JsonEquivalencyStep())); - processor.Transactions.UpdatedCustomFormats.First().Json.Should() - .BeEquivalentTo(JObject.Parse(expectedJson[1]), op => op.Using(new JsonEquivalencyStep())); + processor.Transactions.UpdatedCustomFormats.First().Json.Should() + .BeEquivalentTo(JObject.Parse(expectedJson[1]), op => op.Using(new JsonEquivalencyStep())); - processor.Transactions.UnchangedCustomFormats.First().Json.Should() - .BeEquivalentTo(JObject.Parse(expectedJson[2]), op => op.Using(new JsonEquivalencyStep())); - } + processor.Transactions.UnchangedCustomFormats.First().Json.Should() + .BeEquivalentTo(JObject.Parse(expectedJson[2]), op => op.Using(new JsonEquivalencyStep())); + } - [Test] - public void Deletes_happen_before_updates() - { - const string radarrCfData = @"[{ + [Test] + public void Deletes_happen_before_updates() + { + const string radarrCfData = @"[{ 'id': 1, 'name': 'updated', 'specifications': [{ @@ -275,7 +275,7 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps }] }] }]"; - var guideCfData = JObject.Parse(@"{ + var guideCfData = JObject.Parse(@"{ 'name': 'updated', 'specifications': [{ 'name': 'spec2', @@ -284,23 +284,23 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps } }] }"); - var deletedCfsInCache = new List - { - new("", "") {CustomFormatId = 2} - }; + var deletedCfsInCache = new List + { + new("", "") {CustomFormatId = 2} + }; - var guideCfs = new List - { - new("updated", "", guideCfData) {CacheEntry = new TrashIdMapping("", "") {CustomFormatId = 1}} - }; + var guideCfs = new List + { + new("updated", "", guideCfData) {CacheEntry = new TrashIdMapping("", "") {CustomFormatId = 1}} + }; - var radarrCfs = JsonConvert.DeserializeObject>(radarrCfData); + var radarrCfs = JsonConvert.DeserializeObject>(radarrCfData); - var processor = new JsonTransactionStep(); - processor.Process(guideCfs, radarrCfs!); - processor.RecordDeletions(deletedCfsInCache, radarrCfs!); + var processor = new JsonTransactionStep(); + processor.Process(guideCfs, radarrCfs!); + processor.RecordDeletions(deletedCfsInCache, radarrCfs!); - var expectedJson = @"{ + var expectedJson = @"{ 'id': 1, 'name': 'updated', 'specifications': [{ @@ -311,19 +311,19 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps }] }] }"; - var expectedTransactions = new CustomFormatTransactionData(); - expectedTransactions.DeletedCustomFormatIds.Add(new TrashIdMapping("", "", 2)); - expectedTransactions.UpdatedCustomFormats.Add(guideCfs[0]); - processor.Transactions.Should().BeEquivalentTo(expectedTransactions); + var expectedTransactions = new CustomFormatTransactionData(); + expectedTransactions.DeletedCustomFormatIds.Add(new TrashIdMapping("", "", 2)); + expectedTransactions.UpdatedCustomFormats.Add(guideCfs[0]); + processor.Transactions.Should().BeEquivalentTo(expectedTransactions); - processor.Transactions.UpdatedCustomFormats.First().Json.Should() - .BeEquivalentTo(JObject.Parse(expectedJson), op => op.Using(new JsonEquivalencyStep())); - } + processor.Transactions.UpdatedCustomFormats.First().Json.Should() + .BeEquivalentTo(JObject.Parse(expectedJson), op => op.Using(new JsonEquivalencyStep())); + } - [Test] - public void Only_delete_correct_cfs() - { - const string radarrCfData = @"[{ + [Test] + public void Only_delete_correct_cfs() + { + const string radarrCfData = @"[{ 'id': 1, 'name': 'not_deleted', 'specifications': [{ @@ -347,26 +347,26 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps }] }] }]"; - var deletedCfsInCache = new List - { - new("testtrashid", "testname") {CustomFormatId = 2}, - new("", "not_deleted") {CustomFormatId = 3} - }; + var deletedCfsInCache = new List + { + new("testtrashid", "testname") {CustomFormatId = 2}, + new("", "not_deleted") {CustomFormatId = 3} + }; - var radarrCfs = JsonConvert.DeserializeObject>(radarrCfData); + var radarrCfs = JsonConvert.DeserializeObject>(radarrCfData); - var processor = new JsonTransactionStep(); - processor.RecordDeletions(deletedCfsInCache, radarrCfs!); + var processor = new JsonTransactionStep(); + processor.RecordDeletions(deletedCfsInCache, radarrCfs!); - var expectedTransactions = new CustomFormatTransactionData(); - expectedTransactions.DeletedCustomFormatIds.Add(new TrashIdMapping("testtrashid", "testname", 2)); - processor.Transactions.Should().BeEquivalentTo(expectedTransactions); - } + var expectedTransactions = new CustomFormatTransactionData(); + expectedTransactions.DeletedCustomFormatIds.Add(new TrashIdMapping("testtrashid", "testname", 2)); + processor.Transactions.Should().BeEquivalentTo(expectedTransactions); + } - [Test] - public void Updated_and_unchanged_custom_formats_have_cache_entry_set_when_there_is_no_cache() - { - const string radarrCfData = @"[{ + [Test] + public void Updated_and_unchanged_custom_formats_have_cache_entry_set_when_there_is_no_cache() + { + const string radarrCfData = @"[{ 'id': 1, 'name': 'updated', 'specifications': [{ @@ -388,7 +388,7 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps }] }] }]"; - var guideCfData = JsonConvert.DeserializeObject>(@"[{ + var guideCfData = JsonConvert.DeserializeObject>(@"[{ 'name': 'updated', 'specifications': [{ 'name': 'spec2', @@ -407,21 +407,20 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps }] }]"); - var radarrCfs = JsonConvert.DeserializeObject>(radarrCfData); - var guideCfs = new List - { - new("updated", "", guideCfData![0]), - new("no_change", "", guideCfData[1]) - }; + var radarrCfs = JsonConvert.DeserializeObject>(radarrCfData); + var guideCfs = new List + { + new("updated", "", guideCfData![0]), + new("no_change", "", guideCfData[1]) + }; - var processor = new JsonTransactionStep(); - processor.Process(guideCfs, radarrCfs!); + var processor = new JsonTransactionStep(); + processor.Process(guideCfs, radarrCfs!); - processor.Transactions.UpdatedCustomFormats.First().CacheEntry.Should() - .BeEquivalentTo(new TrashIdMapping("", "updated", 1)); + processor.Transactions.UpdatedCustomFormats.First().CacheEntry.Should() + .BeEquivalentTo(new TrashIdMapping("", "updated", 1)); - processor.Transactions.UnchangedCustomFormats.First().CacheEntry.Should() - .BeEquivalentTo(new TrashIdMapping("", "no_change", 2)); - } + processor.Transactions.UnchangedCustomFormats.First().CacheEntry.Should() + .BeEquivalentTo(new TrashIdMapping("", "no_change", 2)); } } diff --git a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStepTest.cs b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStepTest.cs index bfd20ed5..8bff1a94 100644 --- a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStepTest.cs +++ b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStepTest.cs @@ -12,16 +12,16 @@ using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; using TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps; -namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps +namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class QualityProfileApiPersistenceStepTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class QualityProfileApiPersistenceStepTest + [Test] + public void Do_not_invoke_api_if_no_scores_to_update() { - [Test] - public void Do_not_invoke_api_if_no_scores_to_update() - { - const string radarrQualityProfileData = @"[{ + const string radarrQualityProfileData = @"[{ 'name': 'profile1', 'formatItems': [{ 'format': 1, @@ -42,50 +42,50 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps 'id': 1 }]"; - var api = Substitute.For(); - api.GetQualityProfiles()!.Returns(JsonConvert.DeserializeObject>(radarrQualityProfileData)); + var api = Substitute.For(); + api.GetQualityProfiles()!.Returns(JsonConvert.DeserializeObject>(radarrQualityProfileData)); - var cfScores = new Dictionary + var cfScores = new Dictionary + { { - { - "profile1", CfTestUtils.NewMapping( - new FormatMappingEntry(new ProcessedCustomFormatData("", "", new JObject()) - { - CacheEntry = new TrashIdMapping("", "") {CustomFormatId = 4} - }, 100)) - } - }; + "profile1", CfTestUtils.NewMapping( + new FormatMappingEntry(new ProcessedCustomFormatData("", "", new JObject()) + { + CacheEntry = new TrashIdMapping("", "") {CustomFormatId = 4} + }, 100)) + } + }; - var processor = new QualityProfileApiPersistenceStep(); - processor.Process(api, cfScores); + var processor = new QualityProfileApiPersistenceStep(); + processor.Process(api, cfScores); - api.DidNotReceive().UpdateQualityProfile(Arg.Any(), Arg.Any()); - } + api.DidNotReceive().UpdateQualityProfile(Arg.Any(), Arg.Any()); + } - [Test] - public void Invalid_quality_profile_names_are_reported() - { - const string radarrQualityProfileData = @"[{'name': 'profile1'}]"; + [Test] + public void Invalid_quality_profile_names_are_reported() + { + const string radarrQualityProfileData = @"[{'name': 'profile1'}]"; - var api = Substitute.For(); - api.GetQualityProfiles()!.Returns(JsonConvert.DeserializeObject>(radarrQualityProfileData)); + var api = Substitute.For(); + api.GetQualityProfiles()!.Returns(JsonConvert.DeserializeObject>(radarrQualityProfileData)); - var cfScores = new Dictionary - { - {"wrong_profile_name", CfTestUtils.NewMapping()} - }; + var cfScores = new Dictionary + { + {"wrong_profile_name", CfTestUtils.NewMapping()} + }; - var processor = new QualityProfileApiPersistenceStep(); - processor.Process(api, cfScores); + var processor = new QualityProfileApiPersistenceStep(); + processor.Process(api, cfScores); - processor.InvalidProfileNames.Should().Equal("wrong_profile_name"); - processor.UpdatedScores.Should().BeEmpty(); - } + processor.InvalidProfileNames.Should().Equal("wrong_profile_name"); + processor.UpdatedScores.Should().BeEmpty(); + } - [Test] - public void Reset_scores_for_unmatched_cfs_if_enabled() - { - const string radarrQualityProfileData = @"[{ + [Test] + public void Reset_scores_for_unmatched_cfs_if_enabled() + { + const string radarrQualityProfileData = @"[{ 'name': 'profile1', 'formatItems': [{ 'format': 1, @@ -106,42 +106,42 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps 'id': 1 }]"; - var api = Substitute.For(); - api.GetQualityProfiles()!.Returns(JsonConvert.DeserializeObject>(radarrQualityProfileData)); + var api = Substitute.For(); + api.GetQualityProfiles()!.Returns(JsonConvert.DeserializeObject>(radarrQualityProfileData)); - var cfScores = new Dictionary + var cfScores = new Dictionary + { { - { - "profile1", CfTestUtils.NewMappingWithReset( - new FormatMappingEntry(new ProcessedCustomFormatData("", "", new JObject()) - { - CacheEntry = new TrashIdMapping("", "", 2) - }, 100)) - } - }; + "profile1", CfTestUtils.NewMappingWithReset( + new FormatMappingEntry(new ProcessedCustomFormatData("", "", new JObject()) + { + CacheEntry = new TrashIdMapping("", "", 2) + }, 100)) + } + }; - var processor = new QualityProfileApiPersistenceStep(); - processor.Process(api, cfScores); + var processor = new QualityProfileApiPersistenceStep(); + processor.Process(api, cfScores); - processor.InvalidProfileNames.Should().BeEmpty(); - processor.UpdatedScores.Should() - .ContainKey("profile1").WhoseValue.Should() - .BeEquivalentTo(new List - { - new("cf1", 0, FormatScoreUpdateReason.Reset), - new("cf2", 100, FormatScoreUpdateReason.Updated), - new("cf3", 0, FormatScoreUpdateReason.Reset) - }); + processor.InvalidProfileNames.Should().BeEmpty(); + processor.UpdatedScores.Should() + .ContainKey("profile1").WhoseValue.Should() + .BeEquivalentTo(new List + { + new("cf1", 0, FormatScoreUpdateReason.Reset), + new("cf2", 100, FormatScoreUpdateReason.Updated), + new("cf3", 0, FormatScoreUpdateReason.Reset) + }); - api.Received().UpdateQualityProfile( - Verify.That(j => j["formatItems"]!.Children().Should().HaveCount(3)), - Arg.Any()); - } + api.Received().UpdateQualityProfile( + Verify.That(j => j["formatItems"]!.Children().Should().HaveCount(3)), + Arg.Any()); + } - [Test] - public void Scores_are_set_in_quality_profile() - { - const string radarrQualityProfileData = @"[{ + [Test] + public void Scores_are_set_in_quality_profile() + { + const string radarrQualityProfileData = @"[{ 'name': 'profile1', 'upgradeAllowed': false, 'cutoff': 20, @@ -182,35 +182,35 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps 'id': 1 }]"; - var api = Substitute.For(); - api.GetQualityProfiles()!.Returns(JsonConvert.DeserializeObject>(radarrQualityProfileData)); + var api = Substitute.For(); + api.GetQualityProfiles()!.Returns(JsonConvert.DeserializeObject>(radarrQualityProfileData)); - var cfScores = new Dictionary + var cfScores = new Dictionary + { { - { - "profile1", CfTestUtils.NewMapping( - new FormatMappingEntry(new ProcessedCustomFormatData("", "", new JObject()) - { - // First match by ID - CacheEntry = new TrashIdMapping("", "", 4) - }, 100), - new FormatMappingEntry(new ProcessedCustomFormatData("", "", new JObject()) - { - // Should NOT match because we do not use names to assign scores - CacheEntry = new TrashIdMapping("", "BR-DISK") - }, 101), - new FormatMappingEntry(new ProcessedCustomFormatData("", "", new JObject()) - { - // Second match by ID - CacheEntry = new TrashIdMapping("", "", 1) - }, 102)) - } - }; + "profile1", CfTestUtils.NewMapping( + new FormatMappingEntry(new ProcessedCustomFormatData("", "", new JObject()) + { + // First match by ID + CacheEntry = new TrashIdMapping("", "", 4) + }, 100), + new FormatMappingEntry(new ProcessedCustomFormatData("", "", new JObject()) + { + // Should NOT match because we do not use names to assign scores + CacheEntry = new TrashIdMapping("", "BR-DISK") + }, 101), + new FormatMappingEntry(new ProcessedCustomFormatData("", "", new JObject()) + { + // Second match by ID + CacheEntry = new TrashIdMapping("", "", 1) + }, 102)) + } + }; - var processor = new QualityProfileApiPersistenceStep(); - processor.Process(api, cfScores); + var processor = new QualityProfileApiPersistenceStep(); + processor.Process(api, cfScores); - var expectedProfileJson = JObject.Parse(@"{ + var expectedProfileJson = JObject.Parse(@"{ 'name': 'profile1', 'upgradeAllowed': false, 'cutoff': 20, @@ -251,16 +251,15 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps 'id': 1 }"); - api.Received() - .UpdateQualityProfile(Verify.That(a => a.Should().BeEquivalentTo(expectedProfileJson)), 1); - processor.InvalidProfileNames.Should().BeEmpty(); - processor.UpdatedScores.Should() - .ContainKey("profile1").WhoseValue.Should() - .BeEquivalentTo(new List - { - new("3D", 100, FormatScoreUpdateReason.Updated), - new("asdf2", 102, FormatScoreUpdateReason.Updated) - }); - } + api.Received() + .UpdateQualityProfile(Verify.That(a => a.Should().BeEquivalentTo(expectedProfileJson)), 1); + processor.InvalidProfileNames.Should().BeEmpty(); + processor.UpdatedScores.Should() + .ContainKey("profile1").WhoseValue.Should() + .BeEquivalentTo(new List + { + new("3D", 100, FormatScoreUpdateReason.Updated), + new("asdf2", 102, FormatScoreUpdateReason.Updated) + }); } } diff --git a/src/TrashLib.Tests/Radarr/QualityDefinition/RadarrQualityDataTest.cs b/src/TrashLib.Tests/Radarr/QualityDefinition/RadarrQualityDataTest.cs index 981ed978..ec24028f 100644 --- a/src/TrashLib.Tests/Radarr/QualityDefinition/RadarrQualityDataTest.cs +++ b/src/TrashLib.Tests/Radarr/QualityDefinition/RadarrQualityDataTest.cs @@ -2,120 +2,119 @@ using FluentAssertions; using NUnit.Framework; using TrashLib.Radarr.QualityDefinition; -namespace TrashLib.Tests.Radarr.QualityDefinition +namespace TrashLib.Tests.Radarr.QualityDefinition; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class RadarrQualityDataTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class RadarrQualityDataTest + private static readonly object[] PreferredTestValues = { - private static readonly object[] PreferredTestValues = - { - new object?[] {100m, 100m, false}, - new object?[] {100m, 101m, true}, - new object?[] {100m, 98m, true}, - new object?[] {100m, null, true}, - new object?[] {RadarrQualityData.PreferredUnlimitedThreshold, null, false}, - new object?[] {RadarrQualityData.PreferredUnlimitedThreshold - 1, null, true}, - new object?[] - {RadarrQualityData.PreferredUnlimitedThreshold, RadarrQualityData.PreferredUnlimitedThreshold, true} - }; + new object?[] {100m, 100m, false}, + new object?[] {100m, 101m, true}, + new object?[] {100m, 98m, true}, + new object?[] {100m, null, true}, + new object?[] {RadarrQualityData.PreferredUnlimitedThreshold, null, false}, + new object?[] {RadarrQualityData.PreferredUnlimitedThreshold - 1, null, true}, + new object?[] + {RadarrQualityData.PreferredUnlimitedThreshold, RadarrQualityData.PreferredUnlimitedThreshold, true} + }; - [TestCaseSource(nameof(PreferredTestValues))] - public void PreferredDifferent_WithVariousValues_ReturnsExpectedResult(decimal guideValue, decimal? radarrValue, - bool isDifferent) - { - var data = new RadarrQualityData {Preferred = guideValue}; - data.IsPreferredDifferent(radarrValue) - .Should().Be(isDifferent); - } + [TestCaseSource(nameof(PreferredTestValues))] + public void PreferredDifferent_WithVariousValues_ReturnsExpectedResult(decimal guideValue, decimal? radarrValue, + bool isDifferent) + { + var data = new RadarrQualityData {Preferred = guideValue}; + data.IsPreferredDifferent(radarrValue) + .Should().Be(isDifferent); + } - private static readonly object[] InterpolatedPreferredTestParams = + private static readonly object[] InterpolatedPreferredTestParams = + { + new[] { - new[] - { - 400m, - 1.0m, - RadarrQualityData.PreferredUnlimitedThreshold - }, - new[] - { - RadarrQualityData.PreferredUnlimitedThreshold, - 1.0m, - RadarrQualityData.PreferredUnlimitedThreshold - }, - new[] - { - RadarrQualityData.PreferredUnlimitedThreshold - 1m, - 1.0m, - RadarrQualityData.PreferredUnlimitedThreshold - 1m - }, - new[] - { - 10m, - 0m, - 0m - }, - new[] - { - 100m, - 0.5m, - 50m - } - }; - - [TestCaseSource(nameof(InterpolatedPreferredTestParams))] - public void InterpolatedPreferred_VariousValues_ExpectedResults(decimal max, decimal ratio, - decimal expectedResult) + 400m, + 1.0m, + RadarrQualityData.PreferredUnlimitedThreshold + }, + new[] { - var data = new RadarrQualityData {Min = 0, Max = max}; - data.InterpolatedPreferred(ratio).Should().Be(expectedResult); - } - - [Test] - public void AnnotatedPreferred_OutsideThreshold_EqualsSameValueWithUnlimited() + RadarrQualityData.PreferredUnlimitedThreshold, + 1.0m, + RadarrQualityData.PreferredUnlimitedThreshold + }, + new[] { - const decimal testVal = RadarrQualityData.PreferredUnlimitedThreshold; - var data = new RadarrQualityData {Preferred = testVal}; - data.AnnotatedPreferred.Should().Be($"{testVal} (Unlimited)"); - } - - [Test] - public void AnnotatedPreferred_WithinThreshold_EqualsSameStringValue() + RadarrQualityData.PreferredUnlimitedThreshold - 1m, + 1.0m, + RadarrQualityData.PreferredUnlimitedThreshold - 1m + }, + new[] { - const decimal testVal = RadarrQualityData.PreferredUnlimitedThreshold - 1; - var data = new RadarrQualityData {Preferred = testVal}; - data.AnnotatedPreferred.Should().Be($"{testVal}"); - } - - [Test] - public void Preferred_AboveThreshold_EqualsSameValue() + 10m, + 0m, + 0m + }, + new[] { - const decimal testVal = RadarrQualityData.PreferredUnlimitedThreshold + 1; - var data = new RadarrQualityData {Preferred = testVal}; - data.Preferred.Should().Be(testVal); + 100m, + 0.5m, + 50m } + }; - [Test] - public void PreferredForApi_AboveThreshold_EqualsNull() - { - const decimal testVal = RadarrQualityData.PreferredUnlimitedThreshold + 1; - var data = new RadarrQualityData {Preferred = testVal}; - data.PreferredForApi.Should().Be(null); - } + [TestCaseSource(nameof(InterpolatedPreferredTestParams))] + public void InterpolatedPreferred_VariousValues_ExpectedResults(decimal max, decimal ratio, + decimal expectedResult) + { + var data = new RadarrQualityData {Min = 0, Max = max}; + data.InterpolatedPreferred(ratio).Should().Be(expectedResult); + } - [Test] - public void PreferredForApi_HighestWithinThreshold_EqualsSameValue() - { - const decimal testVal = RadarrQualityData.PreferredUnlimitedThreshold - 0.1m; - var data = new RadarrQualityData {Preferred = testVal}; - data.PreferredForApi.Should().Be(testVal).And.Be(data.Preferred); - } + [Test] + public void AnnotatedPreferred_OutsideThreshold_EqualsSameValueWithUnlimited() + { + const decimal testVal = RadarrQualityData.PreferredUnlimitedThreshold; + var data = new RadarrQualityData {Preferred = testVal}; + data.AnnotatedPreferred.Should().Be($"{testVal} (Unlimited)"); + } - [Test] - public void PreferredForApi_LowestWithinThreshold_EqualsSameValue() - { - var data = new RadarrQualityData {Preferred = 0}; - data.PreferredForApi.Should().Be(0); - } + [Test] + public void AnnotatedPreferred_WithinThreshold_EqualsSameStringValue() + { + const decimal testVal = RadarrQualityData.PreferredUnlimitedThreshold - 1; + var data = new RadarrQualityData {Preferred = testVal}; + data.AnnotatedPreferred.Should().Be($"{testVal}"); + } + + [Test] + public void Preferred_AboveThreshold_EqualsSameValue() + { + const decimal testVal = RadarrQualityData.PreferredUnlimitedThreshold + 1; + var data = new RadarrQualityData {Preferred = testVal}; + data.Preferred.Should().Be(testVal); + } + + [Test] + public void PreferredForApi_AboveThreshold_EqualsNull() + { + const decimal testVal = RadarrQualityData.PreferredUnlimitedThreshold + 1; + var data = new RadarrQualityData {Preferred = testVal}; + data.PreferredForApi.Should().Be(null); + } + + [Test] + public void PreferredForApi_HighestWithinThreshold_EqualsSameValue() + { + const decimal testVal = RadarrQualityData.PreferredUnlimitedThreshold - 0.1m; + var data = new RadarrQualityData {Preferred = testVal}; + data.PreferredForApi.Should().Be(testVal).And.Be(data.Preferred); + } + + [Test] + public void PreferredForApi_LowestWithinThreshold_EqualsSameValue() + { + var data = new RadarrQualityData {Preferred = 0}; + data.PreferredForApi.Should().Be(0); } } diff --git a/src/TrashLib.Tests/Radarr/RadarrConfigurationTest.cs b/src/TrashLib.Tests/Radarr/RadarrConfigurationTest.cs index c26a68ce..f0b95290 100644 --- a/src/TrashLib.Tests/Radarr/RadarrConfigurationTest.cs +++ b/src/TrashLib.Tests/Radarr/RadarrConfigurationTest.cs @@ -10,102 +10,101 @@ using TrashLib.Radarr; using TrashLib.Radarr.Config; using TrashLib.Radarr.QualityDefinition; -namespace TrashLib.Tests.Radarr +namespace TrashLib.Tests.Radarr; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class RadarrConfigurationTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class RadarrConfigurationTest - { - private IContainer _container = default!; + private IContainer _container = default!; - [OneTimeSetUp] - public void Setup() - { - var builder = new ContainerBuilder(); - builder.RegisterModule(); - builder.RegisterModule(); - _container = builder.Build(); - } + [OneTimeSetUp] + public void Setup() + { + var builder = new ContainerBuilder(); + builder.RegisterModule(); + builder.RegisterModule(); + _container = builder.Build(); + } - private static readonly TestCaseData[] NameOrIdsTestData = - { - new(new Collection {"name"}, new Collection()), - new(new Collection(), new Collection {"trash_id"}) - }; + private static readonly TestCaseData[] NameOrIdsTestData = + { + new(new Collection {"name"}, new Collection()), + new(new Collection(), new Collection {"trash_id"}) + }; - [TestCaseSource(nameof(NameOrIdsTestData))] - public void Custom_format_is_valid_with_one_of_either_names_or_trash_id(Collection namesList, - Collection trashIdsList) + [TestCaseSource(nameof(NameOrIdsTestData))] + public void Custom_format_is_valid_with_one_of_either_names_or_trash_id(Collection namesList, + Collection trashIdsList) + { + var config = new RadarrConfiguration { - var config = new RadarrConfiguration + ApiKey = "required value", + BaseUrl = "required value", + CustomFormats = new List { - ApiKey = "required value", - BaseUrl = "required value", - CustomFormats = new List - { - new() {Names = namesList, TrashIds = trashIdsList} - } - }; + new() {Names = namesList, TrashIds = trashIdsList} + } + }; - var validator = _container.Resolve>(); - var result = validator.Validate(config); + var validator = _container.Resolve>(); + var result = validator.Validate(config); - result.IsValid.Should().BeTrue(); - result.Errors.Should().BeEmpty(); - } + result.IsValid.Should().BeTrue(); + result.Errors.Should().BeEmpty(); + } - [Test] - public void Validation_fails_for_all_missing_required_properties() - { - // default construct which should yield default values (invalid) for all required properties - var config = new RadarrConfiguration(); - var validator = _container.Resolve>(); + [Test] + public void Validation_fails_for_all_missing_required_properties() + { + // default construct which should yield default values (invalid) for all required properties + var config = new RadarrConfiguration(); + var validator = _container.Resolve>(); - var result = validator.Validate(config); + var result = validator.Validate(config); - var expectedErrorMessageSubstrings = new[] - { - "Property 'base_url' is required", - "Property 'api_key' is required", - "'custom_formats' elements must contain at least one element in either 'names' or 'trash_ids'", - "'name' is required for elements under 'quality_profiles'", - "'type' is required for 'quality_definition'" - }; + var expectedErrorMessageSubstrings = new[] + { + "Property 'base_url' is required", + "Property 'api_key' is required", + "'custom_formats' elements must contain at least one element in either 'names' or 'trash_ids'", + "'name' is required for elements under 'quality_profiles'", + "'type' is required for 'quality_definition'" + }; - result.IsValid.Should().BeFalse(); - result.Errors.Select(e => e.ErrorMessage).Should() - .OnlyContain(x => expectedErrorMessageSubstrings.Any(x.Contains)); - } + result.IsValid.Should().BeFalse(); + result.Errors.Select(e => e.ErrorMessage).Should() + .OnlyContain(x => expectedErrorMessageSubstrings.Any(x.Contains)); + } - [Test] - public void Validation_succeeds_when_no_missing_required_properties() + [Test] + public void Validation_succeeds_when_no_missing_required_properties() + { + var config = new RadarrConfiguration { - var config = new RadarrConfiguration + ApiKey = "required value", + BaseUrl = "required value", + CustomFormats = new List { - ApiKey = "required value", - BaseUrl = "required value", - CustomFormats = new List + new() { - new() + Names = new List {"required value"}, + QualityProfiles = new List { - Names = new List {"required value"}, - QualityProfiles = new List - { - new() {Name = "required value"} - } + new() {Name = "required value"} } - }, - QualityDefinition = new QualityDefinitionConfig - { - Type = RadarrQualityDefinitionType.Movie } - }; + }, + QualityDefinition = new QualityDefinitionConfig + { + Type = RadarrQualityDefinitionType.Movie + } + }; - var validator = _container.Resolve>(); - var result = validator.Validate(config); + var validator = _container.Resolve>(); + var result = validator.Validate(config); - result.IsValid.Should().BeTrue(); - result.Errors.Should().BeEmpty(); - } + result.IsValid.Should().BeTrue(); + result.Errors.Should().BeEmpty(); } } diff --git a/src/TrashLib.Tests/Sonarr/Api/SonarrReleaseProfileCompatibilityHandlerTest.cs b/src/TrashLib.Tests/Sonarr/Api/SonarrReleaseProfileCompatibilityHandlerTest.cs index af4ff295..6e8a149b 100644 --- a/src/TrashLib.Tests/Sonarr/Api/SonarrReleaseProfileCompatibilityHandlerTest.cs +++ b/src/TrashLib.Tests/Sonarr/Api/SonarrReleaseProfileCompatibilityHandlerTest.cs @@ -14,105 +14,104 @@ using TrashLib.Sonarr.Api; using TrashLib.Sonarr.Api.Objects; using TrashLib.Startup; -namespace TrashLib.Tests.Sonarr.Api +namespace TrashLib.Tests.Sonarr.Api; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class SonarrReleaseProfileCompatibilityHandlerTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class SonarrReleaseProfileCompatibilityHandlerTest + private class TestContext : IDisposable { - private class TestContext : IDisposable - { - private readonly JsonSerializerSettings _jsonSettings; + private readonly JsonSerializerSettings _jsonSettings; - public TestContext() + public TestContext() + { + _jsonSettings = new JsonSerializerSettings { - _jsonSettings = new JsonSerializerSettings - { - ContractResolver = new CamelCasePropertyNamesContractResolver() - }; - - Mapper = AutoMapperConfig.Setup(); - } + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; - public IMapper Mapper { get; } + Mapper = AutoMapperConfig.Setup(); + } - public void Dispose() - { - } + public IMapper Mapper { get; } - public string SerializeJson(T obj) - { - return JsonConvert.SerializeObject(obj, _jsonSettings); - } + public void Dispose() + { } - [Test] - public void Receive_v1_to_v2() + public string SerializeJson(T obj) { - using var ctx = new TestContext(); + return JsonConvert.SerializeObject(obj, _jsonSettings); + } + } - var compat = Substitute.For(); - var dataV1 = new SonarrReleaseProfileV1 {Ignored = "one,two,three"}; - var sut = new SonarrReleaseProfileCompatibilityHandler(compat, ctx.Mapper); + [Test] + public void Receive_v1_to_v2() + { + using var ctx = new TestContext(); - var result = sut.CompatibleReleaseProfileForReceiving(JObject.Parse(ctx.SerializeJson(dataV1))); + var compat = Substitute.For(); + var dataV1 = new SonarrReleaseProfileV1 {Ignored = "one,two,three"}; + var sut = new SonarrReleaseProfileCompatibilityHandler(compat, ctx.Mapper); - result.Should().BeEquivalentTo(new SonarrReleaseProfile - { - Ignored = new List {"one", "two", "three"} - }); - } + var result = sut.CompatibleReleaseProfileForReceiving(JObject.Parse(ctx.SerializeJson(dataV1))); - [Test] - public void Receive_v2_to_v2() + result.Should().BeEquivalentTo(new SonarrReleaseProfile { - using var ctx = new TestContext(); + Ignored = new List {"one", "two", "three"} + }); + } - var compat = Substitute.For(); - var dataV2 = new SonarrReleaseProfile {Ignored = new List {"one", "two", "three"}}; - var sut = new SonarrReleaseProfileCompatibilityHandler(compat, ctx.Mapper); + [Test] + public void Receive_v2_to_v2() + { + using var ctx = new TestContext(); - var result = sut.CompatibleReleaseProfileForReceiving(JObject.Parse(ctx.SerializeJson(dataV2))); + var compat = Substitute.For(); + var dataV2 = new SonarrReleaseProfile {Ignored = new List {"one", "two", "three"}}; + var sut = new SonarrReleaseProfileCompatibilityHandler(compat, ctx.Mapper); - result.Should().BeEquivalentTo(dataV2); - } + var result = sut.CompatibleReleaseProfileForReceiving(JObject.Parse(ctx.SerializeJson(dataV2))); + + result.Should().BeEquivalentTo(dataV2); + } - [Test] - public async Task Send_v2_to_v1() + [Test] + public async Task Send_v2_to_v1() + { + using var ctx = new TestContext(); + + var compat = Substitute.For(); + compat.Capabilities.Returns(new[] { - using var ctx = new TestContext(); + new SonarrCapabilities {ArraysNeededForReleaseProfileRequiredAndIgnored = false} + }.ToObservable()); - var compat = Substitute.For(); - compat.Capabilities.Returns(new[] - { - new SonarrCapabilities {ArraysNeededForReleaseProfileRequiredAndIgnored = false} - }.ToObservable()); + var data = new SonarrReleaseProfile {Ignored = new List {"one", "two", "three"}}; + var sut = new SonarrReleaseProfileCompatibilityHandler(compat, ctx.Mapper); - var data = new SonarrReleaseProfile {Ignored = new List {"one", "two", "three"}}; - var sut = new SonarrReleaseProfileCompatibilityHandler(compat, ctx.Mapper); + var result = await sut.CompatibleReleaseProfileForSendingAsync(data); - var result = await sut.CompatibleReleaseProfileForSendingAsync(data); + result.Should().BeEquivalentTo(new SonarrReleaseProfileV1 {Ignored = "one,two,three"}); + } - result.Should().BeEquivalentTo(new SonarrReleaseProfileV1 {Ignored = "one,two,three"}); - } + [Test] + public async Task Send_v2_to_v2() + { + using var ctx = new TestContext(); - [Test] - public async Task Send_v2_to_v2() + var compat = Substitute.For(); + compat.Capabilities.Returns(new[] { - using var ctx = new TestContext(); + new SonarrCapabilities {ArraysNeededForReleaseProfileRequiredAndIgnored = true} + }.ToObservable()); - var compat = Substitute.For(); - compat.Capabilities.Returns(new[] - { - new SonarrCapabilities {ArraysNeededForReleaseProfileRequiredAndIgnored = true} - }.ToObservable()); - - var data = new SonarrReleaseProfile {Ignored = new List {"one", "two", "three"}}; - var sut = new SonarrReleaseProfileCompatibilityHandler(compat, ctx.Mapper); + var data = new SonarrReleaseProfile {Ignored = new List {"one", "two", "three"}}; + var sut = new SonarrReleaseProfileCompatibilityHandler(compat, ctx.Mapper); - var result = await sut.CompatibleReleaseProfileForSendingAsync(data); + var result = await sut.CompatibleReleaseProfileForSendingAsync(data); - result.Should().BeEquivalentTo(data); - } + result.Should().BeEquivalentTo(data); } } diff --git a/src/TrashLib.Tests/Sonarr/QualityDefinition/SonarrQualityDataTest.cs b/src/TrashLib.Tests/Sonarr/QualityDefinition/SonarrQualityDataTest.cs index 5fccfe92..bde0b9c4 100644 --- a/src/TrashLib.Tests/Sonarr/QualityDefinition/SonarrQualityDataTest.cs +++ b/src/TrashLib.Tests/Sonarr/QualityDefinition/SonarrQualityDataTest.cs @@ -2,101 +2,100 @@ using FluentAssertions; using NUnit.Framework; using TrashLib.Sonarr.QualityDefinition; -namespace TrashLib.Tests.Sonarr.QualityDefinition +namespace TrashLib.Tests.Sonarr.QualityDefinition; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class SonarrQualityDataTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class SonarrQualityDataTest + private static readonly object[] MaxTestValues = { - private static readonly object[] MaxTestValues = - { - new object?[] {100m, 100m, false}, - new object?[] {100m, 101m, true}, - new object?[] {100m, 98m, true}, - new object?[] {100m, null, true}, - new object?[] {SonarrQualityData.MaxUnlimitedThreshold, null, false}, - new object?[] {SonarrQualityData.MaxUnlimitedThreshold - 1, null, true}, - new object?[] {SonarrQualityData.MaxUnlimitedThreshold, SonarrQualityData.MaxUnlimitedThreshold, true} - }; + new object?[] {100m, 100m, false}, + new object?[] {100m, 101m, true}, + new object?[] {100m, 98m, true}, + new object?[] {100m, null, true}, + new object?[] {SonarrQualityData.MaxUnlimitedThreshold, null, false}, + new object?[] {SonarrQualityData.MaxUnlimitedThreshold - 1, null, true}, + new object?[] {SonarrQualityData.MaxUnlimitedThreshold, SonarrQualityData.MaxUnlimitedThreshold, true} + }; - private static readonly object[] MinTestValues = - { - new object?[] {0m, 0m, false}, - new object?[] {0m, -1m, true}, - new object?[] {0m, 1m, true} - }; + private static readonly object[] MinTestValues = + { + new object?[] {0m, 0m, false}, + new object?[] {0m, -1m, true}, + new object?[] {0m, 1m, true} + }; - [TestCaseSource(nameof(MaxTestValues))] - public void MaxDifferent_WithVariousValues_ReturnsExpectedResult(decimal guideValue, decimal? radarrValue, - bool isDifferent) - { - var data = new SonarrQualityData {Max = guideValue}; - data.IsMaxDifferent(radarrValue) - .Should().Be(isDifferent); - } + [TestCaseSource(nameof(MaxTestValues))] + public void MaxDifferent_WithVariousValues_ReturnsExpectedResult(decimal guideValue, decimal? radarrValue, + bool isDifferent) + { + var data = new SonarrQualityData {Max = guideValue}; + data.IsMaxDifferent(radarrValue) + .Should().Be(isDifferent); + } - [TestCaseSource(nameof(MinTestValues))] - public void MinDifferent_WithVariousValues_ReturnsExpectedResult(decimal guideValue, decimal radarrValue, - bool isDifferent) - { - var data = new SonarrQualityData {Min = guideValue}; - data.IsMinDifferent(radarrValue) - .Should().Be(isDifferent); - } + [TestCaseSource(nameof(MinTestValues))] + public void MinDifferent_WithVariousValues_ReturnsExpectedResult(decimal guideValue, decimal radarrValue, + bool isDifferent) + { + var data = new SonarrQualityData {Min = guideValue}; + data.IsMinDifferent(radarrValue) + .Should().Be(isDifferent); + } - [Test] - public void AnnotatedMax_OutsideThreshold_EqualsSameValueWithUnlimited() - { - const decimal testVal = SonarrQualityData.MaxUnlimitedThreshold; - var data = new SonarrQualityData {Max = testVal}; - data.AnnotatedMax.Should().Be($"{testVal} (Unlimited)"); - } + [Test] + public void AnnotatedMax_OutsideThreshold_EqualsSameValueWithUnlimited() + { + const decimal testVal = SonarrQualityData.MaxUnlimitedThreshold; + var data = new SonarrQualityData {Max = testVal}; + data.AnnotatedMax.Should().Be($"{testVal} (Unlimited)"); + } - [Test] - public void AnnotatedMax_WithinThreshold_EqualsSameStringValue() - { - const decimal testVal = SonarrQualityData.MaxUnlimitedThreshold - 1; - var data = new SonarrQualityData {Max = testVal}; - data.AnnotatedMax.Should().Be($"{testVal}"); - } + [Test] + public void AnnotatedMax_WithinThreshold_EqualsSameStringValue() + { + const decimal testVal = SonarrQualityData.MaxUnlimitedThreshold - 1; + var data = new SonarrQualityData {Max = testVal}; + data.AnnotatedMax.Should().Be($"{testVal}"); + } - [Test] - public void AnnotatedMin_NoThreshold_EqualsSameValue() - { - const decimal testVal = 10m; - var data = new SonarrQualityData {Max = testVal}; - data.AnnotatedMax.Should().Be($"{testVal}"); - } + [Test] + public void AnnotatedMin_NoThreshold_EqualsSameValue() + { + const decimal testVal = 10m; + var data = new SonarrQualityData {Max = testVal}; + data.AnnotatedMax.Should().Be($"{testVal}"); + } - [Test] - public void Max_AboveThreshold_EqualsSameValue() - { - const decimal testVal = SonarrQualityData.MaxUnlimitedThreshold + 1; - var data = new SonarrQualityData {Max = testVal}; - data.Max.Should().Be(testVal); - } + [Test] + public void Max_AboveThreshold_EqualsSameValue() + { + const decimal testVal = SonarrQualityData.MaxUnlimitedThreshold + 1; + var data = new SonarrQualityData {Max = testVal}; + data.Max.Should().Be(testVal); + } - [Test] - public void MaxForApi_AboveThreshold_EqualsNull() - { - const decimal testVal = SonarrQualityData.MaxUnlimitedThreshold + 1; - var data = new SonarrQualityData {Max = testVal}; - data.MaxForApi.Should().Be(null); - } + [Test] + public void MaxForApi_AboveThreshold_EqualsNull() + { + const decimal testVal = SonarrQualityData.MaxUnlimitedThreshold + 1; + var data = new SonarrQualityData {Max = testVal}; + data.MaxForApi.Should().Be(null); + } - [Test] - public void MaxForApi_HighestWithinThreshold_EqualsSameValue() - { - const decimal testVal = SonarrQualityData.MaxUnlimitedThreshold - 0.1m; - var data = new SonarrQualityData {Max = testVal}; - data.MaxForApi.Should().Be(testVal).And.Be(data.Max); - } + [Test] + public void MaxForApi_HighestWithinThreshold_EqualsSameValue() + { + const decimal testVal = SonarrQualityData.MaxUnlimitedThreshold - 0.1m; + var data = new SonarrQualityData {Max = testVal}; + data.MaxForApi.Should().Be(testVal).And.Be(data.Max); + } - [Test] - public void MaxForApi_LowestWithinThreshold_EqualsSameValue() - { - var data = new SonarrQualityData {Max = 0}; - data.MaxForApi.Should().Be(0); - } + [Test] + public void MaxForApi_LowestWithinThreshold_EqualsSameValue() + { + var data = new SonarrQualityData {Max = 0}; + data.MaxForApi.Should().Be(0); } } diff --git a/src/TrashLib.Tests/Sonarr/ReleaseProfile/FilteredProfileDataTest.cs b/src/TrashLib.Tests/Sonarr/ReleaseProfile/FilteredProfileDataTest.cs index 7e584b57..0ddff01e 100644 --- a/src/TrashLib.Tests/Sonarr/ReleaseProfile/FilteredProfileDataTest.cs +++ b/src/TrashLib.Tests/Sonarr/ReleaseProfile/FilteredProfileDataTest.cs @@ -4,89 +4,88 @@ using NUnit.Framework; using TrashLib.Sonarr.Config; using TrashLib.Sonarr.ReleaseProfile; -namespace TrashLib.Tests.Sonarr.ReleaseProfile +namespace TrashLib.Tests.Sonarr.ReleaseProfile; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class FilteredProfileDataTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class FilteredProfileDataTest + [Test] + public void Filter_ExcludeOptional_HasNoOptionalItems() { - [Test] - public void Filter_ExcludeOptional_HasNoOptionalItems() - { - var config = new ReleaseProfileConfig(); - config.Filter.IncludeOptional = false; + var config = new ReleaseProfileConfig(); + config.Filter.IncludeOptional = false; - var profileData = new ProfileData + var profileData = new ProfileData + { + Ignored = new List {"ignored1"}, + Required = new List {"required1"}, + Preferred = new Dictionary> { - Ignored = new List {"ignored1"}, - Required = new List {"required1"}, + {100, new List {"preferred1"}} + }, + Optional = new ProfileDataOptional + { + Ignored = new List {"ignored2"}, + Required = new List {"required2"}, Preferred = new Dictionary> { - {100, new List {"preferred1"}} - }, - Optional = new ProfileDataOptional - { - Ignored = new List {"ignored2"}, - Required = new List {"required2"}, - Preferred = new Dictionary> - { - {200, new List {"preferred2"}}, - {100, new List {"preferred3"}} - } + {200, new List {"preferred2"}}, + {100, new List {"preferred3"}} } - }; + } + }; - var filtered = new FilteredProfileData(profileData, config); + var filtered = new FilteredProfileData(profileData, config); - filtered.Should().BeEquivalentTo(new + filtered.Should().BeEquivalentTo(new + { + Ignored = new List {"ignored1"}, + Required = new List {"required1"}, + Preferred = new Dictionary> { - Ignored = new List {"ignored1"}, - Required = new List {"required1"}, - Preferred = new Dictionary> - { - {100, new List {"preferred1"}} - } - }); - } + {100, new List {"preferred1"}} + } + }); + } - [Test] - public void Filter_IncludeOptional_HasAllOptionalItems() - { - var config = new ReleaseProfileConfig(); - config.Filter.IncludeOptional = true; + [Test] + public void Filter_IncludeOptional_HasAllOptionalItems() + { + var config = new ReleaseProfileConfig(); + config.Filter.IncludeOptional = true; - var profileData = new ProfileData + var profileData = new ProfileData + { + Ignored = new List {"ignored1"}, + Required = new List {"required1"}, + Preferred = new Dictionary> { - Ignored = new List {"ignored1"}, - Required = new List {"required1"}, + {100, new List {"preferred1"}} + }, + Optional = new ProfileDataOptional + { + Ignored = new List {"ignored2"}, + Required = new List {"required2"}, Preferred = new Dictionary> { - {100, new List {"preferred1"}} - }, - Optional = new ProfileDataOptional - { - Ignored = new List {"ignored2"}, - Required = new List {"required2"}, - Preferred = new Dictionary> - { - {200, new List {"preferred2"}}, - {100, new List {"preferred3"}} - } + {200, new List {"preferred2"}}, + {100, new List {"preferred3"}} } - }; + } + }; - var filtered = new FilteredProfileData(profileData, config); + var filtered = new FilteredProfileData(profileData, config); - filtered.Should().BeEquivalentTo(new + filtered.Should().BeEquivalentTo(new + { + Ignored = new List {"ignored1", "ignored2"}, + Required = new List {"required1", "required2"}, + Preferred = new Dictionary> { - Ignored = new List {"ignored1", "ignored2"}, - Required = new List {"required1", "required2"}, - Preferred = new Dictionary> - { - {100, new List {"preferred1", "preferred3"}}, - {200, new List {"preferred2"}} - } - }); - } + {100, new List {"preferred1", "preferred3"}}, + {200, new List {"preferred2"}} + } + }); } } diff --git a/src/TrashLib.Tests/Sonarr/ReleaseProfile/ReleaseProfileParserTest.cs b/src/TrashLib.Tests/Sonarr/ReleaseProfile/ReleaseProfileParserTest.cs index 29c2cd20..8466334f 100644 --- a/src/TrashLib.Tests/Sonarr/ReleaseProfile/ReleaseProfileParserTest.cs +++ b/src/TrashLib.Tests/Sonarr/ReleaseProfile/ReleaseProfileParserTest.cs @@ -9,49 +9,49 @@ using TestLibrary; using TrashLib.Sonarr.Config; using TrashLib.Sonarr.ReleaseProfile; -namespace TrashLib.Tests.Sonarr.ReleaseProfile +namespace TrashLib.Tests.Sonarr.ReleaseProfile; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class ReleaseProfileParserTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class ReleaseProfileParserTest + [OneTimeSetUp] + public void Setup() { - [OneTimeSetUp] - public void Setup() - { - // Formatter.AddFormatter(new ProfileDataValueFormatter()); - } + // Formatter.AddFormatter(new ProfileDataValueFormatter()); + } - private class Context + private class Context + { + public Context() { - public Context() - { - var logger = new LoggerConfiguration() - .WriteTo.TestCorrelator() - .MinimumLevel.Debug() - .CreateLogger(); + var logger = new LoggerConfiguration() + .WriteTo.TestCorrelator() + .MinimumLevel.Debug() + .CreateLogger(); - Config = new SonarrConfiguration - { - ReleaseProfiles = new[] {new ReleaseProfileConfig()} - }; + Config = new SonarrConfiguration + { + ReleaseProfiles = new[] {new ReleaseProfileConfig()} + }; - GuideParser = new ReleaseProfileGuideParser(logger); - } + GuideParser = new ReleaseProfileGuideParser(logger); + } - public SonarrConfiguration Config { get; } - public ReleaseProfileGuideParser GuideParser { get; } - public ResourceDataReader TestData { get; } = new(typeof(ReleaseProfileParserTest), "Data"); + public SonarrConfiguration Config { get; } + public ReleaseProfileGuideParser GuideParser { get; } + public ResourceDataReader TestData { get; } = new(typeof(ReleaseProfileParserTest), "Data"); - public IDictionary ParseWithDefaults(string markdown) - { - return GuideParser.ParseMarkdown(Config.ReleaseProfiles.First(), markdown); - } + public IDictionary ParseWithDefaults(string markdown) + { + return GuideParser.ParseMarkdown(Config.ReleaseProfiles.First(), markdown); } + } - [Test] - public void Parse_CodeBlockScopedCategories_CategoriesSwitch() - { - var markdown = StringUtils.TrimmedString(@" + [Test] + public void Parse_CodeBlockScopedCategories_CategoriesSwitch() + { + var markdown = StringUtils.TrimmedString(@" # Test Release Profile Add this to must not contain (ignored) @@ -66,21 +66,21 @@ Add this to must contain (required) xyz ``` "); - var context = new Context(); - var results = context.ParseWithDefaults(markdown); + var context = new Context(); + var results = context.ParseWithDefaults(markdown); - results.Should().ContainKey("Test Release Profile") - .WhoseValue.Should().BeEquivalentTo(new - { - Ignored = new List {"abc"}, - Required = new List {"xyz"} - }); - } + results.Should().ContainKey("Test Release Profile") + .WhoseValue.Should().BeEquivalentTo(new + { + Ignored = new List {"abc"}, + Required = new List {"xyz"} + }); + } - [Test] - public void Parse_HeaderCategoryFollowedByCodeBlockCategories_CodeBlockChangesCurrentCategory() - { - var markdown = StringUtils.TrimmedString(@" + [Test] + public void Parse_HeaderCategoryFollowedByCodeBlockCategories_CodeBlockChangesCurrentCategory() + { + var markdown = StringUtils.TrimmedString(@" # Test Release Profile ## Must Not Contain @@ -103,52 +103,52 @@ One more 123 ``` "); - var context = new Context(); - var results = context.ParseWithDefaults(markdown); + var context = new Context(); + var results = context.ParseWithDefaults(markdown); - results.Should().ContainKey("Test Release Profile") - .WhoseValue.Should().BeEquivalentTo(new - { - Ignored = new List {"abc"}, - Required = new List {"xyz", "123"} - }); - } + results.Should().ContainKey("Test Release Profile") + .WhoseValue.Should().BeEquivalentTo(new + { + Ignored = new List {"abc"}, + Required = new List {"xyz", "123"} + }); + } - [Test] - public void Parse_IgnoredRequiredPreferredScores() - { - var context = new Context(); - var markdown = context.TestData.ReadData("test_parse_markdown_complete_doc.md"); - var results = context.GuideParser.ParseMarkdown(context.Config.ReleaseProfiles.First(), markdown); + [Test] + public void Parse_IgnoredRequiredPreferredScores() + { + var context = new Context(); + var markdown = context.TestData.ReadData("test_parse_markdown_complete_doc.md"); + var results = context.GuideParser.ParseMarkdown(context.Config.ReleaseProfiles.First(), markdown); - results.Count.Should().Be(1); + results.Count.Should().Be(1); - var profile = results.First().Value; + var profile = results.First().Value; - profile.Ignored.Should().BeEquivalentTo("term2", "term3"); - profile.Required.Should().BeEquivalentTo("term4"); - profile.Preferred.Should().ContainKey(100).WhoseValue.Should().BeEquivalentTo(new List {"term1"}); - } + profile.Ignored.Should().BeEquivalentTo("term2", "term3"); + profile.Required.Should().BeEquivalentTo("term4"); + profile.Preferred.Should().ContainKey(100).WhoseValue.Should().BeEquivalentTo(new List {"term1"}); + } - [Test] - public void Parse_IncludePreferredWhenRenaming() - { - var context = new Context(); - var markdown = context.TestData.ReadData("include_preferred_when_renaming.md"); - var results = context.ParseWithDefaults(markdown); - - results.Should() - .ContainKey("First Release Profile") - .WhoseValue.IncludePreferredWhenRenaming.Should().Be(true); - results.Should() - .ContainKey("Second Release Profile") - .WhoseValue.IncludePreferredWhenRenaming.Should().Be(false); - } + [Test] + public void Parse_IncludePreferredWhenRenaming() + { + var context = new Context(); + var markdown = context.TestData.ReadData("include_preferred_when_renaming.md"); + var results = context.ParseWithDefaults(markdown); + + results.Should() + .ContainKey("First Release Profile") + .WhoseValue.IncludePreferredWhenRenaming.Should().Be(true); + results.Should() + .ContainKey("Second Release Profile") + .WhoseValue.IncludePreferredWhenRenaming.Should().Be(false); + } - [Test] - public void Parse_IndentedIncludePreferred_ShouldBeParsed() - { - var markdown = StringUtils.TrimmedString(@" + [Test] + public void Parse_IndentedIncludePreferred_ShouldBeParsed() + { + var markdown = StringUtils.TrimmedString(@" # Release Profile 1 !!! Warning @@ -171,34 +171,34 @@ must contain test2 ``` "); - var context = new Context(); - var results = context.ParseWithDefaults(markdown); + var context = new Context(); + var results = context.ParseWithDefaults(markdown); - var expectedResults = new Dictionary + var expectedResults = new Dictionary + { { + "Release Profile 1", new ProfileData { - "Release Profile 1", new ProfileData - { - IncludePreferredWhenRenaming = false, - Required = new List {"test1"} - } - }, + IncludePreferredWhenRenaming = false, + Required = new List {"test1"} + } + }, + { + "Release Profile 2", new ProfileData { - "Release Profile 2", new ProfileData - { - IncludePreferredWhenRenaming = true, - Required = new List {"test2"} - } + IncludePreferredWhenRenaming = true, + Required = new List {"test2"} } - }; + } + }; - results.Should().BeEquivalentTo(expectedResults); - } + results.Should().BeEquivalentTo(expectedResults); + } - [Test] - public void Parse_OptionalTerms_AreCapturedProperly() - { - var markdown = StringUtils.TrimmedString(@" + [Test] + public void Parse_OptionalTerms_AreCapturedProperly() + { + var markdown = StringUtils.TrimmedString(@" # Optional Release Profile ``` @@ -233,40 +233,40 @@ This must not contain: not-optional1 ``` "); - var context = new Context(); - var results = context.ParseWithDefaults(markdown); + var context = new Context(); + var results = context.ParseWithDefaults(markdown); - var expectedResults = new Dictionary + var expectedResults = new Dictionary + { { + "Optional Release Profile", new ProfileData { - "Optional Release Profile", new ProfileData + Optional = new ProfileDataOptional { - Optional = new ProfileDataOptional + Ignored = new List {"optional1"}, + Required = new List {"optional3"}, + Preferred = new Dictionary> { - Ignored = new List {"optional1"}, - Required = new List {"optional3"}, - Preferred = new Dictionary> - { - {10, new List {"optional2"}} - } + {10, new List {"optional2"}} } } - }, + } + }, + { + "Second Release Profile", new ProfileData { - "Second Release Profile", new ProfileData - { - Ignored = new List {"not-optional1"} - } + Ignored = new List {"not-optional1"} } - }; + } + }; - results.Should().BeEquivalentTo(expectedResults); - } + results.Should().BeEquivalentTo(expectedResults); + } - [Test] - public void Parse_PotentialScore_WarningLogged() - { - string markdown = StringUtils.TrimmedString(@" + [Test] + public void Parse_PotentialScore_WarningLogged() + { + var markdown = StringUtils.TrimmedString(@" # First Release Profile The below line should be a score but isn't because it's missing the word 'score'. @@ -277,23 +277,23 @@ Use this number [100] abc ``` "); - var context = new Context(); - var results = context.ParseWithDefaults(markdown); + var context = new Context(); + var results = context.ParseWithDefaults(markdown); - results.Should().BeEmpty(); + results.Should().BeEmpty(); - const string expectedLog = - "Found a potential score on line #5 that will be ignored because the " + - "word 'score' is missing (This is probably a bug in the guide itself): \"[100]\""; + const string expectedLog = + "Found a potential score on line #5 that will be ignored because the " + + "word 'score' is missing (This is probably a bug in the guide itself): \"[100]\""; - TestCorrelator.GetLogEventsFromCurrentContext() - .Should().ContainSingle(evt => evt.RenderMessage(default) == expectedLog); - } + TestCorrelator.GetLogEventsFromCurrentContext() + .Should().ContainSingle(evt => evt.RenderMessage(default) == expectedLog); + } - [Test] - public void Parse_ScoreWithoutCategory_ImplicitlyPreferred() - { - var markdown = StringUtils.TrimmedString(@" + [Test] + public void Parse_ScoreWithoutCategory_ImplicitlyPreferred() + { + var markdown = StringUtils.TrimmedString(@" # Test Release Profile score is [100] @@ -302,73 +302,73 @@ score is [100] abc ``` "); - var context = new Context(); - var results = context.ParseWithDefaults(markdown); + var context = new Context(); + var results = context.ParseWithDefaults(markdown); - results.Should() - .ContainKey("Test Release Profile") - .WhoseValue.Preferred.Should() - .BeEquivalentTo(new Dictionary> - { - {100, new List {"abc"}} - }); - } + results.Should() + .ContainKey("Test Release Profile") + .WhoseValue.Preferred.Should() + .BeEquivalentTo(new Dictionary> + { + {100, new List {"abc"}} + }); + } - [Test] - public void Parse_SkippableLines_AreSkippedWithLog() - { - var markdown = StringUtils.TrimmedString(@" + [Test] + public void Parse_SkippableLines_AreSkippedWithLog() + { + var markdown = StringUtils.TrimmedString(@" # First Release Profile !!! Admonition lines are skipped Indented lines are skipped "); - // List of substrings of logs that should appear in the resulting list of logs after parsing is done. - // We are only looking for logs relevant to the skipped lines we're testing for. - var expectedLogs = new List - { - "Skip Admonition", - "Skip Indented Line" - }; + // List of substrings of logs that should appear in the resulting list of logs after parsing is done. + // We are only looking for logs relevant to the skipped lines we're testing for. + var expectedLogs = new List + { + "Skip Admonition", + "Skip Indented Line" + }; - var context = new Context(); - var results = context.ParseWithDefaults(markdown); + var context = new Context(); + var results = context.ParseWithDefaults(markdown); - results.Should().BeEmpty(); + results.Should().BeEmpty(); - var ctx = TestCorrelator.GetLogEventsFromCurrentContext().ToList(); - foreach (var log in expectedLogs) - { - ctx.Should().Contain(evt => evt.MessageTemplate.Text.Contains(log)); - } + var ctx = TestCorrelator.GetLogEventsFromCurrentContext().ToList(); + foreach (var log in expectedLogs) + { + ctx.Should().Contain(evt => evt.MessageTemplate.Text.Contains(log)); } + } - [Test] - public void Parse_StrictNegativeScores() + [Test] + public void Parse_StrictNegativeScores() + { + var context = new Context(); + context.Config.ReleaseProfiles = new List { - var context = new Context(); - context.Config.ReleaseProfiles = new List - { - new() {StrictNegativeScores = true} - }; + new() {StrictNegativeScores = true} + }; - var markdown = context.TestData.ReadData("strict_negative_scores.md"); - var results = context.ParseWithDefaults(markdown); + var markdown = context.TestData.ReadData("strict_negative_scores.md"); + var results = context.ParseWithDefaults(markdown); - results.Should() - .ContainKey("Test Release Profile").WhoseValue.Should() - .BeEquivalentTo(new - { - Required = new { }, - Ignored = new List {"abc"}, - Preferred = new Dictionary> {{0, new List {"xyz"}}} - }); - } + results.Should() + .ContainKey("Test Release Profile").WhoseValue.Should() + .BeEquivalentTo(new + { + Required = new { }, + Ignored = new List {"abc"}, + Preferred = new Dictionary> {{0, new List {"xyz"}}} + }); + } - [Test] - public void Parse_TermsWithoutCategory_AreSkipped() - { - var markdown = StringUtils.TrimmedString(@" + [Test] + public void Parse_TermsWithoutCategory_AreSkipped() + { + var markdown = StringUtils.TrimmedString(@" # Test Release Profile ``` @@ -401,24 +401,23 @@ added3 skipped2 ``` "); - var context = new Context(); - var results = context.ParseWithDefaults(markdown); + var context = new Context(); + var results = context.ParseWithDefaults(markdown); - var expectedResults = new Dictionary + var expectedResults = new Dictionary + { { + "Test Release Profile", new ProfileData { - "Test Release Profile", new ProfileData + Ignored = new List {"added1"}, + Preferred = new Dictionary> { - Ignored = new List {"added1"}, - Preferred = new Dictionary> - { - {10, new List {"added2", "added3"}} - } + {10, new List {"added2", "added3"}} } } - }; + } + }; - results.Should().BeEquivalentTo(expectedResults); - } + results.Should().BeEquivalentTo(expectedResults); } } diff --git a/src/TrashLib.Tests/Sonarr/ReleaseProfile/ScopedStateTest.cs b/src/TrashLib.Tests/Sonarr/ReleaseProfile/ScopedStateTest.cs index 775b5c2d..bef73ec2 100644 --- a/src/TrashLib.Tests/Sonarr/ReleaseProfile/ScopedStateTest.cs +++ b/src/TrashLib.Tests/Sonarr/ReleaseProfile/ScopedStateTest.cs @@ -2,130 +2,129 @@ using NUnit.Framework; using TrashLib.Sonarr.ReleaseProfile; -namespace TrashLib.Tests.Sonarr.ReleaseProfile +namespace TrashLib.Tests.Sonarr.ReleaseProfile; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class ScopedStateTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class ScopedStateTest + [Test] + public void AccessValue_MultipleScopes_ScopeValuesReturned() + { + var state = new ScopedState(50); + state.PushValue(100, 0); + state.PushValue(150, 1); + + state.StackSize.Should().Be(2); + state.ActiveScope.Should().Be(1); + state.Value.Should().Be(150); + + state.Reset(1).Should().BeTrue(); + + state.StackSize.Should().Be(1); + state.ActiveScope.Should().Be(0); + state.Value.Should().Be(100); + + state.Reset(0).Should().BeTrue(); + + state.StackSize.Should().Be(0); + state.ActiveScope.Should().BeNull(); + state.Value.Should().Be(50); + } + + [Test] + public void AccessValue_NextBlockScope_ReturnValueUntilSecondSession() + { + var state = new ScopedState(50); + state.PushValue(100, 0); + + state.ActiveScope.Should().Be(0); + state.Value.Should().Be(100); + + state.Reset(0).Should().BeTrue(); + + state.ActiveScope.Should().BeNull(); + state.Value.Should().Be(50); + } + + [Test] + public void AccessValue_NoScope_ReturnDefaultValue() + { + var state = new ScopedState(50); + state.ActiveScope.Should().BeNull(); + state.Value.Should().Be(50); + } + + [Test] + public void AccessValue_ResetAfterScope_ReturnDefault() + { + var state = new ScopedState(50); + state.PushValue(100, 1); + + state.Reset(1).Should().BeTrue(); + + state.ActiveScope.Should().BeNull(); + state.Value.Should().Be(50); + } + + [Test] + public void AccessValue_WholeSectionScope_ReturnValueAcrossMultipleResets() { - [Test] - public void AccessValue_MultipleScopes_ScopeValuesReturned() - { - var state = new ScopedState(50); - state.PushValue(100, 0); - state.PushValue(150, 1); - - state.StackSize.Should().Be(2); - state.ActiveScope.Should().Be(1); - state.Value.Should().Be(150); - - state.Reset(1).Should().BeTrue(); - - state.StackSize.Should().Be(1); - state.ActiveScope.Should().Be(0); - state.Value.Should().Be(100); - - state.Reset(0).Should().BeTrue(); - - state.StackSize.Should().Be(0); - state.ActiveScope.Should().BeNull(); - state.Value.Should().Be(50); - } - - [Test] - public void AccessValue_NextBlockScope_ReturnValueUntilSecondSession() - { - var state = new ScopedState(50); - state.PushValue(100, 0); - - state.ActiveScope.Should().Be(0); - state.Value.Should().Be(100); - - state.Reset(0).Should().BeTrue(); - - state.ActiveScope.Should().BeNull(); - state.Value.Should().Be(50); - } - - [Test] - public void AccessValue_NoScope_ReturnDefaultValue() - { - var state = new ScopedState(50); - state.ActiveScope.Should().BeNull(); - state.Value.Should().Be(50); - } - - [Test] - public void AccessValue_ResetAfterScope_ReturnDefault() - { - var state = new ScopedState(50); - state.PushValue(100, 1); - - state.Reset(1).Should().BeTrue(); - - state.ActiveScope.Should().BeNull(); - state.Value.Should().Be(50); - } - - [Test] - public void AccessValue_WholeSectionScope_ReturnValueAcrossMultipleResets() - { - var state = new ScopedState(50); - state.PushValue(100, 1); - - state.ActiveScope.Should().Be(1); - state.Value.Should().Be(100); - - state.Reset(2).Should().BeFalse(); - - state.ActiveScope.Should().Be(1); - state.Value.Should().Be(100); - } - - [Test] - public void Reset_UsingGreatestScopeWithTwoScopes_ShouldRemoveAllScope() - { - var state = new ScopedState(50); - state.PushValue(100, 1); - state.PushValue(150, 0); - state.Reset(1).Should().BeTrue(); - - state.ActiveScope.Should().BeNull(); - state.Value.Should().Be(50); - } - - [Test] - public void Reset_UsingLesserScopeWithTwoScopes_ShouldRemoveTopScope() - { - var state = new ScopedState(50); - state.PushValue(100, 0); - state.PushValue(150, 1); - state.Reset(1).Should().BeTrue(); - - state.ActiveScope.Should().Be(0); - state.Value.Should().Be(100); - } - - [Test] - public void Reset_WithLesserScope_ShouldDoNothing() - { - var state = new ScopedState(50); - state.PushValue(100, 1); - state.Reset(2).Should().BeFalse(); - - state.ActiveScope.Should().Be(1); - state.Value.Should().Be(100); - } - - [Test] - public void Reset_WithScope_ShouldReset() - { - var state = new ScopedState(50); - state.PushValue(100, 1); - state.Reset(1).Should().BeTrue(); - - state.ActiveScope.Should().BeNull(); - state.Value.Should().Be(50); - } + var state = new ScopedState(50); + state.PushValue(100, 1); + + state.ActiveScope.Should().Be(1); + state.Value.Should().Be(100); + + state.Reset(2).Should().BeFalse(); + + state.ActiveScope.Should().Be(1); + state.Value.Should().Be(100); + } + + [Test] + public void Reset_UsingGreatestScopeWithTwoScopes_ShouldRemoveAllScope() + { + var state = new ScopedState(50); + state.PushValue(100, 1); + state.PushValue(150, 0); + state.Reset(1).Should().BeTrue(); + + state.ActiveScope.Should().BeNull(); + state.Value.Should().Be(50); + } + + [Test] + public void Reset_UsingLesserScopeWithTwoScopes_ShouldRemoveTopScope() + { + var state = new ScopedState(50); + state.PushValue(100, 0); + state.PushValue(150, 1); + state.Reset(1).Should().BeTrue(); + + state.ActiveScope.Should().Be(0); + state.Value.Should().Be(100); + } + + [Test] + public void Reset_WithLesserScope_ShouldDoNothing() + { + var state = new ScopedState(50); + state.PushValue(100, 1); + state.Reset(2).Should().BeFalse(); + + state.ActiveScope.Should().Be(1); + state.Value.Should().Be(100); + } + + [Test] + public void Reset_WithScope_ShouldReset() + { + var state = new ScopedState(50); + state.PushValue(100, 1); + state.Reset(1).Should().BeTrue(); + + state.ActiveScope.Should().BeNull(); + state.Value.Should().Be(50); } } diff --git a/src/TrashLib.Tests/Sonarr/ReleaseProfileUpdaterTest.cs b/src/TrashLib.Tests/Sonarr/ReleaseProfileUpdaterTest.cs index 54255f65..23cd2564 100644 --- a/src/TrashLib.Tests/Sonarr/ReleaseProfileUpdaterTest.cs +++ b/src/TrashLib.Tests/Sonarr/ReleaseProfileUpdaterTest.cs @@ -6,46 +6,45 @@ using TrashLib.Sonarr.Api; using TrashLib.Sonarr.Config; using TrashLib.Sonarr.ReleaseProfile; -namespace TrashLib.Tests.Sonarr +namespace TrashLib.Tests.Sonarr; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class ReleaseProfileUpdaterTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class ReleaseProfileUpdaterTest + private class Context { - private class Context - { - public IReleaseProfileGuideParser Parser { get; } = Substitute.For(); - public ISonarrApi Api { get; } = Substitute.For(); - public ILogger Logger { get; } = Substitute.For(); - public ISonarrCompatibility Compatibility { get; } = Substitute.For(); - } - - [Test] - public void ProcessReleaseProfile_InvalidReleaseProfiles_NoCrashNoCalls() - { - var context = new Context(); + public IReleaseProfileGuideParser Parser { get; } = Substitute.For(); + public ISonarrApi Api { get; } = Substitute.For(); + public ILogger Logger { get; } = Substitute.For(); + public ISonarrCompatibility Compatibility { get; } = Substitute.For(); + } + + [Test] + public void ProcessReleaseProfile_InvalidReleaseProfiles_NoCrashNoCalls() + { + var context = new Context(); - var logic = new ReleaseProfileUpdater(context.Logger, context.Parser, context.Api, context.Compatibility); - logic.Process(false, new SonarrConfiguration()); + var logic = new ReleaseProfileUpdater(context.Logger, context.Parser, context.Api, context.Compatibility); + logic.Process(false, new SonarrConfiguration()); - context.Parser.DidNotReceive().GetMarkdownData(Arg.Any()); - } + context.Parser.DidNotReceive().GetMarkdownData(Arg.Any()); + } - [Test] - public void ProcessReleaseProfile_SingleProfilePreview() - { - var context = new Context(); + [Test] + public void ProcessReleaseProfile_SingleProfilePreview() + { + var context = new Context(); - context.Parser.GetMarkdownData(ReleaseProfileType.Anime).Returns("theMarkdown"); - var config = new SonarrConfiguration - { - ReleaseProfiles = new[] {new ReleaseProfileConfig {Type = ReleaseProfileType.Anime}} - }; + context.Parser.GetMarkdownData(ReleaseProfileType.Anime).Returns("theMarkdown"); + var config = new SonarrConfiguration + { + ReleaseProfiles = new[] {new ReleaseProfileConfig {Type = ReleaseProfileType.Anime}} + }; - var logic = new ReleaseProfileUpdater(context.Logger, context.Parser, context.Api, context.Compatibility); - logic.Process(false, config); + var logic = new ReleaseProfileUpdater(context.Logger, context.Parser, context.Api, context.Compatibility); + logic.Process(false, config); - context.Parser.Received().ParseMarkdown(config.ReleaseProfiles[0], "theMarkdown"); - } + context.Parser.Received().ParseMarkdown(config.ReleaseProfiles[0], "theMarkdown"); } } diff --git a/src/TrashLib.Tests/Sonarr/SonarrConfigurationTest.cs b/src/TrashLib.Tests/Sonarr/SonarrConfigurationTest.cs index 42de601f..87056b37 100644 --- a/src/TrashLib.Tests/Sonarr/SonarrConfigurationTest.cs +++ b/src/TrashLib.Tests/Sonarr/SonarrConfigurationTest.cs @@ -9,62 +9,61 @@ using TrashLib.Sonarr; using TrashLib.Sonarr.Config; using TrashLib.Sonarr.ReleaseProfile; -namespace TrashLib.Tests.Sonarr +namespace TrashLib.Tests.Sonarr; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class SonarrConfigurationTest { - [TestFixture] - [Parallelizable(ParallelScope.All)] - public class SonarrConfigurationTest - { - private IContainer _container = default!; + private IContainer _container = default!; - [OneTimeSetUp] - public void Setup() - { - var builder = new ContainerBuilder(); - builder.RegisterModule(); - builder.RegisterModule(); - _container = builder.Build(); - } + [OneTimeSetUp] + public void Setup() + { + var builder = new ContainerBuilder(); + builder.RegisterModule(); + builder.RegisterModule(); + _container = builder.Build(); + } - [Test] - public void Validation_fails_for_all_missing_required_properties() - { - // default construct which should yield default values (invalid) for all required properties - var config = new SonarrConfiguration(); - var validator = _container.Resolve>(); + [Test] + public void Validation_fails_for_all_missing_required_properties() + { + // default construct which should yield default values (invalid) for all required properties + var config = new SonarrConfiguration(); + var validator = _container.Resolve>(); - var result = validator.Validate(config); + var result = validator.Validate(config); - var expectedErrorMessageSubstrings = new[] - { - "Property 'base_url' is required", - "Property 'api_key' is required", - "'type' is required for 'release_profiles' elements" - }; + var expectedErrorMessageSubstrings = new[] + { + "Property 'base_url' is required", + "Property 'api_key' is required", + "'type' is required for 'release_profiles' elements" + }; - result.IsValid.Should().BeFalse(); - result.Errors.Select(e => e.ErrorMessage).Should() - .OnlyContain(x => expectedErrorMessageSubstrings.Any(x.Contains)); - } + result.IsValid.Should().BeFalse(); + result.Errors.Select(e => e.ErrorMessage).Should() + .OnlyContain(x => expectedErrorMessageSubstrings.Any(x.Contains)); + } - [Test] - public void Validation_succeeds_when_no_missing_required_properties() + [Test] + public void Validation_succeeds_when_no_missing_required_properties() + { + var config = new SonarrConfiguration { - var config = new SonarrConfiguration + ApiKey = "required value", + BaseUrl = "required value", + ReleaseProfiles = new List { - ApiKey = "required value", - BaseUrl = "required value", - ReleaseProfiles = new List - { - new() {Type = ReleaseProfileType.Anime} - } - }; + new() {Type = ReleaseProfileType.Anime} + } + }; - var validator = _container.Resolve>(); - var result = validator.Validate(config); + var validator = _container.Resolve>(); + var result = validator.Validate(config); - result.IsValid.Should().BeTrue(); - result.Errors.Should().BeEmpty(); - } + result.IsValid.Should().BeTrue(); + result.Errors.Should().BeEmpty(); } } diff --git a/src/TrashLib/Cache/CacheAutofacModule.cs b/src/TrashLib/Cache/CacheAutofacModule.cs index a1e7be28..60202198 100644 --- a/src/TrashLib/Cache/CacheAutofacModule.cs +++ b/src/TrashLib/Cache/CacheAutofacModule.cs @@ -1,13 +1,12 @@ using Autofac; -namespace TrashLib.Cache +namespace TrashLib.Cache; + +public class CacheAutofacModule : Module { - public class CacheAutofacModule : Module + protected override void Load(ContainerBuilder builder) { - protected override void Load(ContainerBuilder builder) - { - // Clients must register their own implementation of ICacheStoragePath - builder.RegisterType().As(); - } + // Clients must register their own implementation of ICacheStoragePath + builder.RegisterType().As(); } } diff --git a/src/TrashLib/Cache/CacheObjectNameAttribute.cs b/src/TrashLib/Cache/CacheObjectNameAttribute.cs index 7d8d9b03..c7e28c99 100644 --- a/src/TrashLib/Cache/CacheObjectNameAttribute.cs +++ b/src/TrashLib/Cache/CacheObjectNameAttribute.cs @@ -1,15 +1,14 @@ using System; -namespace TrashLib.Cache +namespace TrashLib.Cache; + +[AttributeUsage(AttributeTargets.Class)] +internal sealed class CacheObjectNameAttribute : Attribute { - [AttributeUsage(AttributeTargets.Class)] - internal sealed class CacheObjectNameAttribute : Attribute + public CacheObjectNameAttribute(string name) { - public CacheObjectNameAttribute(string name) - { - Name = name; - } - - public string Name { get; } + Name = name; } + + public string Name { get; } } diff --git a/src/TrashLib/Cache/ICacheStoragePath.cs b/src/TrashLib/Cache/ICacheStoragePath.cs index 56d077b2..593be31d 100644 --- a/src/TrashLib/Cache/ICacheStoragePath.cs +++ b/src/TrashLib/Cache/ICacheStoragePath.cs @@ -1,7 +1,6 @@ -namespace TrashLib.Cache +namespace TrashLib.Cache; + +public interface ICacheStoragePath { - public interface ICacheStoragePath - { - string Path { get; } - } + string Path { get; } } diff --git a/src/TrashLib/Cache/IServiceCache.cs b/src/TrashLib/Cache/IServiceCache.cs index fcca0f83..c0a99a85 100644 --- a/src/TrashLib/Cache/IServiceCache.cs +++ b/src/TrashLib/Cache/IServiceCache.cs @@ -1,8 +1,7 @@ -namespace TrashLib.Cache +namespace TrashLib.Cache; + +public interface IServiceCache { - public interface IServiceCache - { - T? Load() where T : class; - void Save(T obj) where T : class; - } + T? Load() where T : class; + void Save(T obj) where T : class; } diff --git a/src/TrashLib/Cache/ServiceCache.cs b/src/TrashLib/Cache/ServiceCache.cs index 1c6b9718..2cd96612 100644 --- a/src/TrashLib/Cache/ServiceCache.cs +++ b/src/TrashLib/Cache/ServiceCache.cs @@ -11,91 +11,90 @@ using Newtonsoft.Json.Serialization; using Serilog; using TrashLib.Config; -namespace TrashLib.Cache +namespace TrashLib.Cache; + +internal class ServiceCache : IServiceCache { - internal class ServiceCache : IServiceCache + private static readonly Regex AllowedObjectNameCharacters = new(@"^[\w-]+$", RegexOptions.Compiled); + private readonly IConfigurationProvider _configProvider; + private readonly IFileSystem _fileSystem; + private readonly IFNV1a _hash; + private readonly ICacheStoragePath _storagePath; + + public ServiceCache(IFileSystem fileSystem, ICacheStoragePath storagePath, + IConfigurationProvider configProvider, + ILogger log) { - private static readonly Regex AllowedObjectNameCharacters = new(@"^[\w-]+$", RegexOptions.Compiled); - private readonly IConfigurationProvider _configProvider; - private readonly IFileSystem _fileSystem; - private readonly IFNV1a _hash; - private readonly ICacheStoragePath _storagePath; + _fileSystem = fileSystem; + _storagePath = storagePath; + _configProvider = configProvider; + Log = log; + _hash = FNV1aFactory.Instance.Create(FNVConfig.GetPredefinedConfig(32)); + } - public ServiceCache(IFileSystem fileSystem, ICacheStoragePath storagePath, - IConfigurationProvider configProvider, - ILogger log) + private ILogger Log { get; } + + public T? Load() where T : class + { + var path = PathFromAttribute(); + if (!_fileSystem.File.Exists(path)) { - _fileSystem = fileSystem; - _storagePath = storagePath; - _configProvider = configProvider; - Log = log; - _hash = FNV1aFactory.Instance.Create(FNVConfig.GetPredefinedConfig(32)); + return null; } - private ILogger Log { get; } + var json = _fileSystem.File.ReadAllText(path); - public T? Load() where T : class + try { - var path = PathFromAttribute(); - if (!_fileSystem.File.Exists(path)) - { - return null; - } - - var json = _fileSystem.File.ReadAllText(path); - - try - { - return JObject.Parse(json).ToObject(); - } - catch (JsonException e) - { - Log.Error("Failed to read cache data, will proceed without cache. Reason: {Msg}", e.Message); - } - - return null; + return JObject.Parse(json).ToObject(); } - - public void Save(T obj) where T : class + catch (JsonException e) { - var path = PathFromAttribute(); - _fileSystem.Directory.CreateDirectory(Path.GetDirectoryName(path)); - _fileSystem.File.WriteAllText(path, JsonConvert.SerializeObject(obj, new JsonSerializerSettings - { - Formatting = Formatting.Indented, - ContractResolver = new DefaultContractResolver - { - NamingStrategy = new SnakeCaseNamingStrategy() - } - })); + Log.Error("Failed to read cache data, will proceed without cache. Reason: {Msg}", e.Message); } - private static string GetCacheObjectNameAttribute() + return null; + } + + public void Save(T obj) where T : class + { + var path = PathFromAttribute(); + _fileSystem.Directory.CreateDirectory(Path.GetDirectoryName(path)); + _fileSystem.File.WriteAllText(path, JsonConvert.SerializeObject(obj, new JsonSerializerSettings { - var attribute = typeof(T).GetCustomAttribute(); - if (attribute == null) + Formatting = Formatting.Indented, + ContractResolver = new DefaultContractResolver { - throw new ArgumentException($"{nameof(CacheObjectNameAttribute)} is missing on type {nameof(T)}"); + NamingStrategy = new SnakeCaseNamingStrategy() } + })); + } - return attribute.Name; - } - - private string BuildServiceGuid() + private static string GetCacheObjectNameAttribute() + { + var attribute = typeof(T).GetCustomAttribute(); + if (attribute == null) { - return _hash.ComputeHash(Encoding.ASCII.GetBytes(_configProvider.ActiveConfiguration.BaseUrl)) - .AsHexString(); + throw new ArgumentException($"{nameof(CacheObjectNameAttribute)} is missing on type {nameof(T)}"); } - private string PathFromAttribute() - { - var objectName = GetCacheObjectNameAttribute(); - if (!AllowedObjectNameCharacters.IsMatch(objectName)) - { - throw new ArgumentException($"Object name '{objectName}' has unacceptable characters"); - } + return attribute.Name; + } + + private string BuildServiceGuid() + { + return _hash.ComputeHash(Encoding.ASCII.GetBytes(_configProvider.ActiveConfiguration.BaseUrl)) + .AsHexString(); + } - return Path.Combine(_storagePath.Path, BuildServiceGuid(), objectName + ".json"); + private string PathFromAttribute() + { + var objectName = GetCacheObjectNameAttribute(); + if (!AllowedObjectNameCharacters.IsMatch(objectName)) + { + throw new ArgumentException($"Object name '{objectName}' has unacceptable characters"); } + + return Path.Combine(_storagePath.Path, BuildServiceGuid(), objectName + ".json"); } } diff --git a/src/TrashLib/Config/ConfigAutofacModule.cs b/src/TrashLib/Config/ConfigAutofacModule.cs index 5640d410..7a820c74 100644 --- a/src/TrashLib/Config/ConfigAutofacModule.cs +++ b/src/TrashLib/Config/ConfigAutofacModule.cs @@ -3,19 +3,18 @@ using Autofac; using FluentValidation; using Module = Autofac.Module; -namespace TrashLib.Config +namespace TrashLib.Config; + +public class ConfigAutofacModule : Module { - public class ConfigAutofacModule : Module + protected override void Load(ContainerBuilder builder) { - protected override void Load(ContainerBuilder builder) - { - builder.RegisterType() - .As() - .SingleInstance(); + builder.RegisterType() + .As() + .SingleInstance(); - builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()) - .AsClosedTypesOf(typeof(IValidator<>)) - .AsImplementedInterfaces(); - } + builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()) + .AsClosedTypesOf(typeof(IValidator<>)) + .AsImplementedInterfaces(); } } diff --git a/src/TrashLib/Config/ConfigurationProvider.cs b/src/TrashLib/Config/ConfigurationProvider.cs index ff6d7cec..e15a73c7 100644 --- a/src/TrashLib/Config/ConfigurationProvider.cs +++ b/src/TrashLib/Config/ConfigurationProvider.cs @@ -1,15 +1,14 @@ using System; -namespace TrashLib.Config +namespace TrashLib.Config; + +internal class ConfigurationProvider : IConfigurationProvider { - internal class ConfigurationProvider : IConfigurationProvider - { - private IServiceConfiguration? _activeConfiguration; + private IServiceConfiguration? _activeConfiguration; - public IServiceConfiguration ActiveConfiguration - { - get => _activeConfiguration ?? throw new NullReferenceException("Active configuration has not been set"); - set => _activeConfiguration = value; - } + public IServiceConfiguration ActiveConfiguration + { + get => _activeConfiguration ?? throw new NullReferenceException("Active configuration has not been set"); + set => _activeConfiguration = value; } } diff --git a/src/TrashLib/Config/IConfigurationProvider.cs b/src/TrashLib/Config/IConfigurationProvider.cs index a3668c94..0078d818 100644 --- a/src/TrashLib/Config/IConfigurationProvider.cs +++ b/src/TrashLib/Config/IConfigurationProvider.cs @@ -1,7 +1,6 @@ -namespace TrashLib.Config +namespace TrashLib.Config; + +public interface IConfigurationProvider { - public interface IConfigurationProvider - { - IServiceConfiguration ActiveConfiguration { get; set; } - } + IServiceConfiguration ActiveConfiguration { get; set; } } diff --git a/src/TrashLib/Config/IServerInfo.cs b/src/TrashLib/Config/IServerInfo.cs index 063fbac6..82c9aeb9 100644 --- a/src/TrashLib/Config/IServerInfo.cs +++ b/src/TrashLib/Config/IServerInfo.cs @@ -1,9 +1,8 @@ using Flurl.Http; -namespace TrashLib.Config +namespace TrashLib.Config; + +public interface IServerInfo { - public interface IServerInfo - { - IFlurlRequest BuildRequest(); - } + IFlurlRequest BuildRequest(); } diff --git a/src/TrashLib/Config/IServiceConfiguration.cs b/src/TrashLib/Config/IServiceConfiguration.cs index 6ffd131f..1f9abafe 100644 --- a/src/TrashLib/Config/IServiceConfiguration.cs +++ b/src/TrashLib/Config/IServiceConfiguration.cs @@ -1,8 +1,7 @@ -namespace TrashLib.Config +namespace TrashLib.Config; + +public interface IServiceConfiguration { - public interface IServiceConfiguration - { - string BaseUrl { get; } - string ApiKey { get; } - } + string BaseUrl { get; } + string ApiKey { get; } } diff --git a/src/TrashLib/Config/ServerInfo.cs b/src/TrashLib/Config/ServerInfo.cs index 0157b9e0..9f6f3184 100644 --- a/src/TrashLib/Config/ServerInfo.cs +++ b/src/TrashLib/Config/ServerInfo.cs @@ -3,28 +3,27 @@ using Flurl.Http; using Serilog; using TrashLib.Extensions; -namespace TrashLib.Config +namespace TrashLib.Config; + +internal class ServerInfo : IServerInfo { - internal class ServerInfo : IServerInfo - { - private readonly IConfigurationProvider _config; - private readonly ILogger _log; + private readonly IConfigurationProvider _config; + private readonly ILogger _log; - public ServerInfo(IConfigurationProvider config, ILogger log) - { - _config = config; - _log = log; - } + public ServerInfo(IConfigurationProvider config, ILogger log) + { + _config = config; + _log = log; + } - public IFlurlRequest BuildRequest() - { - var apiKey = _config.ActiveConfiguration.ApiKey; - var baseUrl = _config.ActiveConfiguration.BaseUrl; + public IFlurlRequest BuildRequest() + { + var apiKey = _config.ActiveConfiguration.ApiKey; + var baseUrl = _config.ActiveConfiguration.BaseUrl; - return baseUrl - .AppendPathSegment("api/v3") - .SetQueryParams(new {apikey = apiKey}) - .SanitizedLogging(_log); - } + return baseUrl + .AppendPathSegment("api/v3") + .SetQueryParams(new {apikey = apiKey}) + .SanitizedLogging(_log); } } diff --git a/src/TrashLib/Config/ServiceConfiguration.cs b/src/TrashLib/Config/ServiceConfiguration.cs index 964d19cf..673b6098 100644 --- a/src/TrashLib/Config/ServiceConfiguration.cs +++ b/src/TrashLib/Config/ServiceConfiguration.cs @@ -1,8 +1,7 @@ -namespace TrashLib.Config +namespace TrashLib.Config; + +public abstract class ServiceConfiguration : IServiceConfiguration { - public abstract class ServiceConfiguration : IServiceConfiguration - { - public string BaseUrl { get; init; } = ""; - public string ApiKey { get; init; } = ""; - } + public string BaseUrl { get; init; } = ""; + public string ApiKey { get; init; } = ""; } diff --git a/src/TrashLib/ExceptionTypes/VersionException.cs b/src/TrashLib/ExceptionTypes/VersionException.cs index c24e9980..f11b6985 100644 --- a/src/TrashLib/ExceptionTypes/VersionException.cs +++ b/src/TrashLib/ExceptionTypes/VersionException.cs @@ -1,19 +1,18 @@ using System; using System.Runtime.Serialization; -namespace TrashLib.ExceptionTypes +namespace TrashLib.ExceptionTypes; + +[Serializable] +public class VersionException : Exception { - [Serializable] - public class VersionException : Exception + public VersionException(string msg) + : base(msg) { - public VersionException(string msg) - : base(msg) - { - } + } - protected VersionException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } + protected VersionException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } diff --git a/src/TrashLib/Extensions/FlurlExtensions.cs b/src/TrashLib/Extensions/FlurlExtensions.cs index 9cfb9c21..d0f1dbd7 100644 --- a/src/TrashLib/Extensions/FlurlExtensions.cs +++ b/src/TrashLib/Extensions/FlurlExtensions.cs @@ -3,34 +3,33 @@ using Flurl; using Flurl.Http; using Serilog; -namespace TrashLib.Extensions +namespace TrashLib.Extensions; + +public static class FlurlExtensions { - public static class FlurlExtensions - { - public static IFlurlRequest SanitizedLogging(this Uri url, ILogger log) - => new FlurlRequest(url).SanitizedLogging(log); + public static IFlurlRequest SanitizedLogging(this Uri url, ILogger log) + => new FlurlRequest(url).SanitizedLogging(log); - public static IFlurlRequest SanitizedLogging(this Url url, ILogger log) - => new FlurlRequest(url).SanitizedLogging(log); + public static IFlurlRequest SanitizedLogging(this Url url, ILogger log) + => new FlurlRequest(url).SanitizedLogging(log); - public static IFlurlRequest SanitizedLogging(this string url, ILogger log) - => new FlurlRequest(url).SanitizedLogging(log); + public static IFlurlRequest SanitizedLogging(this string url, ILogger log) + => new FlurlRequest(url).SanitizedLogging(log); - public static IFlurlRequest SanitizedLogging(this IFlurlRequest request, ILogger log) - { - return request.ConfigureRequest(settings => FlurlLogging.SetupLogging(settings, log, SanitizeUrl)); - } + public static IFlurlRequest SanitizedLogging(this IFlurlRequest request, ILogger log) + { + return request.ConfigureRequest(settings => FlurlLogging.SetupLogging(settings, log, SanitizeUrl)); + } - private static Url SanitizeUrl(Url url) + private static Url SanitizeUrl(Url url) + { + // Replace hostname and API key for user privacy + url.Host = "hostname"; + if (url.QueryParams.Contains("apikey")) { - // Replace hostname and API key for user privacy - url.Host = "hostname"; - if (url.QueryParams.Contains("apikey")) - { - url.QueryParams.AddOrReplace("apikey", "SNIP"); - } - - return url; + url.QueryParams.AddOrReplace("apikey", "SNIP"); } + + return url; } } diff --git a/src/TrashLib/Extensions/FlurlLogging.cs b/src/TrashLib/Extensions/FlurlLogging.cs index b6ec5052..32f0f0c9 100644 --- a/src/TrashLib/Extensions/FlurlLogging.cs +++ b/src/TrashLib/Extensions/FlurlLogging.cs @@ -3,26 +3,25 @@ using Flurl; using Flurl.Http.Configuration; using Serilog; -namespace TrashLib.Extensions +namespace TrashLib.Extensions; + +public static class FlurlLogging { - public static class FlurlLogging + public static void SetupLogging(FlurlHttpSettings settings, ILogger log, Func? urlInterceptor = null) { - public static void SetupLogging(FlurlHttpSettings settings, ILogger log, Func? urlInterceptor = null) - { - urlInterceptor ??= url => url; + urlInterceptor ??= url => url; - settings.BeforeCall = call => - { - var url = urlInterceptor(call.Request.Url.Clone()); - log.Debug("HTTP Request to {Url}", url); - }; + settings.BeforeCall = call => + { + var url = urlInterceptor(call.Request.Url.Clone()); + log.Debug("HTTP Request to {Url}", url); + }; - settings.AfterCall = call => - { - var statusCode = call.Response?.StatusCode.ToString() ?? "(No response)"; - var url = urlInterceptor(call.Request.Url.Clone()); - log.Debug("HTTP Response {Status} from {Url}", statusCode, url); - }; - } + settings.AfterCall = call => + { + var statusCode = call.Response?.StatusCode.ToString() ?? "(No response)"; + var url = urlInterceptor(call.Request.Url.Clone()); + log.Debug("HTTP Response {Status} from {Url}", statusCode, url); + }; } } diff --git a/src/TrashLib/Radarr/Config/IRadarrValidationMessages.cs b/src/TrashLib/Radarr/Config/IRadarrValidationMessages.cs index b200295d..38657c05 100644 --- a/src/TrashLib/Radarr/Config/IRadarrValidationMessages.cs +++ b/src/TrashLib/Radarr/Config/IRadarrValidationMessages.cs @@ -1,11 +1,10 @@ -namespace TrashLib.Radarr.Config +namespace TrashLib.Radarr.Config; + +public interface IRadarrValidationMessages { - public interface IRadarrValidationMessages - { - string BaseUrl { get; } - string ApiKey { get; } - string CustomFormatNamesAndIds { get; } - string QualityProfileName { get; } - string QualityDefinitionType { get; } - } + string BaseUrl { get; } + string ApiKey { get; } + string CustomFormatNamesAndIds { get; } + string QualityProfileName { get; } + string QualityDefinitionType { get; } } diff --git a/src/TrashLib/Radarr/Config/IResourcePaths.cs b/src/TrashLib/Radarr/Config/IResourcePaths.cs index a1f51c8b..298b0242 100644 --- a/src/TrashLib/Radarr/Config/IResourcePaths.cs +++ b/src/TrashLib/Radarr/Config/IResourcePaths.cs @@ -1,7 +1,6 @@ -namespace TrashLib.Radarr.Config +namespace TrashLib.Radarr.Config; + +public interface IResourcePaths { - public interface IResourcePaths - { - string RepoPath { get; } - } + string RepoPath { get; } } diff --git a/src/TrashLib/Radarr/Config/RadarrConfiguration.cs b/src/TrashLib/Radarr/Config/RadarrConfiguration.cs index 9d4971be..63f919cb 100644 --- a/src/TrashLib/Radarr/Config/RadarrConfiguration.cs +++ b/src/TrashLib/Radarr/Config/RadarrConfiguration.cs @@ -3,39 +3,38 @@ using JetBrains.Annotations; using TrashLib.Config; using TrashLib.Radarr.QualityDefinition; -namespace TrashLib.Radarr.Config +namespace TrashLib.Radarr.Config; + +[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] +public class RadarrConfiguration : ServiceConfiguration { - [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] - public class RadarrConfiguration : ServiceConfiguration - { - public QualityDefinitionConfig? QualityDefinition { get; init; } - public ICollection CustomFormats { get; init; } = new List(); - public bool DeleteOldCustomFormats { get; init; } - } + public QualityDefinitionConfig? QualityDefinition { get; init; } + public ICollection CustomFormats { get; init; } = new List(); + public bool DeleteOldCustomFormats { get; init; } +} - [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] - public class CustomFormatConfig - { - public ICollection Names { get; init; } = new List(); - public ICollection TrashIds { get; init; } = new List(); - public ICollection QualityProfiles { get; init; } = new List(); - } +[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] +public class CustomFormatConfig +{ + public ICollection Names { get; init; } = new List(); + public ICollection TrashIds { get; init; } = new List(); + public ICollection QualityProfiles { get; init; } = new List(); +} - [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] - public class QualityProfileConfig - { - public string Name { get; init; } = ""; - public int? Score { get; init; } - public bool ResetUnmatchedScores { get; init; } - } +[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] +public class QualityProfileConfig +{ + public string Name { get; init; } = ""; + public int? Score { get; init; } + public bool ResetUnmatchedScores { get; init; } +} - [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] - public class QualityDefinitionConfig - { - // -1 does not map to a valid enumerator. this is to force validation to fail if it is not set from YAML. - // All of this craziness is to avoid making the enum type nullable. - public RadarrQualityDefinitionType Type { get; init; } = (RadarrQualityDefinitionType) (-1); +[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] +public class QualityDefinitionConfig +{ + // -1 does not map to a valid enumerator. this is to force validation to fail if it is not set from YAML. + // All of this craziness is to avoid making the enum type nullable. + public RadarrQualityDefinitionType Type { get; init; } = (RadarrQualityDefinitionType) (-1); - public decimal PreferredRatio { get; set; } = 1.0m; - } + public decimal PreferredRatio { get; set; } = 1.0m; } diff --git a/src/TrashLib/Radarr/Config/RadarrConfigurationValidator.cs b/src/TrashLib/Radarr/Config/RadarrConfigurationValidator.cs index 21dcf3c8..4fc00d0d 100644 --- a/src/TrashLib/Radarr/Config/RadarrConfigurationValidator.cs +++ b/src/TrashLib/Radarr/Config/RadarrConfigurationValidator.cs @@ -2,51 +2,50 @@ using Common.Extensions; using FluentValidation; using JetBrains.Annotations; -namespace TrashLib.Radarr.Config +namespace TrashLib.Radarr.Config; + +[UsedImplicitly] +internal class RadarrConfigurationValidator : AbstractValidator { - [UsedImplicitly] - internal class RadarrConfigurationValidator : AbstractValidator + public RadarrConfigurationValidator( + IRadarrValidationMessages messages, + IValidator qualityDefinitionConfigValidator, + IValidator customFormatConfigValidator) { - public RadarrConfigurationValidator( - IRadarrValidationMessages messages, - IValidator qualityDefinitionConfigValidator, - IValidator customFormatConfigValidator) - { - RuleFor(x => x.BaseUrl).NotEmpty().WithMessage(messages.BaseUrl); - RuleFor(x => x.ApiKey).NotEmpty().WithMessage(messages.ApiKey); - RuleFor(x => x.QualityDefinition).SetNonNullableValidator(qualityDefinitionConfigValidator); - RuleForEach(x => x.CustomFormats).SetValidator(customFormatConfigValidator); - } + RuleFor(x => x.BaseUrl).NotEmpty().WithMessage(messages.BaseUrl); + RuleFor(x => x.ApiKey).NotEmpty().WithMessage(messages.ApiKey); + RuleFor(x => x.QualityDefinition).SetNonNullableValidator(qualityDefinitionConfigValidator); + RuleForEach(x => x.CustomFormats).SetValidator(customFormatConfigValidator); } +} - [UsedImplicitly] - internal class CustomFormatConfigValidator : AbstractValidator +[UsedImplicitly] +internal class CustomFormatConfigValidator : AbstractValidator +{ + public CustomFormatConfigValidator( + IRadarrValidationMessages messages, + IValidator qualityProfileConfigValidator) { - public CustomFormatConfigValidator( - IRadarrValidationMessages messages, - IValidator qualityProfileConfigValidator) - { - RuleFor(x => x.Names).NotEmpty().When(x => x.TrashIds.Count == 0) - .WithMessage(messages.CustomFormatNamesAndIds); - RuleForEach(x => x.QualityProfiles).SetValidator(qualityProfileConfigValidator); - } + RuleFor(x => x.Names).NotEmpty().When(x => x.TrashIds.Count == 0) + .WithMessage(messages.CustomFormatNamesAndIds); + RuleForEach(x => x.QualityProfiles).SetValidator(qualityProfileConfigValidator); } +} - [UsedImplicitly] - internal class QualityProfileConfigValidator : AbstractValidator +[UsedImplicitly] +internal class QualityProfileConfigValidator : AbstractValidator +{ + public QualityProfileConfigValidator(IRadarrValidationMessages messages) { - public QualityProfileConfigValidator(IRadarrValidationMessages messages) - { - RuleFor(x => x.Name).NotEmpty().WithMessage(messages.QualityProfileName); - } + RuleFor(x => x.Name).NotEmpty().WithMessage(messages.QualityProfileName); } +} - [UsedImplicitly] - internal class QualityDefinitionConfigValidator : AbstractValidator +[UsedImplicitly] +internal class QualityDefinitionConfigValidator : AbstractValidator +{ + public QualityDefinitionConfigValidator(IRadarrValidationMessages messages) { - public QualityDefinitionConfigValidator(IRadarrValidationMessages messages) - { - RuleFor(x => x.Type).IsInEnum().WithMessage(messages.QualityDefinitionType); - } + RuleFor(x => x.Type).IsInEnum().WithMessage(messages.QualityDefinitionType); } } diff --git a/src/TrashLib/Radarr/Config/RadarrValidationMessages.cs b/src/TrashLib/Radarr/Config/RadarrValidationMessages.cs index b6c69d14..de2454b5 100644 --- a/src/TrashLib/Radarr/Config/RadarrValidationMessages.cs +++ b/src/TrashLib/Radarr/Config/RadarrValidationMessages.cs @@ -1,20 +1,19 @@ -namespace TrashLib.Radarr.Config +namespace TrashLib.Radarr.Config; + +internal class RadarrValidationMessages : IRadarrValidationMessages { - internal class RadarrValidationMessages : IRadarrValidationMessages - { - public string BaseUrl => - "Property 'base_url' is required"; + public string BaseUrl => + "Property 'base_url' is required"; - public string ApiKey => - "Property 'api_key' is required"; + public string ApiKey => + "Property 'api_key' is required"; - public string CustomFormatNamesAndIds => - "'custom_formats' elements must contain at least one element in either 'names' or 'trash_ids'"; + public string CustomFormatNamesAndIds => + "'custom_formats' elements must contain at least one element in either 'names' or 'trash_ids'"; - public string QualityProfileName => - "'name' is required for elements under 'quality_profiles'"; + public string QualityProfileName => + "'name' is required for elements under 'quality_profiles'"; - public string QualityDefinitionType => - "'type' is required for 'quality_definition'"; - } + public string QualityDefinitionType => + "'type' is required for 'quality_definition'"; } diff --git a/src/TrashLib/Radarr/CustomFormat/Api/CustomFormatService.cs b/src/TrashLib/Radarr/CustomFormat/Api/CustomFormatService.cs index 3757a2cf..04ef2650 100644 --- a/src/TrashLib/Radarr/CustomFormat/Api/CustomFormatService.cs +++ b/src/TrashLib/Radarr/CustomFormat/Api/CustomFormatService.cs @@ -5,52 +5,51 @@ using Newtonsoft.Json.Linq; using TrashLib.Config; using TrashLib.Radarr.CustomFormat.Models; -namespace TrashLib.Radarr.CustomFormat.Api +namespace TrashLib.Radarr.CustomFormat.Api; + +internal class CustomFormatService : ICustomFormatService { - internal class CustomFormatService : ICustomFormatService - { - private readonly IServerInfo _serverInfo; + private readonly IServerInfo _serverInfo; - public CustomFormatService(IServerInfo serverInfo) - { - _serverInfo = serverInfo; - } + public CustomFormatService(IServerInfo serverInfo) + { + _serverInfo = serverInfo; + } - public async Task> GetCustomFormats() - { - return await BuildRequest() - .AppendPathSegment("customformat") - .GetJsonAsync>(); - } + public async Task> GetCustomFormats() + { + return await BuildRequest() + .AppendPathSegment("customformat") + .GetJsonAsync>(); + } - public async Task CreateCustomFormat(ProcessedCustomFormatData cf) - { - var response = await BuildRequest() - .AppendPathSegment("customformat") - .PostJsonAsync(cf.Json) - .ReceiveJson(); - - if (response != null) - { - cf.SetCache(response.Value("id")); - } - } + public async Task CreateCustomFormat(ProcessedCustomFormatData cf) + { + var response = await BuildRequest() + .AppendPathSegment("customformat") + .PostJsonAsync(cf.Json) + .ReceiveJson(); - public async Task UpdateCustomFormat(ProcessedCustomFormatData cf) + if (response != null) { - await BuildRequest() - .AppendPathSegment($"customformat/{cf.GetCustomFormatId()}") - .PutJsonAsync(cf.Json) - .ReceiveJson(); + cf.SetCache(response.Value("id")); } + } - public async Task DeleteCustomFormat(int customFormatId) - { - await BuildRequest() - .AppendPathSegment($"customformat/{customFormatId}") - .DeleteAsync(); - } + public async Task UpdateCustomFormat(ProcessedCustomFormatData cf) + { + await BuildRequest() + .AppendPathSegment($"customformat/{cf.GetCustomFormatId()}") + .PutJsonAsync(cf.Json) + .ReceiveJson(); + } - private IFlurlRequest BuildRequest() => _serverInfo.BuildRequest(); + public async Task DeleteCustomFormat(int customFormatId) + { + await BuildRequest() + .AppendPathSegment($"customformat/{customFormatId}") + .DeleteAsync(); } + + private IFlurlRequest BuildRequest() => _serverInfo.BuildRequest(); } diff --git a/src/TrashLib/Radarr/CustomFormat/Api/ICustomFormatService.cs b/src/TrashLib/Radarr/CustomFormat/Api/ICustomFormatService.cs index 9c8a64c4..1a906d84 100644 --- a/src/TrashLib/Radarr/CustomFormat/Api/ICustomFormatService.cs +++ b/src/TrashLib/Radarr/CustomFormat/Api/ICustomFormatService.cs @@ -3,13 +3,12 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; using TrashLib.Radarr.CustomFormat.Models; -namespace TrashLib.Radarr.CustomFormat.Api +namespace TrashLib.Radarr.CustomFormat.Api; + +public interface ICustomFormatService { - public interface ICustomFormatService - { - Task> GetCustomFormats(); - Task CreateCustomFormat(ProcessedCustomFormatData cf); - Task UpdateCustomFormat(ProcessedCustomFormatData cf); - Task DeleteCustomFormat(int customFormatId); - } + Task> GetCustomFormats(); + Task CreateCustomFormat(ProcessedCustomFormatData cf); + Task UpdateCustomFormat(ProcessedCustomFormatData cf); + Task DeleteCustomFormat(int customFormatId); } diff --git a/src/TrashLib/Radarr/CustomFormat/Api/IQualityProfileService.cs b/src/TrashLib/Radarr/CustomFormat/Api/IQualityProfileService.cs index f1ba6442..611aa8a2 100644 --- a/src/TrashLib/Radarr/CustomFormat/Api/IQualityProfileService.cs +++ b/src/TrashLib/Radarr/CustomFormat/Api/IQualityProfileService.cs @@ -2,11 +2,10 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; -namespace TrashLib.Radarr.CustomFormat.Api +namespace TrashLib.Radarr.CustomFormat.Api; + +public interface IQualityProfileService { - public interface IQualityProfileService - { - Task> GetQualityProfiles(); - Task UpdateQualityProfile(JObject profileJson, int id); - } + Task> GetQualityProfiles(); + Task UpdateQualityProfile(JObject profileJson, int id); } diff --git a/src/TrashLib/Radarr/CustomFormat/Api/QualityProfileService.cs b/src/TrashLib/Radarr/CustomFormat/Api/QualityProfileService.cs index 8131c7bc..812ac74d 100644 --- a/src/TrashLib/Radarr/CustomFormat/Api/QualityProfileService.cs +++ b/src/TrashLib/Radarr/CustomFormat/Api/QualityProfileService.cs @@ -4,32 +4,31 @@ using Flurl.Http; using Newtonsoft.Json.Linq; using TrashLib.Config; -namespace TrashLib.Radarr.CustomFormat.Api -{ - internal class QualityProfileService : IQualityProfileService - { - private readonly IServerInfo _serverInfo; +namespace TrashLib.Radarr.CustomFormat.Api; - public QualityProfileService(IServerInfo serverInfo) - { - _serverInfo = serverInfo; - } +internal class QualityProfileService : IQualityProfileService +{ + private readonly IServerInfo _serverInfo; - public async Task> GetQualityProfiles() - { - return await BuildRequest() - .AppendPathSegment("qualityprofile") - .GetJsonAsync>(); - } + public QualityProfileService(IServerInfo serverInfo) + { + _serverInfo = serverInfo; + } - public async Task UpdateQualityProfile(JObject profileJson, int id) - { - return await BuildRequest() - .AppendPathSegment($"qualityprofile/{id}") - .PutJsonAsync(profileJson) - .ReceiveJson(); - } + public async Task> GetQualityProfiles() + { + return await BuildRequest() + .AppendPathSegment("qualityprofile") + .GetJsonAsync>(); + } - private IFlurlRequest BuildRequest() => _serverInfo.BuildRequest(); + public async Task UpdateQualityProfile(JObject profileJson, int id) + { + return await BuildRequest() + .AppendPathSegment($"qualityprofile/{id}") + .PutJsonAsync(profileJson) + .ReceiveJson(); } + + private IFlurlRequest BuildRequest() => _serverInfo.BuildRequest(); } diff --git a/src/TrashLib/Radarr/CustomFormat/ApiOperationType.cs b/src/TrashLib/Radarr/CustomFormat/ApiOperationType.cs index 6b920792..658bde80 100644 --- a/src/TrashLib/Radarr/CustomFormat/ApiOperationType.cs +++ b/src/TrashLib/Radarr/CustomFormat/ApiOperationType.cs @@ -1,10 +1,9 @@ -namespace TrashLib.Radarr.CustomFormat +namespace TrashLib.Radarr.CustomFormat; + +public enum ApiOperationType { - public enum ApiOperationType - { - Create, - Update, - NoChange, - Delete - } + Create, + Update, + NoChange, + Delete } diff --git a/src/TrashLib/Radarr/CustomFormat/CachePersister.cs b/src/TrashLib/Radarr/CustomFormat/CachePersister.cs index 8551e833..23e068d9 100644 --- a/src/TrashLib/Radarr/CustomFormat/CachePersister.cs +++ b/src/TrashLib/Radarr/CustomFormat/CachePersister.cs @@ -6,63 +6,62 @@ using TrashLib.Cache; using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; -namespace TrashLib.Radarr.CustomFormat +namespace TrashLib.Radarr.CustomFormat; + +internal class CachePersister : ICachePersister { - internal class CachePersister : ICachePersister - { - private readonly IServiceCache _cache; + private readonly IServiceCache _cache; - public CachePersister(ILogger log, IServiceCache cache) - { - Log = log; - _cache = cache; - } + public CachePersister(ILogger log, IServiceCache cache) + { + Log = log; + _cache = cache; + } - private ILogger Log { get; } - public CustomFormatCache? CfCache { get; private set; } + private ILogger Log { get; } + public CustomFormatCache? CfCache { get; private set; } - public void Load() + public void Load() + { + CfCache = _cache.Load(); + // ReSharper disable once ConvertIfStatementToConditionalTernaryExpression + if (CfCache != null) { - CfCache = _cache.Load(); - // ReSharper disable once ConvertIfStatementToConditionalTernaryExpression - if (CfCache != null) - { - Log.Debug("Loaded Cache"); + Log.Debug("Loaded Cache"); - // If the version is higher OR lower, we invalidate the cache. It means there's an - // incompatibility that we do not support. - if (CfCache.Version != CustomFormatCache.LatestVersion) - { - Log.Information("Cache version mismatch ({OldVersion} vs {LatestVersion}); ignoring cache data", - CfCache.Version, CustomFormatCache.LatestVersion); - CfCache = null; - } - } - else + // If the version is higher OR lower, we invalidate the cache. It means there's an + // incompatibility that we do not support. + if (CfCache.Version != CustomFormatCache.LatestVersion) { - Log.Debug("Custom format cache does not exist; proceeding without it"); + Log.Information("Cache version mismatch ({OldVersion} vs {LatestVersion}); ignoring cache data", + CfCache.Version, CustomFormatCache.LatestVersion); + CfCache = null; } } - - public void Save() + else { - if (CfCache == null) - { - Log.Debug("Not saving cache because it is null"); - return; - } - - Log.Debug("Saving Cache"); - _cache.Save(CfCache); + Log.Debug("Custom format cache does not exist; proceeding without it"); } + } - public void Update(IEnumerable customFormats) + public void Save() + { + if (CfCache == null) { - Log.Debug("Updating cache"); - CfCache = new CustomFormatCache(); - CfCache!.TrashIdMappings.AddRange(customFormats - .Where(cf => cf.CacheEntry != null) - .Select(cf => cf.CacheEntry!)); + Log.Debug("Not saving cache because it is null"); + return; } + + Log.Debug("Saving Cache"); + _cache.Save(CfCache); + } + + public void Update(IEnumerable customFormats) + { + Log.Debug("Updating cache"); + CfCache = new CustomFormatCache(); + CfCache!.TrashIdMappings.AddRange(customFormats + .Where(cf => cf.CacheEntry != null) + .Select(cf => cf.CacheEntry!)); } } diff --git a/src/TrashLib/Radarr/CustomFormat/CustomFormatUpdater.cs b/src/TrashLib/Radarr/CustomFormat/CustomFormatUpdater.cs index 86dfc386..803caffb 100644 --- a/src/TrashLib/Radarr/CustomFormat/CustomFormatUpdater.cs +++ b/src/TrashLib/Radarr/CustomFormat/CustomFormatUpdater.cs @@ -7,266 +7,265 @@ using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Processors; using TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps; -namespace TrashLib.Radarr.CustomFormat +namespace TrashLib.Radarr.CustomFormat; + +internal class CustomFormatUpdater : ICustomFormatUpdater { - internal class CustomFormatUpdater : ICustomFormatUpdater + private readonly ICachePersister _cache; + private readonly IGuideProcessor _guideProcessor; + private readonly IPersistenceProcessor _persistenceProcessor; + + public CustomFormatUpdater( + ILogger log, + ICachePersister cache, + IGuideProcessor guideProcessor, + IPersistenceProcessor persistenceProcessor) { - private readonly ICachePersister _cache; - private readonly IGuideProcessor _guideProcessor; - private readonly IPersistenceProcessor _persistenceProcessor; - - public CustomFormatUpdater( - ILogger log, - ICachePersister cache, - IGuideProcessor guideProcessor, - IPersistenceProcessor persistenceProcessor) - { - Log = log; - _cache = cache; - _guideProcessor = guideProcessor; - _persistenceProcessor = persistenceProcessor; - } - - private ILogger Log { get; } + Log = log; + _cache = cache; + _guideProcessor = guideProcessor; + _persistenceProcessor = persistenceProcessor; + } - public async Task Process(bool isPreview, RadarrConfiguration config) - { - _cache.Load(); + private ILogger Log { get; } - await _guideProcessor.BuildGuideDataAsync(config.CustomFormats.AsReadOnly(), _cache.CfCache); + public async Task Process(bool isPreview, RadarrConfiguration config) + { + _cache.Load(); - if (!ValidateGuideDataAndCheckShouldProceed(config)) - { - return; - } + await _guideProcessor.BuildGuideDataAsync(config.CustomFormats.AsReadOnly(), _cache.CfCache); - if (isPreview) - { - PreviewCustomFormats(); - } - else - { - await _persistenceProcessor.PersistCustomFormats(_guideProcessor.ProcessedCustomFormats, - _guideProcessor.DeletedCustomFormatsInCache, _guideProcessor.ProfileScores); + if (!ValidateGuideDataAndCheckShouldProceed(config)) + { + return; + } - PrintApiStatistics(_persistenceProcessor.Transactions); - PrintQualityProfileUpdates(); + if (isPreview) + { + PreviewCustomFormats(); + } + else + { + await _persistenceProcessor.PersistCustomFormats(_guideProcessor.ProcessedCustomFormats, + _guideProcessor.DeletedCustomFormatsInCache, _guideProcessor.ProfileScores); - // Cache all the custom formats (using ID from API response). - _cache.Update(_guideProcessor.ProcessedCustomFormats); - _cache.Save(); - } + PrintApiStatistics(_persistenceProcessor.Transactions); + PrintQualityProfileUpdates(); - _persistenceProcessor.Reset(); - _guideProcessor.Reset(); + // Cache all the custom formats (using ID from API response). + _cache.Update(_guideProcessor.ProcessedCustomFormats); + _cache.Save(); } - private void PrintQualityProfileUpdates() + _persistenceProcessor.Reset(); + _guideProcessor.Reset(); + } + + private void PrintQualityProfileUpdates() + { + if (_persistenceProcessor.UpdatedScores.Count > 0) { - if (_persistenceProcessor.UpdatedScores.Count > 0) + foreach (var (profileName, scores) in _persistenceProcessor.UpdatedScores) { - foreach (var (profileName, scores) in _persistenceProcessor.UpdatedScores) - { - Log.Debug("> Scores updated for quality profile: {ProfileName}", profileName); + Log.Debug("> Scores updated for quality profile: {ProfileName}", profileName); - foreach (var (customFormatName, score, reason) in scores) - { - Log.Debug(" - {Format}: {Score} ({Reason})", customFormatName, score, reason); - } + foreach (var (customFormatName, score, reason) in scores) + { + Log.Debug(" - {Format}: {Score} ({Reason})", customFormatName, score, reason); } - - Log.Information("Updated {ProfileCount} profiles and a total of {ScoreCount} scores", - _persistenceProcessor.UpdatedScores.Keys.Count, - _persistenceProcessor.UpdatedScores.Sum(s => s.Value.Count)); - } - else - { - Log.Information("All quality profile scores are already up to date!"); } - if (_persistenceProcessor.InvalidProfileNames.Count > 0) - { - Log.Warning("The following quality profile names are not valid and should either be " + - "removed or renamed in your YAML config"); - Log.Warning("{QualityProfileNames}", _persistenceProcessor.InvalidProfileNames); - } + Log.Information("Updated {ProfileCount} profiles and a total of {ScoreCount} scores", + _persistenceProcessor.UpdatedScores.Keys.Count, + _persistenceProcessor.UpdatedScores.Sum(s => s.Value.Count)); + } + else + { + Log.Information("All quality profile scores are already up to date!"); } - private void PrintApiStatistics(CustomFormatTransactionData transactions) + if (_persistenceProcessor.InvalidProfileNames.Count > 0) { - var created = transactions.NewCustomFormats; - if (created.Count > 0) - { - Log.Information("Created {Count} New Custom Formats: {CustomFormats}", created.Count, - created.Select(r => r.Name)); - } + Log.Warning("The following quality profile names are not valid and should either be " + + "removed or renamed in your YAML config"); + Log.Warning("{QualityProfileNames}", _persistenceProcessor.InvalidProfileNames); + } + } - var updated = transactions.UpdatedCustomFormats; - if (updated.Count > 0) - { - Log.Information("Updated {Count} Existing Custom Formats: {CustomFormats}", updated.Count, - updated.Select(r => r.Name)); - } + private void PrintApiStatistics(CustomFormatTransactionData transactions) + { + var created = transactions.NewCustomFormats; + if (created.Count > 0) + { + Log.Information("Created {Count} New Custom Formats: {CustomFormats}", created.Count, + created.Select(r => r.Name)); + } - var skipped = transactions.UnchangedCustomFormats; - if (skipped.Count > 0) - { - Log.Debug("Skipped {Count} Custom Formats that did not change: {CustomFormats}", skipped.Count, - skipped.Select(r => r.Name)); - } + var updated = transactions.UpdatedCustomFormats; + if (updated.Count > 0) + { + Log.Information("Updated {Count} Existing Custom Formats: {CustomFormats}", updated.Count, + updated.Select(r => r.Name)); + } - var deleted = transactions.DeletedCustomFormatIds; - if (deleted.Count > 0) - { - Log.Information("Deleted {Count} Custom Formats: {CustomFormats}", deleted.Count, - deleted.Select(r => r.CustomFormatName)); - } + var skipped = transactions.UnchangedCustomFormats; + if (skipped.Count > 0) + { + Log.Debug("Skipped {Count} Custom Formats that did not change: {CustomFormats}", skipped.Count, + skipped.Select(r => r.Name)); + } - var totalCount = created.Count + updated.Count; - if (totalCount > 0) - { - Log.Information("Total of {Count} custom formats synced to Radarr", totalCount); - } - else - { - Log.Information("All custom formats are already up to date!"); - } + var deleted = transactions.DeletedCustomFormatIds; + if (deleted.Count > 0) + { + Log.Information("Deleted {Count} Custom Formats: {CustomFormats}", deleted.Count, + deleted.Select(r => r.CustomFormatName)); } - private bool ValidateGuideDataAndCheckShouldProceed(RadarrConfiguration config) + var totalCount = created.Count + updated.Count; + if (totalCount > 0) { - Console.WriteLine(""); + Log.Information("Total of {Count} custom formats synced to Radarr", totalCount); + } + else + { + Log.Information("All custom formats are already up to date!"); + } + } - if (_guideProcessor.DuplicatedCustomFormats.Count > 0) - { - Log.Warning("One or more of the custom formats you want are duplicated in the guide. These custom " + - "formats WILL BE SKIPPED. Trash Updater is not able to choose which one you actually " + - "wanted. To resolve this ambiguity, use the `trash_ids` property in your YML " + - "configuration to refer to the custom format using its Trash ID instead of its name"); + private bool ValidateGuideDataAndCheckShouldProceed(RadarrConfiguration config) + { + Console.WriteLine(""); - foreach (var (cfName, dupes) in _guideProcessor.DuplicatedCustomFormats) + if (_guideProcessor.DuplicatedCustomFormats.Count > 0) + { + Log.Warning("One or more of the custom formats you want are duplicated in the guide. These custom " + + "formats WILL BE SKIPPED. Trash Updater is not able to choose which one you actually " + + "wanted. To resolve this ambiguity, use the `trash_ids` property in your YML " + + "configuration to refer to the custom format using its Trash ID instead of its name"); + + foreach (var (cfName, dupes) in _guideProcessor.DuplicatedCustomFormats) + { + Log.Warning("{CfName} is duplicated {DupeTimes} with the following Trash IDs:", cfName, + dupes.Count); + foreach (var cf in dupes) { - Log.Warning("{CfName} is duplicated {DupeTimes} with the following Trash IDs:", cfName, - dupes.Count); - foreach (var cf in dupes) - { - Log.Warning(" - {TrashId}", cf.TrashId); - } + Log.Warning(" - {TrashId}", cf.TrashId); } - - Console.WriteLine(""); } - if (_guideProcessor.CustomFormatsNotInGuide.Count > 0) - { - Log.Warning("The Custom Formats below do not exist in the guide and will " + - "be skipped. Names must match the 'name' field in the actual JSON, not the header in " + - "the guide! Either fix the names or remove them from your YAML config to resolve this " + - "warning"); - Log.Warning("{CfList}", _guideProcessor.CustomFormatsNotInGuide); - - Console.WriteLine(""); - } + Console.WriteLine(""); + } - var cfsWithoutQualityProfiles = _guideProcessor.ConfigData - .Where(d => d.QualityProfiles.Count == 0) - .SelectMany(d => d.CustomFormats.Select(cf => cf.Name)) - .ToList(); + if (_guideProcessor.CustomFormatsNotInGuide.Count > 0) + { + Log.Warning("The Custom Formats below do not exist in the guide and will " + + "be skipped. Names must match the 'name' field in the actual JSON, not the header in " + + "the guide! Either fix the names or remove them from your YAML config to resolve this " + + "warning"); + Log.Warning("{CfList}", _guideProcessor.CustomFormatsNotInGuide); - if (cfsWithoutQualityProfiles.Count > 0) - { - Log.Debug("These custom formats will be uploaded but are not associated to a quality profile in the " + - "config file: {UnassociatedCfs}", cfsWithoutQualityProfiles); + Console.WriteLine(""); + } - Console.WriteLine(""); - } + var cfsWithoutQualityProfiles = _guideProcessor.ConfigData + .Where(d => d.QualityProfiles.Count == 0) + .SelectMany(d => d.CustomFormats.Select(cf => cf.Name)) + .ToList(); - // No CFs are defined in this item, or they are all invalid. Skip this whole instance. - if (_guideProcessor.ConfigData.Count == 0) - { - Log.Error("Guide processing yielded no custom formats for configured instance host {BaseUrl}", - config.BaseUrl); - return false; - } + if (cfsWithoutQualityProfiles.Count > 0) + { + Log.Debug("These custom formats will be uploaded but are not associated to a quality profile in the " + + "config file: {UnassociatedCfs}", cfsWithoutQualityProfiles); - if (_guideProcessor.CustomFormatsWithoutScore.Count > 0) - { - Log.Warning("The below custom formats have no score in the guide or YAML " + - "config and will be skipped (remove them from your config or specify a " + - "score to fix this warning)"); - foreach (var tuple in _guideProcessor.CustomFormatsWithoutScore) - { - Log.Warning("{CfList}", tuple); - } + Console.WriteLine(""); + } - Console.WriteLine(""); - } + // No CFs are defined in this item, or they are all invalid. Skip this whole instance. + if (_guideProcessor.ConfigData.Count == 0) + { + Log.Error("Guide processing yielded no custom formats for configured instance host {BaseUrl}", + config.BaseUrl); + return false; + } - if (_guideProcessor.CustomFormatsWithOutdatedNames.Count > 0) + if (_guideProcessor.CustomFormatsWithoutScore.Count > 0) + { + Log.Warning("The below custom formats have no score in the guide or YAML " + + "config and will be skipped (remove them from your config or specify a " + + "score to fix this warning)"); + foreach (var tuple in _guideProcessor.CustomFormatsWithoutScore) { - Log.Warning("One or more custom format names in your YAML config have been renamed in the guide and " + - "are outdated. Each outdated name will be listed below. These custom formats will refuse " + - "to sync if your cache is deleted. To fix this warning, rename each one to its new name"); - - foreach (var (oldName, newName) in _guideProcessor.CustomFormatsWithOutdatedNames) - { - Log.Warning(" - '{OldName}' -> '{NewName}'", oldName, newName); - } - - Console.WriteLine(""); + Log.Warning("{CfList}", tuple); } - return true; + Console.WriteLine(""); } - private void PreviewCustomFormats() + if (_guideProcessor.CustomFormatsWithOutdatedNames.Count > 0) { - Console.WriteLine(""); - Console.WriteLine("========================================================="); - Console.WriteLine(" >>> Custom Formats From Guide <<< "); - Console.WriteLine("========================================================="); - Console.WriteLine(""); - - const string format = "{0,-30} {1,-35}"; - Console.WriteLine(format, "Custom Format", "Trash ID"); - Console.WriteLine(string.Concat(Enumerable.Repeat('-', 1 + 30 + 35))); + Log.Warning("One or more custom format names in your YAML config have been renamed in the guide and " + + "are outdated. Each outdated name will be listed below. These custom formats will refuse " + + "to sync if your cache is deleted. To fix this warning, rename each one to its new name"); - foreach (var cf in _guideProcessor.ProcessedCustomFormats) + foreach (var (oldName, newName) in _guideProcessor.CustomFormatsWithOutdatedNames) { - Console.WriteLine(format, cf.Name, cf.TrashId); + Log.Warning(" - '{OldName}' -> '{NewName}'", oldName, newName); } Console.WriteLine(""); - Console.WriteLine("========================================================="); - Console.WriteLine(" >>> Quality Profile Assignments & Scores <<< "); - Console.WriteLine("========================================================="); - Console.WriteLine(""); + } - const string profileFormat = "{0,-18} {1,-20} {2,-8}"; - Console.WriteLine(profileFormat, "Profile", "Custom Format", "Score"); - Console.WriteLine(string.Concat(Enumerable.Repeat('-', 2 + 18 + 20 + 8))); + return true; + } - foreach (var (profileName, scoreMap) in _guideProcessor.ProfileScores) - { - Console.WriteLine(profileFormat, profileName, "", ""); + private void PreviewCustomFormats() + { + Console.WriteLine(""); + Console.WriteLine("========================================================="); + Console.WriteLine(" >>> Custom Formats From Guide <<< "); + Console.WriteLine("========================================================="); + Console.WriteLine(""); - foreach (var (customFormat, score) in scoreMap.Mapping) - { - var matchingCf = _guideProcessor.ProcessedCustomFormats - .FirstOrDefault(cf => cf.TrashId.EqualsIgnoreCase(customFormat.TrashId)); + const string format = "{0,-30} {1,-35}"; + Console.WriteLine(format, "Custom Format", "Trash ID"); + Console.WriteLine(string.Concat(Enumerable.Repeat('-', 1 + 30 + 35))); + + foreach (var cf in _guideProcessor.ProcessedCustomFormats) + { + Console.WriteLine(format, cf.Name, cf.TrashId); + } + + Console.WriteLine(""); + Console.WriteLine("========================================================="); + Console.WriteLine(" >>> Quality Profile Assignments & Scores <<< "); + Console.WriteLine("========================================================="); + Console.WriteLine(""); + + const string profileFormat = "{0,-18} {1,-20} {2,-8}"; + Console.WriteLine(profileFormat, "Profile", "Custom Format", "Score"); + Console.WriteLine(string.Concat(Enumerable.Repeat('-', 2 + 18 + 20 + 8))); + + foreach (var (profileName, scoreMap) in _guideProcessor.ProfileScores) + { + Console.WriteLine(profileFormat, profileName, "", ""); - if (matchingCf == null) - { - Log.Warning("Quality Profile refers to CF not found in guide: {TrashId}", - customFormat.TrashId); - continue; - } + foreach (var (customFormat, score) in scoreMap.Mapping) + { + var matchingCf = _guideProcessor.ProcessedCustomFormats + .FirstOrDefault(cf => cf.TrashId.EqualsIgnoreCase(customFormat.TrashId)); - Console.WriteLine(profileFormat, "", matchingCf.Name, score); + if (matchingCf == null) + { + Log.Warning("Quality Profile refers to CF not found in guide: {TrashId}", + customFormat.TrashId); + continue; } - } - Console.WriteLine(""); + Console.WriteLine(profileFormat, "", matchingCf.Name, score); + } } + + Console.WriteLine(""); } } diff --git a/src/TrashLib/Radarr/CustomFormat/Guide/IRadarrGuideService.cs b/src/TrashLib/Radarr/CustomFormat/Guide/IRadarrGuideService.cs index 71dbee07..cddd6762 100644 --- a/src/TrashLib/Radarr/CustomFormat/Guide/IRadarrGuideService.cs +++ b/src/TrashLib/Radarr/CustomFormat/Guide/IRadarrGuideService.cs @@ -1,10 +1,9 @@ using System.Collections.Generic; using System.Threading.Tasks; -namespace TrashLib.Radarr.CustomFormat.Guide +namespace TrashLib.Radarr.CustomFormat.Guide; + +public interface IRadarrGuideService { - public interface IRadarrGuideService - { - Task> GetCustomFormatJsonAsync(); - } + Task> GetCustomFormatJsonAsync(); } diff --git a/src/TrashLib/Radarr/CustomFormat/ICachePersister.cs b/src/TrashLib/Radarr/CustomFormat/ICachePersister.cs index bb6ecdf9..0a4573dc 100644 --- a/src/TrashLib/Radarr/CustomFormat/ICachePersister.cs +++ b/src/TrashLib/Radarr/CustomFormat/ICachePersister.cs @@ -2,13 +2,12 @@ using System.Collections.Generic; using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; -namespace TrashLib.Radarr.CustomFormat +namespace TrashLib.Radarr.CustomFormat; + +public interface ICachePersister { - public interface ICachePersister - { - CustomFormatCache? CfCache { get; } - void Load(); - void Save(); - void Update(IEnumerable customFormats); - } + CustomFormatCache? CfCache { get; } + void Load(); + void Save(); + void Update(IEnumerable customFormats); } diff --git a/src/TrashLib/Radarr/CustomFormat/ICustomFormatUpdater.cs b/src/TrashLib/Radarr/CustomFormat/ICustomFormatUpdater.cs index 58fd89f0..cb02336a 100644 --- a/src/TrashLib/Radarr/CustomFormat/ICustomFormatUpdater.cs +++ b/src/TrashLib/Radarr/CustomFormat/ICustomFormatUpdater.cs @@ -1,10 +1,9 @@ using System.Threading.Tasks; using TrashLib.Radarr.Config; -namespace TrashLib.Radarr.CustomFormat +namespace TrashLib.Radarr.CustomFormat; + +public interface ICustomFormatUpdater { - public interface ICustomFormatUpdater - { - Task Process(bool isPreview, RadarrConfiguration config); - } + Task Process(bool isPreview, RadarrConfiguration config); } diff --git a/src/TrashLib/Radarr/CustomFormat/Models/Cache/CustomFormatCache.cs b/src/TrashLib/Radarr/CustomFormat/Models/Cache/CustomFormatCache.cs index 5231a6aa..658cc4b1 100644 --- a/src/TrashLib/Radarr/CustomFormat/Models/Cache/CustomFormatCache.cs +++ b/src/TrashLib/Radarr/CustomFormat/Models/Cache/CustomFormatCache.cs @@ -1,28 +1,27 @@ using System.Collections.ObjectModel; using TrashLib.Cache; -namespace TrashLib.Radarr.CustomFormat.Models.Cache +namespace TrashLib.Radarr.CustomFormat.Models.Cache; + +[CacheObjectName("custom-format-cache")] +public class CustomFormatCache { - [CacheObjectName("custom-format-cache")] - public class CustomFormatCache - { - public const int LatestVersion = 1; + public const int LatestVersion = 1; - public int Version { get; init; } = LatestVersion; - public Collection TrashIdMappings { get; init; } = new(); - } + public int Version { get; init; } = LatestVersion; + public Collection TrashIdMappings { get; init; } = new(); +} - public class TrashIdMapping +public class TrashIdMapping +{ + public TrashIdMapping(string trashId, string customFormatName, int customFormatId = default) { - public TrashIdMapping(string trashId, string customFormatName, int customFormatId = default) - { - CustomFormatName = customFormatName; - TrashId = trashId; - CustomFormatId = customFormatId; - } - - public string CustomFormatName { get; set; } - public string TrashId { get; } - public int CustomFormatId { get; set; } + CustomFormatName = customFormatName; + TrashId = trashId; + CustomFormatId = customFormatId; } + + public string CustomFormatName { get; set; } + public string TrashId { get; } + public int CustomFormatId { get; set; } } diff --git a/src/TrashLib/Radarr/CustomFormat/Models/ProcessedConfigData.cs b/src/TrashLib/Radarr/CustomFormat/Models/ProcessedConfigData.cs index 832ad755..4a3c401e 100644 --- a/src/TrashLib/Radarr/CustomFormat/Models/ProcessedConfigData.cs +++ b/src/TrashLib/Radarr/CustomFormat/Models/ProcessedConfigData.cs @@ -1,14 +1,13 @@ using System.Collections.Generic; using TrashLib.Radarr.Config; -namespace TrashLib.Radarr.CustomFormat.Models +namespace TrashLib.Radarr.CustomFormat.Models; + +public class ProcessedConfigData { - public class ProcessedConfigData - { - public ICollection CustomFormats { get; init; } - = new List(); + public ICollection CustomFormats { get; init; } + = new List(); - public ICollection QualityProfiles { get; init; } - = new List(); - } + public ICollection QualityProfiles { get; init; } + = new List(); } diff --git a/src/TrashLib/Radarr/CustomFormat/Models/ProcessedCustomFormatData.cs b/src/TrashLib/Radarr/CustomFormat/Models/ProcessedCustomFormatData.cs index 564788f8..e98d6e2a 100644 --- a/src/TrashLib/Radarr/CustomFormat/Models/ProcessedCustomFormatData.cs +++ b/src/TrashLib/Radarr/CustomFormat/Models/ProcessedCustomFormatData.cs @@ -3,34 +3,33 @@ using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json.Linq; using TrashLib.Radarr.CustomFormat.Models.Cache; -namespace TrashLib.Radarr.CustomFormat.Models +namespace TrashLib.Radarr.CustomFormat.Models; + +public class ProcessedCustomFormatData { - public class ProcessedCustomFormatData + public ProcessedCustomFormatData(string name, string trashId, JObject json) { - public ProcessedCustomFormatData(string name, string trashId, JObject json) - { - Name = name; - TrashId = trashId; - Json = json; - } - - public string Name { get; } - public string TrashId { get; } - public int? Score { get; init; } - public JObject Json { get; set; } - public TrashIdMapping? CacheEntry { get; set; } + Name = name; + TrashId = trashId; + Json = json; + } - public string CacheAwareName => CacheEntry?.CustomFormatName ?? Name; + public string Name { get; } + public string TrashId { get; } + public int? Score { get; init; } + public JObject Json { get; set; } + public TrashIdMapping? CacheEntry { get; set; } - public void SetCache(int customFormatId) - { - CacheEntry ??= new TrashIdMapping(TrashId, Name); - CacheEntry.CustomFormatId = customFormatId; - } + public string CacheAwareName => CacheEntry?.CustomFormatName ?? Name; - [SuppressMessage("Microsoft.Design", "CA1024", Justification = "Method throws an exception")] - public int GetCustomFormatId() - => CacheEntry?.CustomFormatId ?? - throw new InvalidOperationException("CacheEntry must exist to obtain custom format ID"); + public void SetCache(int customFormatId) + { + CacheEntry ??= new TrashIdMapping(TrashId, Name); + CacheEntry.CustomFormatId = customFormatId; } + + [SuppressMessage("Microsoft.Design", "CA1024", Justification = "Method throws an exception")] + public int GetCustomFormatId() + => CacheEntry?.CustomFormatId ?? + throw new InvalidOperationException("CacheEntry must exist to obtain custom format ID"); } diff --git a/src/TrashLib/Radarr/CustomFormat/Models/QualityProfileCustomFormatScoreMapping.cs b/src/TrashLib/Radarr/CustomFormat/Models/QualityProfileCustomFormatScoreMapping.cs index 2db37b26..d85528f2 100644 --- a/src/TrashLib/Radarr/CustomFormat/Models/QualityProfileCustomFormatScoreMapping.cs +++ b/src/TrashLib/Radarr/CustomFormat/Models/QualityProfileCustomFormatScoreMapping.cs @@ -1,17 +1,16 @@ using System.Collections.Generic; -namespace TrashLib.Radarr.CustomFormat.Models -{ - public record FormatMappingEntry(ProcessedCustomFormatData CustomFormat, int Score); +namespace TrashLib.Radarr.CustomFormat.Models; - public class QualityProfileCustomFormatScoreMapping - { - public QualityProfileCustomFormatScoreMapping(bool resetUnmatchedScores) - { - ResetUnmatchedScores = resetUnmatchedScores; - } +public record FormatMappingEntry(ProcessedCustomFormatData CustomFormat, int Score); - public bool ResetUnmatchedScores { get; } - public ICollection Mapping { get; init; } = new List(); +public class QualityProfileCustomFormatScoreMapping +{ + public QualityProfileCustomFormatScoreMapping(bool resetUnmatchedScores) + { + ResetUnmatchedScores = resetUnmatchedScores; } + + public bool ResetUnmatchedScores { get; } + public ICollection Mapping { get; init; } = new List(); } diff --git a/src/TrashLib/Radarr/CustomFormat/Models/UpdatedFormatScore.cs b/src/TrashLib/Radarr/CustomFormat/Models/UpdatedFormatScore.cs index 50b51f98..ed6e1bd5 100644 --- a/src/TrashLib/Radarr/CustomFormat/Models/UpdatedFormatScore.cs +++ b/src/TrashLib/Radarr/CustomFormat/Models/UpdatedFormatScore.cs @@ -1,13 +1,12 @@ -namespace TrashLib.Radarr.CustomFormat.Models -{ - public enum FormatScoreUpdateReason - { - Updated, - Reset - } +namespace TrashLib.Radarr.CustomFormat.Models; - public record UpdatedFormatScore( - string CustomFormatName, - int Score, - FormatScoreUpdateReason Reason); +public enum FormatScoreUpdateReason +{ + Updated, + Reset } + +public record UpdatedFormatScore( + string CustomFormatName, + int Score, + FormatScoreUpdateReason Reason); diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/GuideProcessor.cs b/src/TrashLib/Radarr/CustomFormat/Processors/GuideProcessor.cs index 9447c0a9..e33cf6cf 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/GuideProcessor.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/GuideProcessor.cs @@ -9,86 +9,85 @@ using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; using TrashLib.Radarr.CustomFormat.Processors.GuideSteps; -namespace TrashLib.Radarr.CustomFormat.Processors +namespace TrashLib.Radarr.CustomFormat.Processors; + +public interface IGuideProcessorSteps { - public interface IGuideProcessorSteps - { - ICustomFormatStep CustomFormat { get; } - IConfigStep Config { get; } - IQualityProfileStep QualityProfile { get; } - } + ICustomFormatStep CustomFormat { get; } + IConfigStep Config { get; } + IQualityProfileStep QualityProfile { get; } +} - internal class GuideProcessor : IGuideProcessor - { - private readonly IRadarrGuideService _guideService; - private readonly Func _stepsFactory; - private IList? _guideCustomFormatJson; - private IGuideProcessorSteps _steps; +internal class GuideProcessor : IGuideProcessor +{ + private readonly IRadarrGuideService _guideService; + private readonly Func _stepsFactory; + private IList? _guideCustomFormatJson; + private IGuideProcessorSteps _steps; - public GuideProcessor(ILogger log, IRadarrGuideService guideService, Func stepsFactory) - { - _guideService = guideService; - _stepsFactory = stepsFactory; - Log = log; - _steps = stepsFactory(); - } + public GuideProcessor(ILogger log, IRadarrGuideService guideService, Func stepsFactory) + { + _guideService = guideService; + _stepsFactory = stepsFactory; + Log = log; + _steps = stepsFactory(); + } - private ILogger Log { get; } + private ILogger Log { get; } - public IReadOnlyCollection ProcessedCustomFormats - => _steps.CustomFormat.ProcessedCustomFormats; + public IReadOnlyCollection ProcessedCustomFormats + => _steps.CustomFormat.ProcessedCustomFormats; - public IReadOnlyCollection CustomFormatsNotInGuide - => _steps.Config.CustomFormatsNotInGuide; + public IReadOnlyCollection CustomFormatsNotInGuide + => _steps.Config.CustomFormatsNotInGuide; - public IReadOnlyCollection ConfigData - => _steps.Config.ConfigData; + public IReadOnlyCollection ConfigData + => _steps.Config.ConfigData; - public IDictionary ProfileScores - => _steps.QualityProfile.ProfileScores; + public IDictionary ProfileScores + => _steps.QualityProfile.ProfileScores; - public IReadOnlyCollection<(string name, string trashId, string profileName)> CustomFormatsWithoutScore - => _steps.QualityProfile.CustomFormatsWithoutScore; + public IReadOnlyCollection<(string name, string trashId, string profileName)> CustomFormatsWithoutScore + => _steps.QualityProfile.CustomFormatsWithoutScore; - public IReadOnlyCollection DeletedCustomFormatsInCache - => _steps.CustomFormat.DeletedCustomFormatsInCache; + public IReadOnlyCollection DeletedCustomFormatsInCache + => _steps.CustomFormat.DeletedCustomFormatsInCache; - public IReadOnlyCollection<(string, string)> CustomFormatsWithOutdatedNames - => _steps.CustomFormat.CustomFormatsWithOutdatedNames; + public IReadOnlyCollection<(string, string)> CustomFormatsWithOutdatedNames + => _steps.CustomFormat.CustomFormatsWithOutdatedNames; - public IDictionary> DuplicatedCustomFormats - => _steps.CustomFormat.DuplicatedCustomFormats; + public IDictionary> DuplicatedCustomFormats + => _steps.CustomFormat.DuplicatedCustomFormats; - public async Task BuildGuideDataAsync(IReadOnlyCollection config, CustomFormatCache? cache) + public async Task BuildGuideDataAsync(IReadOnlyCollection config, CustomFormatCache? cache) + { + if (_guideCustomFormatJson == null) { - if (_guideCustomFormatJson == null) - { - Log.Information("Requesting and parsing guide markdown"); - _guideCustomFormatJson = (await _guideService.GetCustomFormatJsonAsync()).ToList(); - } - - // Step 1: Process and filter the custom formats from the guide. - // Custom formats in the guide not mentioned in the config are filtered out. - _steps.CustomFormat.Process(_guideCustomFormatJson, config, cache); - - // todo: Process cache entries that do not exist in the guide. Those should be deleted - // This might get taken care of when we rebuild the cache based on what is actually updated when - // we call the Radarr API - - // Step 2: Use the processed custom formats from step 1 to process the configuration. - // CFs in config not in the guide are filtered out. - // Actual CF objects are associated to the quality profile objects to reduce lookups - _steps.Config.Process(_steps.CustomFormat.ProcessedCustomFormats, config); - - // Step 3: Use the processed config (which contains processed CFs) to process the quality profile scores. - // Score precedence logic is utilized here to decide the CF score per profile (same CF can actually have - // different scores depending on which profile it goes into). - _steps.QualityProfile.Process(_steps.Config.ConfigData); + Log.Information("Requesting and parsing guide markdown"); + _guideCustomFormatJson = (await _guideService.GetCustomFormatJsonAsync()).ToList(); } - public void Reset() - { - _steps = _stepsFactory(); - } + // Step 1: Process and filter the custom formats from the guide. + // Custom formats in the guide not mentioned in the config are filtered out. + _steps.CustomFormat.Process(_guideCustomFormatJson, config, cache); + + // todo: Process cache entries that do not exist in the guide. Those should be deleted + // This might get taken care of when we rebuild the cache based on what is actually updated when + // we call the Radarr API + + // Step 2: Use the processed custom formats from step 1 to process the configuration. + // CFs in config not in the guide are filtered out. + // Actual CF objects are associated to the quality profile objects to reduce lookups + _steps.Config.Process(_steps.CustomFormat.ProcessedCustomFormats, config); + + // Step 3: Use the processed config (which contains processed CFs) to process the quality profile scores. + // Score precedence logic is utilized here to decide the CF score per profile (same CF can actually have + // different scores depending on which profile it goes into). + _steps.QualityProfile.Process(_steps.Config.ConfigData); + } + + public void Reset() + { + _steps = _stepsFactory(); } } diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/ConfigStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/ConfigStep.cs index 6be2f1ce..e4caf76d 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/ConfigStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/ConfigStep.cs @@ -5,64 +5,63 @@ using Common.Extensions; using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Models; -namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps +namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps; + +internal class ConfigStep : IConfigStep { - internal class ConfigStep : IConfigStep - { - private readonly List _configData = new(); - private readonly List _customFormatsNotInGuide = new(); + private readonly List _configData = new(); + private readonly List _customFormatsNotInGuide = new(); - public IReadOnlyCollection CustomFormatsNotInGuide => _customFormatsNotInGuide; - public IReadOnlyCollection ConfigData => _configData; + public IReadOnlyCollection CustomFormatsNotInGuide => _customFormatsNotInGuide; + public IReadOnlyCollection ConfigData => _configData; - public void Process(IReadOnlyCollection processedCfs, - IEnumerable config) + public void Process(IReadOnlyCollection processedCfs, + IEnumerable config) + { + foreach (var singleConfig in config) { - foreach (var singleConfig in config) - { - var validCfs = new List(); + var validCfs = new List(); - foreach (var name in singleConfig.Names) + foreach (var name in singleConfig.Names) + { + var match = FindCustomFormatByName(processedCfs, name); + if (match == null) { - var match = FindCustomFormatByName(processedCfs, name); - if (match == null) - { - _customFormatsNotInGuide.Add(name); - } - else - { - validCfs.Add(match); - } + _customFormatsNotInGuide.Add(name); } - - foreach (var trashId in singleConfig.TrashIds) + else { - var match = processedCfs.FirstOrDefault(cf => cf.TrashId.EqualsIgnoreCase(trashId)); - if (match == null) - { - _customFormatsNotInGuide.Add(trashId); - } - else - { - validCfs.Add(match); - } + validCfs.Add(match); } + } - _configData.Add(new ProcessedConfigData + foreach (var trashId in singleConfig.TrashIds) + { + var match = processedCfs.FirstOrDefault(cf => cf.TrashId.EqualsIgnoreCase(trashId)); + if (match == null) { - QualityProfiles = singleConfig.QualityProfiles, - CustomFormats = validCfs - .DistinctBy(cf => cf.TrashId, StringComparer.InvariantCultureIgnoreCase) - .ToList() - }); + _customFormatsNotInGuide.Add(trashId); + } + else + { + validCfs.Add(match); + } } - } - private static ProcessedCustomFormatData? FindCustomFormatByName( - IReadOnlyCollection processedCfs, string name) - { - return processedCfs.FirstOrDefault(cf => cf.CacheEntry?.CustomFormatName.EqualsIgnoreCase(name) ?? false) - ?? processedCfs.FirstOrDefault(cf => cf.Name.EqualsIgnoreCase(name)); + _configData.Add(new ProcessedConfigData + { + QualityProfiles = singleConfig.QualityProfiles, + CustomFormats = validCfs + .DistinctBy(cf => cf.TrashId, StringComparer.InvariantCultureIgnoreCase) + .ToList() + }); } } + + private static ProcessedCustomFormatData? FindCustomFormatByName( + IReadOnlyCollection processedCfs, string name) + { + return processedCfs.FirstOrDefault(cf => cf.CacheEntry?.CustomFormatName.EqualsIgnoreCase(name) ?? false) + ?? processedCfs.FirstOrDefault(cf => cf.Name.EqualsIgnoreCase(name)); + } } diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStep.cs index ffc96596..f9d3ac21 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStep.cs @@ -8,133 +8,132 @@ using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; -namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps +namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps; + +internal class CustomFormatStep : ICustomFormatStep { - internal class CustomFormatStep : ICustomFormatStep + private readonly List<(string, string)> _customFormatsWithOutdatedNames = new(); + private readonly List _processedCustomFormats = new(); + private readonly List _deletedCustomFormatsInCache = new(); + private readonly Dictionary> _duplicatedCustomFormats = new(); + + public IReadOnlyCollection<(string, string)> CustomFormatsWithOutdatedNames => _customFormatsWithOutdatedNames; + public IReadOnlyCollection ProcessedCustomFormats => _processedCustomFormats; + public IReadOnlyCollection DeletedCustomFormatsInCache => _deletedCustomFormatsInCache; + public IDictionary> DuplicatedCustomFormats => _duplicatedCustomFormats; + + public void Process(IEnumerable customFormatGuideData, + IReadOnlyCollection config, CustomFormatCache? cache) { - private readonly List<(string, string)> _customFormatsWithOutdatedNames = new(); - private readonly List _processedCustomFormats = new(); - private readonly List _deletedCustomFormatsInCache = new(); - private readonly Dictionary> _duplicatedCustomFormats = new(); - - public IReadOnlyCollection<(string, string)> CustomFormatsWithOutdatedNames => _customFormatsWithOutdatedNames; - public IReadOnlyCollection ProcessedCustomFormats => _processedCustomFormats; - public IReadOnlyCollection DeletedCustomFormatsInCache => _deletedCustomFormatsInCache; - public IDictionary> DuplicatedCustomFormats => _duplicatedCustomFormats; - - public void Process(IEnumerable customFormatGuideData, - IReadOnlyCollection config, CustomFormatCache? cache) + var processedCfs = customFormatGuideData + .Select(cf => ProcessCustomFormatData(cf, cache)) + .ToList(); + + // For each ID listed under the `trash_ids` YML property, match it to an existing CF + _processedCustomFormats.AddRange(config + .SelectMany(c => c.TrashIds) + .Distinct(StringComparer.CurrentCultureIgnoreCase) + .Join(processedCfs, + id => id, + cf => cf.TrashId, + (_, cf) => cf, + StringComparer.InvariantCultureIgnoreCase)); + + // Build a list of CF names under the `names` property in YAML. Exclude any names that + // are already provided by the `trash_ids` property. + var allConfigCfNames = config + .SelectMany(c => c.Names) + .Distinct(StringComparer.CurrentCultureIgnoreCase) + .Where(n => !ProcessedCustomFormats.Any(cf => cf.CacheAwareName.EqualsIgnoreCase(n))) + .ToList(); + + // Perform updates and deletions based on matches in the cache. Matches in the cache are by ID. + foreach (var cf in processedCfs) { - var processedCfs = customFormatGuideData - .Select(cf => ProcessCustomFormatData(cf, cache)) - .ToList(); - - // For each ID listed under the `trash_ids` YML property, match it to an existing CF - _processedCustomFormats.AddRange(config - .SelectMany(c => c.TrashIds) - .Distinct(StringComparer.CurrentCultureIgnoreCase) - .Join(processedCfs, - id => id, - cf => cf.TrashId, - (_, cf) => cf, - StringComparer.InvariantCultureIgnoreCase)); - - // Build a list of CF names under the `names` property in YAML. Exclude any names that - // are already provided by the `trash_ids` property. - var allConfigCfNames = config - .SelectMany(c => c.Names) - .Distinct(StringComparer.CurrentCultureIgnoreCase) - .Where(n => !ProcessedCustomFormats.Any(cf => cf.CacheAwareName.EqualsIgnoreCase(n))) - .ToList(); - - // Perform updates and deletions based on matches in the cache. Matches in the cache are by ID. - foreach (var cf in processedCfs) + // Does the name of the CF in the guide match a name in the config? If yes, we keep it. + var configName = allConfigCfNames.FirstOrDefault(n => n.EqualsIgnoreCase(cf.Name)); + if (configName != null) { - // Does the name of the CF in the guide match a name in the config? If yes, we keep it. - var configName = allConfigCfNames.FirstOrDefault(n => n.EqualsIgnoreCase(cf.Name)); - if (configName != null) - { - if (cf.CacheEntry != null) - { - // The cache entry might be using an old name. This will happen if: - // - A user has synced this CF before, AND - // - The name of the CF in the guide changed, AND - // - The user updated the name in their config to match the name in the guide. - cf.CacheEntry.CustomFormatName = cf.Name; - } - - _processedCustomFormats.Add(cf); - continue; - } - - // Does the name of the CF in the cache match a name in the config? If yes, we keep it. - configName = allConfigCfNames.FirstOrDefault(n => n.EqualsIgnoreCase(cf.CacheEntry?.CustomFormatName)); - if (configName != null) + if (cf.CacheEntry != null) { - // Config name is out of sync with the guide and should be updated - _customFormatsWithOutdatedNames.Add((configName, cf.Name)); - _processedCustomFormats.Add(cf); + // The cache entry might be using an old name. This will happen if: + // - A user has synced this CF before, AND + // - The name of the CF in the guide changed, AND + // - The user updated the name in their config to match the name in the guide. + cf.CacheEntry.CustomFormatName = cf.Name; } - // If we get here, we can't find a match in the config using cache or guide name, so the user must have - // removed it from their config. This will get marked for deletion later. + _processedCustomFormats.Add(cf); + continue; } - // Orphaned entries in cache represent custom formats we need to delete. - ProcessDeletedCustomFormats(cache); + // Does the name of the CF in the cache match a name in the config? If yes, we keep it. + configName = allConfigCfNames.FirstOrDefault(n => n.EqualsIgnoreCase(cf.CacheEntry?.CustomFormatName)); + if (configName != null) + { + // Config name is out of sync with the guide and should be updated + _customFormatsWithOutdatedNames.Add((configName, cf.Name)); + _processedCustomFormats.Add(cf); + } - // Check for multiple custom formats with the same name in the guide data (e.g. "DoVi") - ProcessDuplicates(); + // If we get here, we can't find a match in the config using cache or guide name, so the user must have + // removed it from their config. This will get marked for deletion later. } - private void ProcessDuplicates() - { - _duplicatedCustomFormats.Clear(); - _duplicatedCustomFormats.AddRange(ProcessedCustomFormats - .GroupBy(cf => cf.Name) - .Where(grp => grp.Count() > 1) - .ToDictionary(grp => grp.Key, grp => grp.ToList())); + // Orphaned entries in cache represent custom formats we need to delete. + ProcessDeletedCustomFormats(cache); - _processedCustomFormats.RemoveAll(cf => DuplicatedCustomFormats.ContainsKey(cf.Name)); - } + // Check for multiple custom formats with the same name in the guide data (e.g. "DoVi") + ProcessDuplicates(); + } - private static ProcessedCustomFormatData ProcessCustomFormatData(string guideData, CustomFormatCache? cache) - { - JObject obj = JObject.Parse(guideData); - var name = obj.ValueOrThrow("name"); - var trashId = obj.ValueOrThrow("trash_id"); - int? finalScore = null; + private void ProcessDuplicates() + { + _duplicatedCustomFormats.Clear(); + _duplicatedCustomFormats.AddRange(ProcessedCustomFormats + .GroupBy(cf => cf.Name) + .Where(grp => grp.Count() > 1) + .ToDictionary(grp => grp.Key, grp => grp.ToList())); - if (obj.TryGetValue("trash_score", out var score)) - { - finalScore = (int) score; - obj.Property("trash_score")?.Remove(); - } + _processedCustomFormats.RemoveAll(cf => DuplicatedCustomFormats.ContainsKey(cf.Name)); + } - // Remove trash_id, it's metadata that is not meant for Radarr itself - // Radarr supposedly drops this anyway, but I prefer it to be removed by TrashUpdater - obj.Property("trash_id")?.Remove(); + private static ProcessedCustomFormatData ProcessCustomFormatData(string guideData, CustomFormatCache? cache) + { + var obj = JObject.Parse(guideData); + var name = obj.ValueOrThrow("name"); + var trashId = obj.ValueOrThrow("trash_id"); + int? finalScore = null; - return new ProcessedCustomFormatData(name, trashId, obj) - { - Score = finalScore, - CacheEntry = cache?.TrashIdMappings.FirstOrDefault(c => c.TrashId == trashId) - }; + if (obj.TryGetValue("trash_score", out var score)) + { + finalScore = (int) score; + obj.Property("trash_score")?.Remove(); } - private void ProcessDeletedCustomFormats(CustomFormatCache? cache) - { - if (cache == null) - { - return; - } + // Remove trash_id, it's metadata that is not meant for Radarr itself + // Radarr supposedly drops this anyway, but I prefer it to be removed by TrashUpdater + obj.Property("trash_id")?.Remove(); - static bool MatchCfInCache(ProcessedCustomFormatData cf, TrashIdMapping c) - => cf.CacheEntry != null && cf.CacheEntry.TrashId == c.TrashId; + return new ProcessedCustomFormatData(name, trashId, obj) + { + Score = finalScore, + CacheEntry = cache?.TrashIdMappings.FirstOrDefault(c => c.TrashId == trashId) + }; + } - // Delete if CF is in cache and not in the guide or config - _deletedCustomFormatsInCache.AddRange(cache.TrashIdMappings - .Where(c => !ProcessedCustomFormats.Any(cf => MatchCfInCache(cf, c)))); + private void ProcessDeletedCustomFormats(CustomFormatCache? cache) + { + if (cache == null) + { + return; } + + static bool MatchCfInCache(ProcessedCustomFormatData cf, TrashIdMapping c) + => cf.CacheEntry != null && cf.CacheEntry.TrashId == c.TrashId; + + // Delete if CF is in cache and not in the guide or config + _deletedCustomFormatsInCache.AddRange(cache.TrashIdMappings + .Where(c => !ProcessedCustomFormats.Any(cf => MatchCfInCache(cf, c)))); } } diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/IConfigStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/IConfigStep.cs index eddc5492..aab48e93 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/IConfigStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/IConfigStep.cs @@ -2,14 +2,13 @@ using System.Collections.Generic; using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Models; -namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps +namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps; + +public interface IConfigStep { - public interface IConfigStep - { - IReadOnlyCollection CustomFormatsNotInGuide { get; } - IReadOnlyCollection ConfigData { get; } + IReadOnlyCollection CustomFormatsNotInGuide { get; } + IReadOnlyCollection ConfigData { get; } - void Process(IReadOnlyCollection processedCfs, - IEnumerable config); - } + void Process(IReadOnlyCollection processedCfs, + IEnumerable config); } diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/ICustomFormatStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/ICustomFormatStep.cs index 67f72e9a..b37de3a7 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/ICustomFormatStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/ICustomFormatStep.cs @@ -3,16 +3,15 @@ using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; -namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps +namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps; + +public interface ICustomFormatStep { - public interface ICustomFormatStep - { - IReadOnlyCollection ProcessedCustomFormats { get; } - IReadOnlyCollection DeletedCustomFormatsInCache { get; } - IReadOnlyCollection<(string, string)> CustomFormatsWithOutdatedNames { get; } - IDictionary> DuplicatedCustomFormats { get; } + IReadOnlyCollection ProcessedCustomFormats { get; } + IReadOnlyCollection DeletedCustomFormatsInCache { get; } + IReadOnlyCollection<(string, string)> CustomFormatsWithOutdatedNames { get; } + IDictionary> DuplicatedCustomFormats { get; } - void Process(IEnumerable customFormatGuideData, - IReadOnlyCollection config, CustomFormatCache? cache); - } + void Process(IEnumerable customFormatGuideData, + IReadOnlyCollection config, CustomFormatCache? cache); } diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/IQualityProfileStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/IQualityProfileStep.cs index 2bf4cd66..738182c8 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/IQualityProfileStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/IQualityProfileStep.cs @@ -1,12 +1,11 @@ using System.Collections.Generic; using TrashLib.Radarr.CustomFormat.Models; -namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps +namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps; + +public interface IQualityProfileStep { - public interface IQualityProfileStep - { - IDictionary ProfileScores { get; } - IReadOnlyCollection<(string name, string trashId, string profileName)> CustomFormatsWithoutScore { get; } - void Process(IEnumerable configData); - } + IDictionary ProfileScores { get; } + IReadOnlyCollection<(string name, string trashId, string profileName)> CustomFormatsWithoutScore { get; } + void Process(IEnumerable configData); } diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStep.cs index 8a767896..96100473 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStep.cs @@ -1,52 +1,53 @@ using System.Collections.Generic; using TrashLib.Radarr.CustomFormat.Models; -namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps +namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps; + +internal class QualityProfileStep : IQualityProfileStep { - internal class QualityProfileStep : IQualityProfileStep - { - private readonly Dictionary _profileScores = new(); - private readonly List<(string name, string trashId, string profileName)> _customFormatsWithoutScore = new(); + private readonly Dictionary _profileScores = new(); + private readonly List<(string name, string trashId, string profileName)> _customFormatsWithoutScore = new(); + + public IDictionary ProfileScores => _profileScores; - public IDictionary ProfileScores => _profileScores; - public IReadOnlyCollection<(string name, string trashId, string profileName)> CustomFormatsWithoutScore => _customFormatsWithoutScore; + public IReadOnlyCollection<(string name, string trashId, string profileName)> CustomFormatsWithoutScore + => _customFormatsWithoutScore; - public void Process(IEnumerable configData) + public void Process(IEnumerable configData) + { + foreach (var config in configData) + foreach (var profile in config.QualityProfiles) + foreach (var cf in config.CustomFormats) { - foreach (var config in configData) - foreach (var profile in config.QualityProfiles) - foreach (var cf in config.CustomFormats) + // Check if there is a score we can use. Priority is: + // 1. Score from the YAML config is used. If user did not provide, + // 2. Score from the guide is used. If the guide did not have one, + // 3. Warn the user and + var scoreToUse = profile.Score; + if (scoreToUse == null) { - // Check if there is a score we can use. Priority is: - // 1. Score from the YAML config is used. If user did not provide, - // 2. Score from the guide is used. If the guide did not have one, - // 3. Warn the user and - var scoreToUse = profile.Score; - if (scoreToUse == null) + if (cf.Score == null) { - if (cf.Score == null) - { - _customFormatsWithoutScore.Add((cf.Name, cf.TrashId, profile.Name)); - } - else - { - scoreToUse = cf.Score.Value; - } + _customFormatsWithoutScore.Add((cf.Name, cf.TrashId, profile.Name)); } - - if (scoreToUse == null) + else { - continue; + scoreToUse = cf.Score.Value; } + } - if (!ProfileScores.TryGetValue(profile.Name, out var mapping)) - { - mapping = new QualityProfileCustomFormatScoreMapping(profile.ResetUnmatchedScores); - ProfileScores[profile.Name] = mapping; - } + if (scoreToUse == null) + { + continue; + } - mapping.Mapping.Add(new FormatMappingEntry(cf, scoreToUse.Value)); + if (!ProfileScores.TryGetValue(profile.Name, out var mapping)) + { + mapping = new QualityProfileCustomFormatScoreMapping(profile.ResetUnmatchedScores); + ProfileScores[profile.Name] = mapping; } + + mapping.Mapping.Add(new FormatMappingEntry(cf, scoreToUse.Value)); } } } diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/IGuideProcessor.cs b/src/TrashLib/Radarr/CustomFormat/Processors/IGuideProcessor.cs index f979e3aa..f6737014 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/IGuideProcessor.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/IGuideProcessor.cs @@ -4,20 +4,19 @@ using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; -namespace TrashLib.Radarr.CustomFormat.Processors +namespace TrashLib.Radarr.CustomFormat.Processors; + +internal interface IGuideProcessor { - internal interface IGuideProcessor - { - IReadOnlyCollection ProcessedCustomFormats { get; } - IReadOnlyCollection CustomFormatsNotInGuide { get; } - IReadOnlyCollection ConfigData { get; } - IDictionary ProfileScores { get; } - IReadOnlyCollection<(string name, string trashId, string profileName)> CustomFormatsWithoutScore { get; } - IReadOnlyCollection DeletedCustomFormatsInCache { get; } - IReadOnlyCollection<(string, string)> CustomFormatsWithOutdatedNames { get; } - IDictionary> DuplicatedCustomFormats { get; } + IReadOnlyCollection ProcessedCustomFormats { get; } + IReadOnlyCollection CustomFormatsNotInGuide { get; } + IReadOnlyCollection ConfigData { get; } + IDictionary ProfileScores { get; } + IReadOnlyCollection<(string name, string trashId, string profileName)> CustomFormatsWithoutScore { get; } + IReadOnlyCollection DeletedCustomFormatsInCache { get; } + IReadOnlyCollection<(string, string)> CustomFormatsWithOutdatedNames { get; } + IDictionary> DuplicatedCustomFormats { get; } - Task BuildGuideDataAsync(IReadOnlyCollection config, CustomFormatCache? cache); - void Reset(); - } + Task BuildGuideDataAsync(IReadOnlyCollection config, CustomFormatCache? cache); + void Reset(); } diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/IPersistenceProcessor.cs b/src/TrashLib/Radarr/CustomFormat/Processors/IPersistenceProcessor.cs index 34721b40..8190824d 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/IPersistenceProcessor.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/IPersistenceProcessor.cs @@ -4,18 +4,17 @@ using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; using TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps; -namespace TrashLib.Radarr.CustomFormat.Processors +namespace TrashLib.Radarr.CustomFormat.Processors; + +public interface IPersistenceProcessor { - public interface IPersistenceProcessor - { - IDictionary> UpdatedScores { get; } - IReadOnlyCollection InvalidProfileNames { get; } - CustomFormatTransactionData Transactions { get; } + IDictionary> UpdatedScores { get; } + IReadOnlyCollection InvalidProfileNames { get; } + CustomFormatTransactionData Transactions { get; } - Task PersistCustomFormats(IReadOnlyCollection guideCfs, - IEnumerable deletedCfsInCache, - IDictionary profileScores); + Task PersistCustomFormats(IReadOnlyCollection guideCfs, + IEnumerable deletedCfsInCache, + IDictionary profileScores); - void Reset(); - } + void Reset(); } diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceProcessor.cs b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceProcessor.cs index aaae4f7d..6139fe07 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceProcessor.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceProcessor.cs @@ -8,75 +8,74 @@ using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; using TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps; -namespace TrashLib.Radarr.CustomFormat.Processors +namespace TrashLib.Radarr.CustomFormat.Processors; + +public interface IPersistenceProcessorSteps { - public interface IPersistenceProcessorSteps + public IJsonTransactionStep JsonTransactionStep { get; } + public ICustomFormatApiPersistenceStep CustomFormatCustomFormatApiPersister { get; } + public IQualityProfileApiPersistenceStep ProfileQualityProfileApiPersister { get; } +} + +internal class PersistenceProcessor : IPersistenceProcessor +{ + private readonly IConfigurationProvider _configProvider; + private readonly ICustomFormatService _customFormatService; + private readonly IQualityProfileService _qualityProfileService; + private readonly Func _stepsFactory; + private IPersistenceProcessorSteps _steps; + + public PersistenceProcessor( + ICustomFormatService customFormatService, + IQualityProfileService qualityProfileService, + IConfigurationProvider configProvider, + Func stepsFactory) { - public IJsonTransactionStep JsonTransactionStep { get; } - public ICustomFormatApiPersistenceStep CustomFormatCustomFormatApiPersister { get; } - public IQualityProfileApiPersistenceStep ProfileQualityProfileApiPersister { get; } + _customFormatService = customFormatService; + _qualityProfileService = qualityProfileService; + _stepsFactory = stepsFactory; + _configProvider = configProvider; + _steps = _stepsFactory(); } - internal class PersistenceProcessor : IPersistenceProcessor - { - private readonly IConfigurationProvider _configProvider; - private readonly ICustomFormatService _customFormatService; - private readonly IQualityProfileService _qualityProfileService; - private readonly Func _stepsFactory; - private IPersistenceProcessorSteps _steps; + public CustomFormatTransactionData Transactions + => _steps.JsonTransactionStep.Transactions; - public PersistenceProcessor( - ICustomFormatService customFormatService, - IQualityProfileService qualityProfileService, - IConfigurationProvider configProvider, - Func stepsFactory) - { - _customFormatService = customFormatService; - _qualityProfileService = qualityProfileService; - _stepsFactory = stepsFactory; - _configProvider = configProvider; - _steps = _stepsFactory(); - } + public IDictionary> UpdatedScores + => _steps.ProfileQualityProfileApiPersister.UpdatedScores; - public CustomFormatTransactionData Transactions - => _steps.JsonTransactionStep.Transactions; + public IReadOnlyCollection InvalidProfileNames + => _steps.ProfileQualityProfileApiPersister.InvalidProfileNames; - public IDictionary> UpdatedScores - => _steps.ProfileQualityProfileApiPersister.UpdatedScores; + public void Reset() + { + _steps = _stepsFactory(); + } - public IReadOnlyCollection InvalidProfileNames - => _steps.ProfileQualityProfileApiPersister.InvalidProfileNames; + public async Task PersistCustomFormats(IReadOnlyCollection guideCfs, + IEnumerable deletedCfsInCache, + IDictionary profileScores) + { + var radarrCfs = await _customFormatService.GetCustomFormats(); - public void Reset() - { - _steps = _stepsFactory(); - } + // Step 1: Match CFs between the guide & Radarr and merge the data. The goal is to retain as much of the + // original data from Radarr as possible. There are many properties in the response JSON that we don't + // directly care about. We keep those and just update the ones we do care about. + _steps.JsonTransactionStep.Process(guideCfs, radarrCfs); - public async Task PersistCustomFormats(IReadOnlyCollection guideCfs, - IEnumerable deletedCfsInCache, - IDictionary profileScores) + // Step 1.1: Optionally record deletions of custom formats in cache but not in the guide + var config = (RadarrConfiguration) _configProvider.ActiveConfiguration; + if (config.DeleteOldCustomFormats) { - var radarrCfs = await _customFormatService.GetCustomFormats(); - - // Step 1: Match CFs between the guide & Radarr and merge the data. The goal is to retain as much of the - // original data from Radarr as possible. There are many properties in the response JSON that we don't - // directly care about. We keep those and just update the ones we do care about. - _steps.JsonTransactionStep.Process(guideCfs, radarrCfs); - - // Step 1.1: Optionally record deletions of custom formats in cache but not in the guide - var config = (RadarrConfiguration) _configProvider.ActiveConfiguration; - if (config.DeleteOldCustomFormats) - { - _steps.JsonTransactionStep.RecordDeletions(deletedCfsInCache, radarrCfs); - } + _steps.JsonTransactionStep.RecordDeletions(deletedCfsInCache, radarrCfs); + } - // Step 2: For each merged CF, persist it to Radarr via its API. This will involve a combination of updates - // to existing CFs and creation of brand new ones, depending on what's already available in Radarr. - await _steps.CustomFormatCustomFormatApiPersister.Process(_customFormatService, - _steps.JsonTransactionStep.Transactions); + // Step 2: For each merged CF, persist it to Radarr via its API. This will involve a combination of updates + // to existing CFs and creation of brand new ones, depending on what's already available in Radarr. + await _steps.CustomFormatCustomFormatApiPersister.Process(_customFormatService, + _steps.JsonTransactionStep.Transactions); - // Step 3: Update all quality profiles with the scores from the guide for the uploaded custom formats - await _steps.ProfileQualityProfileApiPersister.Process(_qualityProfileService, profileScores); - } + // Step 3: Update all quality profiles with the scores from the guide for the uploaded custom formats + await _steps.ProfileQualityProfileApiPersister.Process(_qualityProfileService, profileScores); } } diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/CustomFormatApiPersistenceStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/CustomFormatApiPersistenceStep.cs index 49ec80be..a01df683 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/CustomFormatApiPersistenceStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/CustomFormatApiPersistenceStep.cs @@ -1,26 +1,25 @@ using System.Threading.Tasks; using TrashLib.Radarr.CustomFormat.Api; -namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps +namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps; + +internal class CustomFormatApiPersistenceStep : ICustomFormatApiPersistenceStep { - internal class CustomFormatApiPersistenceStep : ICustomFormatApiPersistenceStep + public async Task Process(ICustomFormatService api, CustomFormatTransactionData transactions) { - public async Task Process(ICustomFormatService api, CustomFormatTransactionData transactions) + foreach (var cf in transactions.NewCustomFormats) { - foreach (var cf in transactions.NewCustomFormats) - { - await api.CreateCustomFormat(cf); - } + await api.CreateCustomFormat(cf); + } - foreach (var cf in transactions.UpdatedCustomFormats) - { - await api.UpdateCustomFormat(cf); - } + foreach (var cf in transactions.UpdatedCustomFormats) + { + await api.UpdateCustomFormat(cf); + } - foreach (var cfId in transactions.DeletedCustomFormatIds) - { - await api.DeleteCustomFormat(cfId.CustomFormatId); - } + foreach (var cfId in transactions.DeletedCustomFormatIds) + { + await api.DeleteCustomFormat(cfId.CustomFormatId); } } } diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/ICustomFormatApiPersistenceStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/ICustomFormatApiPersistenceStep.cs index d2d01eae..5306d6b4 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/ICustomFormatApiPersistenceStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/ICustomFormatApiPersistenceStep.cs @@ -1,10 +1,9 @@ using System.Threading.Tasks; using TrashLib.Radarr.CustomFormat.Api; -namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps +namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps; + +public interface ICustomFormatApiPersistenceStep { - public interface ICustomFormatApiPersistenceStep - { - Task Process(ICustomFormatService api, CustomFormatTransactionData transactions); - } + Task Process(ICustomFormatService api, CustomFormatTransactionData transactions); } diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/IJsonTransactionStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/IJsonTransactionStep.cs index fbebc789..225ebe32 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/IJsonTransactionStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/IJsonTransactionStep.cs @@ -3,15 +3,14 @@ using Newtonsoft.Json.Linq; using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; -namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps +namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps; + +public interface IJsonTransactionStep { - public interface IJsonTransactionStep - { - CustomFormatTransactionData Transactions { get; } + CustomFormatTransactionData Transactions { get; } - void Process(IEnumerable guideCfs, - IReadOnlyCollection radarrCfs); + void Process(IEnumerable guideCfs, + IReadOnlyCollection radarrCfs); - void RecordDeletions(IEnumerable deletedCfsInCache, IEnumerable radarrCfs); - } + void RecordDeletions(IEnumerable deletedCfsInCache, IEnumerable radarrCfs); } diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/IQualityProfileApiPersistenceStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/IQualityProfileApiPersistenceStep.cs index a4bd0c75..e875ae6d 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/IQualityProfileApiPersistenceStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/IQualityProfileApiPersistenceStep.cs @@ -3,14 +3,13 @@ using System.Threading.Tasks; using TrashLib.Radarr.CustomFormat.Api; using TrashLib.Radarr.CustomFormat.Models; -namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps +namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps; + +public interface IQualityProfileApiPersistenceStep { - public interface IQualityProfileApiPersistenceStep - { - IDictionary> UpdatedScores { get; } - IReadOnlyCollection InvalidProfileNames { get; } + IDictionary> UpdatedScores { get; } + IReadOnlyCollection InvalidProfileNames { get; } - Task Process(IQualityProfileService api, - IDictionary cfScores); - } + Task Process(IQualityProfileService api, + IDictionary cfScores); } diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/JsonTransactionStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/JsonTransactionStep.cs index 5c79eb90..3f499cf7 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/JsonTransactionStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/JsonTransactionStep.cs @@ -6,179 +6,178 @@ using Newtonsoft.Json.Linq; using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; -namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps +namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps; + +public class CustomFormatTransactionData { - public class CustomFormatTransactionData - { - public Collection NewCustomFormats { get; } = new(); - public Collection UpdatedCustomFormats { get; } = new(); - public Collection DeletedCustomFormatIds { get; } = new(); - public Collection UnchangedCustomFormats { get; } = new(); - } + public Collection NewCustomFormats { get; } = new(); + public Collection UpdatedCustomFormats { get; } = new(); + public Collection DeletedCustomFormatIds { get; } = new(); + public Collection UnchangedCustomFormats { get; } = new(); +} - internal class JsonTransactionStep : IJsonTransactionStep - { - public CustomFormatTransactionData Transactions { get; } = new(); +internal class JsonTransactionStep : IJsonTransactionStep +{ + public CustomFormatTransactionData Transactions { get; } = new(); - public void Process(IEnumerable guideCfs, - IReadOnlyCollection radarrCfs) + public void Process(IEnumerable guideCfs, + IReadOnlyCollection radarrCfs) + { + foreach (var (guideCf, radarrCf) in guideCfs + .Select(gcf => (GuideCf: gcf, RadarrCf: FindRadarrCf(radarrCfs, gcf)))) { - foreach (var (guideCf, radarrCf) in guideCfs - .Select(gcf => (GuideCf: gcf, RadarrCf: FindRadarrCf(radarrCfs, gcf)))) + var guideCfJson = BuildNewRadarrCf(guideCf.Json); + + // no match; we add this CF as brand new + if (radarrCf == null) { - var guideCfJson = BuildNewRadarrCf(guideCf.Json); + guideCf.Json = guideCfJson; + Transactions.NewCustomFormats.Add(guideCf); + } + // found match in radarr CFs; update the existing CF + else + { + guideCf.Json = (JObject) radarrCf.DeepClone(); + UpdateRadarrCf(guideCf.Json, guideCfJson); - // no match; we add this CF as brand new - if (radarrCf == null) + // Set the cache for use later (like updating scores) if it hasn't been updated already. + // This handles CFs that already exist in Radarr but aren't cached (they will be added to cache + // later). + if (guideCf.CacheEntry == null) { - guideCf.Json = guideCfJson; - Transactions.NewCustomFormats.Add(guideCf); + guideCf.SetCache(guideCf.Json.Value("id")); + } + + if (!JToken.DeepEquals(radarrCf, guideCf.Json)) + { + Transactions.UpdatedCustomFormats.Add(guideCf); } - // found match in radarr CFs; update the existing CF else { - guideCf.Json = (JObject) radarrCf.DeepClone(); - UpdateRadarrCf(guideCf.Json, guideCfJson); - - // Set the cache for use later (like updating scores) if it hasn't been updated already. - // This handles CFs that already exist in Radarr but aren't cached (they will be added to cache - // later). - if (guideCf.CacheEntry == null) - { - guideCf.SetCache(guideCf.Json.Value("id")); - } - - if (!JToken.DeepEquals(radarrCf, guideCf.Json)) - { - Transactions.UpdatedCustomFormats.Add(guideCf); - } - else - { - Transactions.UnchangedCustomFormats.Add(guideCf); - } + Transactions.UnchangedCustomFormats.Add(guideCf); } } } + } - public void RecordDeletions(IEnumerable deletedCfsInCache, IEnumerable radarrCfs) - { - var cfs = radarrCfs.ToList(); - - // The 'Where' excludes cached CFs that were deleted manually by the user in Radarr - // FindRadarrCf() specifies 'null' for name because we should never delete unless an ID is found - foreach (var del in deletedCfsInCache.Where( - del => FindRadarrCf(cfs, del.CustomFormatId, null) != null)) - { - Transactions.DeletedCustomFormatIds.Add(del); - } - } + public void RecordDeletions(IEnumerable deletedCfsInCache, IEnumerable radarrCfs) + { + var cfs = radarrCfs.ToList(); - private static JObject? FindRadarrCf(IReadOnlyCollection radarrCfs, ProcessedCustomFormatData guideCf) + // The 'Where' excludes cached CFs that were deleted manually by the user in Radarr + // FindRadarrCf() specifies 'null' for name because we should never delete unless an ID is found + foreach (var del in deletedCfsInCache.Where( + del => FindRadarrCf(cfs, del.CustomFormatId, null) != null)) { - return FindRadarrCf(radarrCfs, guideCf.CacheEntry?.CustomFormatId, guideCf.Name); + Transactions.DeletedCustomFormatIds.Add(del); } + } - private static JObject? FindRadarrCf(IReadOnlyCollection radarrCfs, int? cfId, string? cfName) - { - JObject? match = null; - - // Try to find match in cache first - if (cfId != null) - { - match = radarrCfs.FirstOrDefault(rcf => cfId == rcf.Value("id")); - } + private static JObject? FindRadarrCf(IReadOnlyCollection radarrCfs, ProcessedCustomFormatData guideCf) + { + return FindRadarrCf(radarrCfs, guideCf.CacheEntry?.CustomFormatId, guideCf.Name); + } - // If we don't find by ID, search by name (if a name was given) - if (match == null && cfName != null) - { - match = radarrCfs.FirstOrDefault(rcf => cfName.EqualsIgnoreCase(rcf.Value("name"))); - } + private static JObject? FindRadarrCf(IReadOnlyCollection radarrCfs, int? cfId, string? cfName) + { + JObject? match = null; - return match; + // Try to find match in cache first + if (cfId != null) + { + match = radarrCfs.FirstOrDefault(rcf => cfId == rcf.Value("id")); } - private static void UpdateRadarrCf(JObject cfToModify, JObject cfToMergeFrom) + // If we don't find by ID, search by name (if a name was given) + if (match == null && cfName != null) { - MergeProperties(cfToModify, cfToMergeFrom, JTokenType.Array); + match = radarrCfs.FirstOrDefault(rcf => cfName.EqualsIgnoreCase(rcf.Value("name"))); + } + + return match; + } - var radarrSpecs = cfToModify["specifications"]?.Children() ?? new JEnumerable(); - var guideSpecs = cfToMergeFrom["specifications"]?.Children() ?? new JEnumerable(); + private static void UpdateRadarrCf(JObject cfToModify, JObject cfToMergeFrom) + { + MergeProperties(cfToModify, cfToMergeFrom, JTokenType.Array); + + var radarrSpecs = cfToModify["specifications"]?.Children() ?? new JEnumerable(); + var guideSpecs = cfToMergeFrom["specifications"]?.Children() ?? new JEnumerable(); - var matchedGuideSpecs = guideSpecs - .GroupBy(gs => radarrSpecs.FirstOrDefault(gss => KeyMatch(gss, gs, "name"))) - .SelectMany(kvp => kvp.Select(gs => new {GuideSpec = gs, RadarrSpec = kvp.Key})); + var matchedGuideSpecs = guideSpecs + .GroupBy(gs => radarrSpecs.FirstOrDefault(gss => KeyMatch(gss, gs, "name"))) + .SelectMany(kvp => kvp.Select(gs => new {GuideSpec = gs, RadarrSpec = kvp.Key})); - var newRadarrSpecs = new JArray(); + var newRadarrSpecs = new JArray(); - foreach (var match in matchedGuideSpecs) + foreach (var match in matchedGuideSpecs) + { + if (match.RadarrSpec != null) { - if (match.RadarrSpec != null) - { - MergeProperties(match.RadarrSpec, match.GuideSpec); - newRadarrSpecs.Add(match.RadarrSpec); - } - else - { - newRadarrSpecs.Add(match.GuideSpec); - } + MergeProperties(match.RadarrSpec, match.GuideSpec); + newRadarrSpecs.Add(match.RadarrSpec); + } + else + { + newRadarrSpecs.Add(match.GuideSpec); } - - cfToModify["specifications"] = newRadarrSpecs; } - private static bool KeyMatch(JObject left, JObject right, string keyName) - => left.Value(keyName) == right.Value(keyName); + cfToModify["specifications"] = newRadarrSpecs; + } + + private static bool KeyMatch(JObject left, JObject right, string keyName) + => left.Value(keyName) == right.Value(keyName); - private static void MergeProperties(JObject radarrCf, JObject guideCfJson, - JTokenType exceptType = JTokenType.None) + private static void MergeProperties(JObject radarrCf, JObject guideCfJson, + JTokenType exceptType = JTokenType.None) + { + foreach (var guideProp in guideCfJson.Properties().Where(p => p.Value.Type != exceptType)) { - foreach (var guideProp in guideCfJson.Properties().Where(p => p.Value.Type != exceptType)) + if (guideProp.Value.Type == JTokenType.Array && + radarrCf.TryGetValue(guideProp.Name, out var radarrArray)) { - if (guideProp.Value.Type == JTokenType.Array && - radarrCf.TryGetValue(guideProp.Name, out var radarrArray)) + ((JArray) radarrArray).Merge(guideProp.Value, new JsonMergeSettings { - ((JArray) radarrArray).Merge(guideProp.Value, new JsonMergeSettings - { - MergeArrayHandling = MergeArrayHandling.Merge - }); - } - else - { - radarrCf[guideProp.Name] = guideProp.Value; - } + MergeArrayHandling = MergeArrayHandling.Merge + }); + } + else + { + radarrCf[guideProp.Name] = guideProp.Value; } } + } - private static JObject BuildNewRadarrCf(JObject jsonPayload) + private static JObject BuildNewRadarrCf(JObject jsonPayload) + { + // Information on required fields from nitsua + /* + ok, for the specs.. you need name, implementation, negate, required, fields + for fields you need name & value + top level you need name, includeCustomFormatWhenRenaming, specs and id (if updating) + everything else radarr can handle with backend logic + */ + + var specs = jsonPayload["specifications"]; + if (specs is not null) { - // Information on required fields from nitsua - /* - ok, for the specs.. you need name, implementation, negate, required, fields - for fields you need name & value - top level you need name, includeCustomFormatWhenRenaming, specs and id (if updating) - everything else radarr can handle with backend logic - */ - - var specs = jsonPayload["specifications"]; - if (specs is not null) + foreach (var child in specs) { - foreach (var child in specs) + // convert from `"fields": {}` to `"fields": [{}]` (object to array of object) + // Weirdly the exported version of a custom format is not in array form, but the API requires the array + // even if there's only one element. + var field = child["fields"]; + if (field is null) { - // convert from `"fields": {}` to `"fields": [{}]` (object to array of object) - // Weirdly the exported version of a custom format is not in array form, but the API requires the array - // even if there's only one element. - var field = child["fields"]; - if (field is null) - { - continue; - } - - field["name"] = "value"; - child["fields"] = new JArray {field}; + continue; } - } - return jsonPayload; + field["name"] = "value"; + child["fields"] = new JArray {field}; + } } + + return jsonPayload; } } diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStep.cs index 5be36652..774784b7 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStep.cs @@ -8,84 +8,83 @@ using Newtonsoft.Json.Linq; using TrashLib.Radarr.CustomFormat.Api; using TrashLib.Radarr.CustomFormat.Models; -namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps +namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps; + +internal class QualityProfileApiPersistenceStep : IQualityProfileApiPersistenceStep { - internal class QualityProfileApiPersistenceStep : IQualityProfileApiPersistenceStep + private readonly List _invalidProfileNames = new(); + private readonly Dictionary> _updatedScores = new(); + + public IDictionary> UpdatedScores => _updatedScores; + public IReadOnlyCollection InvalidProfileNames => _invalidProfileNames; + + public async Task Process(IQualityProfileService api, + IDictionary cfScores) { - private readonly List _invalidProfileNames = new(); - private readonly Dictionary> _updatedScores = new(); + var radarrProfiles = await api.GetQualityProfiles(); - public IDictionary> UpdatedScores => _updatedScores; - public IReadOnlyCollection InvalidProfileNames => _invalidProfileNames; + // Match quality profiles in Radarr to ones the user put in their config. + // For each match, we return a tuple including the list of custom format scores ("formatItems"). + // Using GroupJoin() because we want a LEFT OUTER JOIN so we can list which quality profiles in config + // do not match profiles in Radarr. + var profileScores = cfScores.GroupJoin(radarrProfiles, + s => s.Key, + p => p.Value("name"), + (s, p) => (s.Key, s.Value, p.SelectMany(pi => pi.Children("formatItems")).ToList()), + StringComparer.InvariantCultureIgnoreCase); - public async Task Process(IQualityProfileService api, - IDictionary cfScores) + foreach (var (profileName, scoreMap, formatItems) in profileScores) { - var radarrProfiles = await api.GetQualityProfiles(); - - // Match quality profiles in Radarr to ones the user put in their config. - // For each match, we return a tuple including the list of custom format scores ("formatItems"). - // Using GroupJoin() because we want a LEFT OUTER JOIN so we can list which quality profiles in config - // do not match profiles in Radarr. - var profileScores = cfScores.GroupJoin(radarrProfiles, - s => s.Key, - p => p.Value("name"), - (s, p) => (s.Key, s.Value, p.SelectMany(pi => pi.Children("formatItems")).ToList()), - StringComparer.InvariantCultureIgnoreCase); + if (formatItems.Count == 0) + { + _invalidProfileNames.Add(profileName); + continue; + } - foreach (var (profileName, scoreMap, formatItems) in profileScores) + foreach (var json in formatItems) { - if (formatItems.Count == 0) + var map = FindScoreEntry(json, scoreMap); + + int? scoreToUse = null; + FormatScoreUpdateReason? reason = null; + + if (map != null) { - _invalidProfileNames.Add(profileName); - continue; + scoreToUse = map.Score; + reason = FormatScoreUpdateReason.Updated; } - - foreach (var json in formatItems) + else if (scoreMap.ResetUnmatchedScores) { - var map = FindScoreEntry(json, scoreMap); - - int? scoreToUse = null; - FormatScoreUpdateReason? reason = null; - - if (map != null) - { - scoreToUse = map.Score; - reason = FormatScoreUpdateReason.Updated; - } - else if (scoreMap.ResetUnmatchedScores) - { - scoreToUse = 0; - reason = FormatScoreUpdateReason.Reset; - } - - if (scoreToUse == null || reason == null || json.Value("score") == scoreToUse) - { - continue; - } - - json["score"] = scoreToUse.Value; - _updatedScores.GetOrCreate(profileName) - .Add(new UpdatedFormatScore(json.ValueOrThrow("name"), scoreToUse.Value, reason.Value)); + scoreToUse = 0; + reason = FormatScoreUpdateReason.Reset; } - if (!_updatedScores.TryGetValue(profileName, out var updatedScores) || updatedScores.Count == 0) + if (scoreToUse == null || reason == null || json.Value("score") == scoreToUse) { - // No scores to update, so don't bother with the API call continue; } - var jsonRoot = (JObject) formatItems.First().Root; - await api.UpdateQualityProfile(jsonRoot, jsonRoot.Value("id")); + json["score"] = scoreToUse.Value; + _updatedScores.GetOrCreate(profileName) + .Add(new UpdatedFormatScore(json.ValueOrThrow("name"), scoreToUse.Value, reason.Value)); } - } - private static FormatMappingEntry? FindScoreEntry(JObject formatItem, - QualityProfileCustomFormatScoreMapping scoreMap) - { - return scoreMap.Mapping.FirstOrDefault( - m => m.CustomFormat.CacheEntry != null && - formatItem.Value("format") == m.CustomFormat.CacheEntry.CustomFormatId); + if (!_updatedScores.TryGetValue(profileName, out var updatedScores) || updatedScores.Count == 0) + { + // No scores to update, so don't bother with the API call + continue; + } + + var jsonRoot = (JObject) formatItems.First().Root; + await api.UpdateQualityProfile(jsonRoot, jsonRoot.Value("id")); } } + + private static FormatMappingEntry? FindScoreEntry(JObject formatItem, + QualityProfileCustomFormatScoreMapping scoreMap) + { + return scoreMap.Mapping.FirstOrDefault( + m => m.CustomFormat.CacheEntry != null && + formatItem.Value("format") == m.CustomFormat.CacheEntry.CustomFormatId); + } } diff --git a/src/TrashLib/Radarr/QualityDefinition/Api/IQualityDefinitionService.cs b/src/TrashLib/Radarr/QualityDefinition/Api/IQualityDefinitionService.cs index 84d3e08e..b831908e 100644 --- a/src/TrashLib/Radarr/QualityDefinition/Api/IQualityDefinitionService.cs +++ b/src/TrashLib/Radarr/QualityDefinition/Api/IQualityDefinitionService.cs @@ -2,11 +2,10 @@ using System.Threading.Tasks; using TrashLib.Radarr.QualityDefinition.Api.Objects; -namespace TrashLib.Radarr.QualityDefinition.Api +namespace TrashLib.Radarr.QualityDefinition.Api; + +public interface IQualityDefinitionService { - public interface IQualityDefinitionService - { - Task> GetQualityDefinition(); - Task> UpdateQualityDefinition(IList newQuality); - } + Task> GetQualityDefinition(); + Task> UpdateQualityDefinition(IList newQuality); } diff --git a/src/TrashLib/Radarr/QualityDefinition/Api/Objects/RadarrQualityDefinitionItem.cs b/src/TrashLib/Radarr/QualityDefinition/Api/Objects/RadarrQualityDefinitionItem.cs index 606586b9..bb4455de 100644 --- a/src/TrashLib/Radarr/QualityDefinition/Api/Objects/RadarrQualityDefinitionItem.cs +++ b/src/TrashLib/Radarr/QualityDefinition/Api/Objects/RadarrQualityDefinitionItem.cs @@ -1,26 +1,25 @@ using JetBrains.Annotations; -namespace TrashLib.Radarr.QualityDefinition.Api.Objects +namespace TrashLib.Radarr.QualityDefinition.Api.Objects; + +[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] +public class RadarrQualityItem { - [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] - public class RadarrQualityItem - { - public int Id { get; set; } - public string Modifier { get; set; } = ""; - public string Name { get; set; } = ""; - public string Source { get; set; } = ""; - public int Resolution { get; set; } - } + public int Id { get; set; } + public string Modifier { get; set; } = ""; + public string Name { get; set; } = ""; + public string Source { get; set; } = ""; + public int Resolution { get; set; } +} - [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] - public class RadarrQualityDefinitionItem - { - public int Id { get; set; } - public RadarrQualityItem? Quality { get; set; } - public string Title { get; set; } = ""; - public int Weight { get; set; } - public decimal MinSize { get; set; } - public decimal? MaxSize { get; set; } - public decimal? PreferredSize { get; set; } - } +[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] +public class RadarrQualityDefinitionItem +{ + public int Id { get; set; } + public RadarrQualityItem? Quality { get; set; } + public string Title { get; set; } = ""; + public int Weight { get; set; } + public decimal MinSize { get; set; } + public decimal? MaxSize { get; set; } + public decimal? PreferredSize { get; set; } } diff --git a/src/TrashLib/Radarr/QualityDefinition/Api/QualityDefinitionService.cs b/src/TrashLib/Radarr/QualityDefinition/Api/QualityDefinitionService.cs index e2433d38..aaa24caa 100644 --- a/src/TrashLib/Radarr/QualityDefinition/Api/QualityDefinitionService.cs +++ b/src/TrashLib/Radarr/QualityDefinition/Api/QualityDefinitionService.cs @@ -4,33 +4,32 @@ using Flurl.Http; using TrashLib.Config; using TrashLib.Radarr.QualityDefinition.Api.Objects; -namespace TrashLib.Radarr.QualityDefinition.Api -{ - internal class QualityDefinitionService : IQualityDefinitionService - { - private readonly IServerInfo _serverInfo; +namespace TrashLib.Radarr.QualityDefinition.Api; - public QualityDefinitionService(IServerInfo serverInfo) - { - _serverInfo = serverInfo; - } +internal class QualityDefinitionService : IQualityDefinitionService +{ + private readonly IServerInfo _serverInfo; - public async Task> GetQualityDefinition() - { - return await BuildRequest() - .AppendPathSegment("qualitydefinition") - .GetJsonAsync>(); - } + public QualityDefinitionService(IServerInfo serverInfo) + { + _serverInfo = serverInfo; + } - public async Task> UpdateQualityDefinition( - IList newQuality) - { - return await BuildRequest() - .AppendPathSegment("qualityDefinition/update") - .PutJsonAsync(newQuality) - .ReceiveJson>(); - } + public async Task> GetQualityDefinition() + { + return await BuildRequest() + .AppendPathSegment("qualitydefinition") + .GetJsonAsync>(); + } - private IFlurlRequest BuildRequest() => _serverInfo.BuildRequest(); + public async Task> UpdateQualityDefinition( + IList newQuality) + { + return await BuildRequest() + .AppendPathSegment("qualityDefinition/update") + .PutJsonAsync(newQuality) + .ReceiveJson>(); } + + private IFlurlRequest BuildRequest() => _serverInfo.BuildRequest(); } diff --git a/src/TrashLib/Radarr/QualityDefinition/IRadarrQualityDefinitionGuideParser.cs b/src/TrashLib/Radarr/QualityDefinition/IRadarrQualityDefinitionGuideParser.cs index 684dec45..d83eb5aa 100644 --- a/src/TrashLib/Radarr/QualityDefinition/IRadarrQualityDefinitionGuideParser.cs +++ b/src/TrashLib/Radarr/QualityDefinition/IRadarrQualityDefinitionGuideParser.cs @@ -1,11 +1,10 @@ using System.Collections.Generic; using System.Threading.Tasks; -namespace TrashLib.Radarr.QualityDefinition +namespace TrashLib.Radarr.QualityDefinition; + +public interface IRadarrQualityDefinitionGuideParser { - public interface IRadarrQualityDefinitionGuideParser - { - Task GetMarkdownData(); - IDictionary> ParseMarkdown(string markdown); - } + Task GetMarkdownData(); + IDictionary> ParseMarkdown(string markdown); } diff --git a/src/TrashLib/Radarr/QualityDefinition/IRadarrQualityDefinitionUpdater.cs b/src/TrashLib/Radarr/QualityDefinition/IRadarrQualityDefinitionUpdater.cs index 38942bd6..eb50b4d4 100644 --- a/src/TrashLib/Radarr/QualityDefinition/IRadarrQualityDefinitionUpdater.cs +++ b/src/TrashLib/Radarr/QualityDefinition/IRadarrQualityDefinitionUpdater.cs @@ -1,10 +1,9 @@ using System.Threading.Tasks; using TrashLib.Radarr.Config; -namespace TrashLib.Radarr.QualityDefinition +namespace TrashLib.Radarr.QualityDefinition; + +public interface IRadarrQualityDefinitionUpdater { - public interface IRadarrQualityDefinitionUpdater - { - Task Process(bool isPreview, RadarrConfiguration config); - } + Task Process(bool isPreview, RadarrConfiguration config); } diff --git a/src/TrashLib/Radarr/QualityDefinition/RadarrQualityData.cs b/src/TrashLib/Radarr/QualityDefinition/RadarrQualityData.cs index aa1823c0..6457e1a6 100644 --- a/src/TrashLib/Radarr/QualityDefinition/RadarrQualityData.cs +++ b/src/TrashLib/Radarr/QualityDefinition/RadarrQualityData.cs @@ -1,27 +1,26 @@ using System; using TrashLib.Sonarr.QualityDefinition; -namespace TrashLib.Radarr.QualityDefinition +namespace TrashLib.Radarr.QualityDefinition; + +public class RadarrQualityData : SonarrQualityData { - public class RadarrQualityData : SonarrQualityData - { - public const decimal PreferredUnlimitedThreshold = 395; + public const decimal PreferredUnlimitedThreshold = 395; - public decimal Preferred { get; set; } - public decimal? PreferredForApi => Preferred < PreferredUnlimitedThreshold ? Preferred : null; - public string AnnotatedPreferred => AnnotatedValue(Preferred, PreferredUnlimitedThreshold); + public decimal Preferred { get; set; } + public decimal? PreferredForApi => Preferred < PreferredUnlimitedThreshold ? Preferred : null; + public string AnnotatedPreferred => AnnotatedValue(Preferred, PreferredUnlimitedThreshold); - public decimal InterpolatedPreferred(decimal ratio) - { - var cappedMax = Math.Min(Max, PreferredUnlimitedThreshold); - return Math.Round(Min + (cappedMax - Min) * ratio, 1); - } + public decimal InterpolatedPreferred(decimal ratio) + { + var cappedMax = Math.Min(Max, PreferredUnlimitedThreshold); + return Math.Round(Min + (cappedMax - Min) * ratio, 1); + } - public bool IsPreferredDifferent(decimal? serviceValue) - { - return serviceValue == null - ? PreferredUnlimitedThreshold != Preferred - : serviceValue != Preferred || PreferredUnlimitedThreshold == Preferred; - } + public bool IsPreferredDifferent(decimal? serviceValue) + { + return serviceValue == null + ? PreferredUnlimitedThreshold != Preferred + : serviceValue != Preferred || PreferredUnlimitedThreshold == Preferred; } } diff --git a/src/TrashLib/Radarr/QualityDefinition/RadarrQualityDefinitionGuideParser.cs b/src/TrashLib/Radarr/QualityDefinition/RadarrQualityDefinitionGuideParser.cs index f9e0d775..51a1d406 100644 --- a/src/TrashLib/Radarr/QualityDefinition/RadarrQualityDefinitionGuideParser.cs +++ b/src/TrashLib/Radarr/QualityDefinition/RadarrQualityDefinitionGuideParser.cs @@ -5,67 +5,66 @@ using System.Threading.Tasks; using Common.Extensions; using Flurl.Http; -namespace TrashLib.Radarr.QualityDefinition +namespace TrashLib.Radarr.QualityDefinition; + +internal class RadarrQualityDefinitionGuideParser : IRadarrQualityDefinitionGuideParser { - internal class RadarrQualityDefinitionGuideParser : IRadarrQualityDefinitionGuideParser - { - private readonly Regex _regexHeader = new(@"^#+", RegexOptions.Compiled); + private readonly Regex _regexHeader = new(@"^#+", RegexOptions.Compiled); - private readonly Regex _regexTableRow = - new(@"\| *(.*?) *\| *([\d.]+) *\| *([\d.]+) *\|", RegexOptions.Compiled); + private readonly Regex _regexTableRow = + new(@"\| *(.*?) *\| *([\d.]+) *\| *([\d.]+) *\|", RegexOptions.Compiled); - public async Task GetMarkdownData() - { - return await - "https://raw.githubusercontent.com/TRaSH-/Guides/master/docs/Radarr/Radarr-Quality-Settings-File-Size.md" - .GetStringAsync(); - } + public async Task GetMarkdownData() + { + return await + "https://raw.githubusercontent.com/TRaSH-/Guides/master/docs/Radarr/Radarr-Quality-Settings-File-Size.md" + .GetStringAsync(); + } + + public IDictionary> ParseMarkdown(string markdown) + { + var results = new Dictionary>(); + List? table = null; - public IDictionary> ParseMarkdown(string markdown) + var reader = new StringReader(markdown); + for (var line = reader.ReadLine(); line != null; line = reader.ReadLine()) { - var results = new Dictionary>(); - List? table = null; + if (string.IsNullOrEmpty(line)) + { + continue; + } - var reader = new StringReader(markdown); - for (var line = reader.ReadLine(); line != null; line = reader.ReadLine()) + var match = _regexHeader.Match(line); + if (match.Success) { - if (string.IsNullOrEmpty(line)) - { - continue; - } + // todo: hard-coded for now since there's only one supported right now. + var type = RadarrQualityDefinitionType.Movie; + table = results.GetOrCreate(type); - var match = _regexHeader.Match(line); - if (match.Success) + // If we grab a table that isn't empty, that means for whatever reason *another* table + // in the markdown is trying to modify a previous table's data. For example, maybe there + // are two "Series" quality tables. That would be a weird edge case, but handle that + // here just in case. + if (table.Count > 0) { - // todo: hard-coded for now since there's only one supported right now. - var type = RadarrQualityDefinitionType.Movie; - table = results.GetOrCreate(type); - - // If we grab a table that isn't empty, that means for whatever reason *another* table - // in the markdown is trying to modify a previous table's data. For example, maybe there - // are two "Series" quality tables. That would be a weird edge case, but handle that - // here just in case. - if (table.Count > 0) - { - table = null; - } + table = null; } - else if (table != null) + } + else if (table != null) + { + match = _regexTableRow.Match(line); + if (match.Success) { - match = _regexTableRow.Match(line); - if (match.Success) + table.Add(new RadarrQualityData { - table.Add(new RadarrQualityData - { - Name = match.Groups[1].Value, - Min = match.Groups[2].Value.ToDecimal(), - Max = match.Groups[3].Value.ToDecimal() - }); - } + Name = match.Groups[1].Value, + Min = match.Groups[2].Value.ToDecimal(), + Max = match.Groups[3].Value.ToDecimal() + }); } } - - return results; } + + return results; } } diff --git a/src/TrashLib/Radarr/QualityDefinition/RadarrQualityDefinitionType.cs b/src/TrashLib/Radarr/QualityDefinition/RadarrQualityDefinitionType.cs index a736046d..d6ee3a0e 100644 --- a/src/TrashLib/Radarr/QualityDefinition/RadarrQualityDefinitionType.cs +++ b/src/TrashLib/Radarr/QualityDefinition/RadarrQualityDefinitionType.cs @@ -1,7 +1,6 @@ -namespace TrashLib.Radarr.QualityDefinition +namespace TrashLib.Radarr.QualityDefinition; + +public enum RadarrQualityDefinitionType { - public enum RadarrQualityDefinitionType - { - Movie - } + Movie } diff --git a/src/TrashLib/Radarr/QualityDefinition/RadarrQualityDefinitionUpdater.cs b/src/TrashLib/Radarr/QualityDefinition/RadarrQualityDefinitionUpdater.cs index 0014ff2b..5c4eefe6 100644 --- a/src/TrashLib/Radarr/QualityDefinition/RadarrQualityDefinitionUpdater.cs +++ b/src/TrashLib/Radarr/QualityDefinition/RadarrQualityDefinitionUpdater.cs @@ -7,115 +7,114 @@ using TrashLib.Radarr.Config; using TrashLib.Radarr.QualityDefinition.Api; using TrashLib.Radarr.QualityDefinition.Api.Objects; -namespace TrashLib.Radarr.QualityDefinition +namespace TrashLib.Radarr.QualityDefinition; + +internal class RadarrQualityDefinitionUpdater : IRadarrQualityDefinitionUpdater { - internal class RadarrQualityDefinitionUpdater : IRadarrQualityDefinitionUpdater + private readonly IQualityDefinitionService _api; + private readonly IRadarrQualityDefinitionGuideParser _parser; + + public RadarrQualityDefinitionUpdater(ILogger logger, IRadarrQualityDefinitionGuideParser parser, + IQualityDefinitionService api) { - private readonly IQualityDefinitionService _api; - private readonly IRadarrQualityDefinitionGuideParser _parser; + Log = logger; + _parser = parser; + _api = api; + } - public RadarrQualityDefinitionUpdater(ILogger logger, IRadarrQualityDefinitionGuideParser parser, - IQualityDefinitionService api) - { - Log = logger; - _parser = parser; - _api = api; - } + private ILogger Log { get; } + + public async Task Process(bool isPreview, RadarrConfiguration config) + { + Log.Information("Processing Quality Definition: {QualityDefinition}", config.QualityDefinition!.Type); + var qualityDefinitions = _parser.ParseMarkdown(await _parser.GetMarkdownData()); - private ILogger Log { get; } + var selectedQuality = qualityDefinitions[config.QualityDefinition!.Type]; - public async Task Process(bool isPreview, RadarrConfiguration config) + // Fix an out of range ratio and warn the user + if (config.QualityDefinition.PreferredRatio is < 0 or > 1) { - Log.Information("Processing Quality Definition: {QualityDefinition}", config.QualityDefinition!.Type); - var qualityDefinitions = _parser.ParseMarkdown(await _parser.GetMarkdownData()); - - var selectedQuality = qualityDefinitions[config.QualityDefinition!.Type]; + var clampedRatio = Math.Clamp(config.QualityDefinition.PreferredRatio, 0, 1); + Log.Warning("Your `preferred_ratio` of {CurrentRatio} is out of range. " + + "It must be a decimal between 0.0 and 1.0. It has been clamped to {ClampedRatio}", + config.QualityDefinition.PreferredRatio, clampedRatio); - // Fix an out of range ratio and warn the user - if (config.QualityDefinition.PreferredRatio is < 0 or > 1) - { - var clampedRatio = Math.Clamp(config.QualityDefinition.PreferredRatio, 0, 1); - Log.Warning("Your `preferred_ratio` of {CurrentRatio} is out of range. " + - "It must be a decimal between 0.0 and 1.0. It has been clamped to {ClampedRatio}", - config.QualityDefinition.PreferredRatio, clampedRatio); + config.QualityDefinition.PreferredRatio = clampedRatio; + } - config.QualityDefinition.PreferredRatio = clampedRatio; - } + // Apply a calculated preferred size + foreach (var quality in selectedQuality) + { + quality.Preferred = quality.InterpolatedPreferred(config.QualityDefinition.PreferredRatio); + } - // Apply a calculated preferred size - foreach (var quality in selectedQuality) - { - quality.Preferred = quality.InterpolatedPreferred(config.QualityDefinition.PreferredRatio); - } + if (isPreview) + { + PrintQualityPreview(selectedQuality); + return; + } - if (isPreview) - { - PrintQualityPreview(selectedQuality); - return; - } + await ProcessQualityDefinition(selectedQuality); + } - await ProcessQualityDefinition(selectedQuality); - } + private static void PrintQualityPreview(IEnumerable quality) + { + Console.WriteLine(""); + const string format = "{0,-20} {1,-10} {2,-15} {3,-15}"; + Console.WriteLine(format, "Quality", "Min", "Max", "Preferred"); + Console.WriteLine(format, "-------", "---", "---", "---------"); - private static void PrintQualityPreview(IEnumerable quality) + foreach (var q in quality) { - Console.WriteLine(""); - const string format = "{0,-20} {1,-10} {2,-15} {3,-15}"; - Console.WriteLine(format, "Quality", "Min", "Max", "Preferred"); - Console.WriteLine(format, "-------", "---", "---", "---------"); + Console.WriteLine(format, q.Name, q.AnnotatedMin, q.AnnotatedMax, q.AnnotatedPreferred); + } - foreach (var q in quality) - { - Console.WriteLine(format, q.Name, q.AnnotatedMin, q.AnnotatedMax, q.AnnotatedPreferred); - } + Console.WriteLine(""); + } - Console.WriteLine(""); - } + private async Task ProcessQualityDefinition(IEnumerable guideQuality) + { + var serverQuality = await _api.GetQualityDefinition(); + await UpdateQualityDefinition(serverQuality, guideQuality); + } - private async Task ProcessQualityDefinition(IEnumerable guideQuality) + private async Task UpdateQualityDefinition(IReadOnlyCollection serverQuality, + IEnumerable guideQuality) + { + static bool QualityIsDifferent(RadarrQualityDefinitionItem a, RadarrQualityData b) { - var serverQuality = await _api.GetQualityDefinition(); - await UpdateQualityDefinition(serverQuality, guideQuality); + return b.IsMinDifferent(a.MinSize) || + b.IsMaxDifferent(a.MaxSize) || + b.IsPreferredDifferent(a.PreferredSize); } - private async Task UpdateQualityDefinition(IReadOnlyCollection serverQuality, - IEnumerable guideQuality) + var newQuality = new List(); + foreach (var qualityData in guideQuality) { - static bool QualityIsDifferent(RadarrQualityDefinitionItem a, RadarrQualityData b) + var entry = serverQuality.FirstOrDefault(q => q.Quality?.Name == qualityData.Name); + if (entry == null) { - return b.IsMinDifferent(a.MinSize) || - b.IsMaxDifferent(a.MaxSize) || - b.IsPreferredDifferent(a.PreferredSize); + Log.Warning("Server lacks quality definition for {Quality}; it will be skipped", qualityData.Name); + continue; } - var newQuality = new List(); - foreach (var qualityData in guideQuality) + if (!QualityIsDifferent(entry, qualityData)) { - var entry = serverQuality.FirstOrDefault(q => q.Quality?.Name == qualityData.Name); - if (entry == null) - { - Log.Warning("Server lacks quality definition for {Quality}; it will be skipped", qualityData.Name); - continue; - } - - if (!QualityIsDifferent(entry, qualityData)) - { - continue; - } - - // Not using the original list again, so it's OK to modify the definition reftype objects in-place. - entry.MinSize = qualityData.MinForApi; - entry.MaxSize = qualityData.MaxForApi; - entry.PreferredSize = qualityData.PreferredForApi; - newQuality.Add(entry); - - Log.Debug("Setting Quality " + - "[Name: {Name}] [Source: {Source}] [Min: {Min}] [Max: {Max}] [Preferred: {Preferred}]", - entry.Quality?.Name, entry.Quality?.Source, entry.MinSize, entry.MaxSize, entry.PreferredSize); + continue; } - await _api.UpdateQualityDefinition(newQuality); - Log.Information("Number of updated qualities: {Count}", newQuality.Count); + // Not using the original list again, so it's OK to modify the definition reftype objects in-place. + entry.MinSize = qualityData.MinForApi; + entry.MaxSize = qualityData.MaxForApi; + entry.PreferredSize = qualityData.PreferredForApi; + newQuality.Add(entry); + + Log.Debug("Setting Quality " + + "[Name: {Name}] [Source: {Source}] [Min: {Min}] [Max: {Max}] [Preferred: {Preferred}]", + entry.Quality?.Name, entry.Quality?.Source, entry.MinSize, entry.MaxSize, entry.PreferredSize); } + + await _api.UpdateQualityDefinition(newQuality); + Log.Information("Number of updated qualities: {Count}", newQuality.Count); } } diff --git a/src/TrashLib/Radarr/RadarrAutofacModule.cs b/src/TrashLib/Radarr/RadarrAutofacModule.cs index 6d601fed..23646229 100644 --- a/src/TrashLib/Radarr/RadarrAutofacModule.cs +++ b/src/TrashLib/Radarr/RadarrAutofacModule.cs @@ -11,45 +11,44 @@ using TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps; using TrashLib.Radarr.QualityDefinition; using TrashLib.Radarr.QualityDefinition.Api; -namespace TrashLib.Radarr +namespace TrashLib.Radarr; + +public class RadarrAutofacModule : Module { - public class RadarrAutofacModule : Module + protected override void Load(ContainerBuilder builder) { - protected override void Load(ContainerBuilder builder) - { - // Services - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As(); - - // Configuration - builder.RegisterType().As(); - builder.RegisterType().As(); - - // Quality Definition Support - builder.RegisterType().As(); - builder.RegisterType().As(); - - // Custom Format Support - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As(); - - // Guide Processor - - // todo: register as singleton to avoid parsing guide multiple times when using 2 or more instances in config - builder.RegisterType().As(); - builder.RegisterAggregateService(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As(); - - // Persistence Processor - builder.RegisterType().As(); - builder.RegisterAggregateService(); - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As(); - } + // Services + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + + // Configuration + builder.RegisterType().As(); + builder.RegisterType().As(); + + // Quality Definition Support + builder.RegisterType().As(); + builder.RegisterType().As(); + + // Custom Format Support + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + + // Guide Processor + + // todo: register as singleton to avoid parsing guide multiple times when using 2 or more instances in config + builder.RegisterType().As(); + builder.RegisterAggregateService(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + + // Persistence Processor + builder.RegisterType().As(); + builder.RegisterAggregateService(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); } } diff --git a/src/TrashLib/Sonarr/Api/ISonarrApi.cs b/src/TrashLib/Sonarr/Api/ISonarrApi.cs index 0eb70df7..9ed2d997 100644 --- a/src/TrashLib/Sonarr/Api/ISonarrApi.cs +++ b/src/TrashLib/Sonarr/Api/ISonarrApi.cs @@ -2,18 +2,17 @@ using System.Threading.Tasks; using TrashLib.Sonarr.Api.Objects; -namespace TrashLib.Sonarr.Api +namespace TrashLib.Sonarr.Api; + +public interface ISonarrApi { - public interface ISonarrApi - { - Task> GetTags(); - Task CreateTag(string tag); - Task> GetReleaseProfiles(); - Task UpdateReleaseProfile(SonarrReleaseProfile profileToUpdate); - Task CreateReleaseProfile(SonarrReleaseProfile newProfile); - Task> GetQualityDefinition(); + Task> GetTags(); + Task CreateTag(string tag); + Task> GetReleaseProfiles(); + Task UpdateReleaseProfile(SonarrReleaseProfile profileToUpdate); + Task CreateReleaseProfile(SonarrReleaseProfile newProfile); + Task> GetQualityDefinition(); - Task> UpdateQualityDefinition( - IReadOnlyCollection newQuality); - } + Task> UpdateQualityDefinition( + IReadOnlyCollection newQuality); } diff --git a/src/TrashLib/Sonarr/Api/ISonarrReleaseProfileCompatibilityHandler.cs b/src/TrashLib/Sonarr/Api/ISonarrReleaseProfileCompatibilityHandler.cs index 00cb77c0..e35adc39 100644 --- a/src/TrashLib/Sonarr/Api/ISonarrReleaseProfileCompatibilityHandler.cs +++ b/src/TrashLib/Sonarr/Api/ISonarrReleaseProfileCompatibilityHandler.cs @@ -2,11 +2,10 @@ using System.Threading.Tasks; using Newtonsoft.Json.Linq; using TrashLib.Sonarr.Api.Objects; -namespace TrashLib.Sonarr.Api +namespace TrashLib.Sonarr.Api; + +public interface ISonarrReleaseProfileCompatibilityHandler { - public interface ISonarrReleaseProfileCompatibilityHandler - { - Task CompatibleReleaseProfileForSendingAsync(SonarrReleaseProfile profile); - SonarrReleaseProfile CompatibleReleaseProfileForReceiving(JObject profile); - } + Task CompatibleReleaseProfileForSendingAsync(SonarrReleaseProfile profile); + SonarrReleaseProfile CompatibleReleaseProfileForReceiving(JObject profile); } diff --git a/src/TrashLib/Sonarr/Api/Mappings/SonarrApiObjectMappingProfile.cs b/src/TrashLib/Sonarr/Api/Mappings/SonarrApiObjectMappingProfile.cs index f58cfc11..7b967d32 100644 --- a/src/TrashLib/Sonarr/Api/Mappings/SonarrApiObjectMappingProfile.cs +++ b/src/TrashLib/Sonarr/Api/Mappings/SonarrApiObjectMappingProfile.cs @@ -4,24 +4,23 @@ using AutoMapper; using JetBrains.Annotations; using TrashLib.Sonarr.Api.Objects; -namespace TrashLib.Sonarr.Api.Mappings +namespace TrashLib.Sonarr.Api.Mappings; + +[UsedImplicitly] +public class SonarrApiObjectMappingProfile : Profile { - [UsedImplicitly] - public class SonarrApiObjectMappingProfile : Profile + public SonarrApiObjectMappingProfile() { - public SonarrApiObjectMappingProfile() - { - CreateMap() - .ForMember(d => d.Ignored, x => x.MapFrom( - s => s.Ignored.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList())) - .ForMember(d => d.Required, x => x.MapFrom( - s => s.Required.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList())); + CreateMap() + .ForMember(d => d.Ignored, x => x.MapFrom( + s => s.Ignored.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList())) + .ForMember(d => d.Required, x => x.MapFrom( + s => s.Required.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList())); - CreateMap() - .ForMember(d => d.Ignored, x => x.MapFrom( - s => string.Join(',', s.Ignored))) - .ForMember(d => d.Required, x => x.MapFrom( - s => string.Join(',', s.Required))); - } + CreateMap() + .ForMember(d => d.Ignored, x => x.MapFrom( + s => string.Join(',', s.Ignored))) + .ForMember(d => d.Required, x => x.MapFrom( + s => string.Join(',', s.Required))); } } diff --git a/src/TrashLib/Sonarr/Api/Objects/SonarrQualityDefinitionItem.cs b/src/TrashLib/Sonarr/Api/Objects/SonarrQualityDefinitionItem.cs index 9c1d8bb7..2a4c6291 100644 --- a/src/TrashLib/Sonarr/Api/Objects/SonarrQualityDefinitionItem.cs +++ b/src/TrashLib/Sonarr/Api/Objects/SonarrQualityDefinitionItem.cs @@ -1,24 +1,23 @@ using JetBrains.Annotations; -namespace TrashLib.Sonarr.Api.Objects +namespace TrashLib.Sonarr.Api.Objects; + +[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] +public class SonarrQualityItem { - [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] - public class SonarrQualityItem - { - public int Id { get; set; } - public string Name { get; set; } = ""; - public string Source { get; set; } = ""; - public int Resolution { get; set; } - } + public int Id { get; set; } + public string Name { get; set; } = ""; + public string Source { get; set; } = ""; + public int Resolution { get; set; } +} - [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] - public class SonarrQualityDefinitionItem - { - public int Id { get; set; } - public SonarrQualityItem? Quality { get; set; } - public string Title { get; set; } = ""; - public int Weight { get; set; } - public decimal MinSize { get; set; } - public decimal? MaxSize { get; set; } - } +[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] +public class SonarrQualityDefinitionItem +{ + public int Id { get; set; } + public SonarrQualityItem? Quality { get; set; } + public string Title { get; set; } = ""; + public int Weight { get; set; } + public decimal MinSize { get; set; } + public decimal? MaxSize { get; set; } } diff --git a/src/TrashLib/Sonarr/Api/Objects/SonarrReleaseProfile.cs b/src/TrashLib/Sonarr/Api/Objects/SonarrReleaseProfile.cs index 4cadfc84..213ed0a5 100644 --- a/src/TrashLib/Sonarr/Api/Objects/SonarrReleaseProfile.cs +++ b/src/TrashLib/Sonarr/Api/Objects/SonarrReleaseProfile.cs @@ -2,53 +2,52 @@ using JetBrains.Annotations; using Newtonsoft.Json; -namespace TrashLib.Sonarr.Api.Objects +namespace TrashLib.Sonarr.Api.Objects; + +[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Members)] +public class SonarrPreferredTerm { - [UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Members)] - public class SonarrPreferredTerm + public SonarrPreferredTerm(int score, string term) { - public SonarrPreferredTerm(int score, string term) - { - Term = term; - Score = score; - } + Term = term; + Score = score; + } - [JsonProperty("key")] - public string Term { get; set; } + [JsonProperty("key")] + public string Term { get; set; } - [JsonProperty("value")] - public int Score { get; set; } - } + [JsonProperty("value")] + public int Score { get; set; } +} - // Retained for supporting versions of Sonarr prior to v3.0.6.1355 - // Offending change is here: - // https://github.com/Sonarr/Sonarr/blob/deed85d2f9147e6180014507ef4f5af3695b0c61/src/NzbDrone.Core/Datastore/Migration/162_release_profile_to_array.cs - // The Ignored and Required JSON properties were converted from string -> array in a backward-breaking way. - [UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Members)] - public class SonarrReleaseProfileV1 - { - public int Id { get; set; } - public bool Enabled { get; set; } - public string Name { get; set; } = ""; - public string Required { get; set; } = ""; - public string Ignored { get; set; } = ""; - public IReadOnlyCollection Preferred { get; set; } = new List(); - public bool IncludePreferredWhenRenaming { get; set; } - public int IndexerId { get; set; } - public IReadOnlyCollection Tags { get; set; } = new List(); - } +// Retained for supporting versions of Sonarr prior to v3.0.6.1355 +// Offending change is here: +// https://github.com/Sonarr/Sonarr/blob/deed85d2f9147e6180014507ef4f5af3695b0c61/src/NzbDrone.Core/Datastore/Migration/162_release_profile_to_array.cs +// The Ignored and Required JSON properties were converted from string -> array in a backward-breaking way. +[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Members)] +public class SonarrReleaseProfileV1 +{ + public int Id { get; set; } + public bool Enabled { get; set; } + public string Name { get; set; } = ""; + public string Required { get; set; } = ""; + public string Ignored { get; set; } = ""; + public IReadOnlyCollection Preferred { get; set; } = new List(); + public bool IncludePreferredWhenRenaming { get; set; } + public int IndexerId { get; set; } + public IReadOnlyCollection Tags { get; set; } = new List(); +} - [UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Members)] - public class SonarrReleaseProfile - { - public int Id { get; set; } - public bool Enabled { get; set; } - public string Name { get; set; } = ""; - public IReadOnlyCollection Required { get; set; } = new List(); - public IReadOnlyCollection Ignored { get; set; } = new List(); - public IReadOnlyCollection Preferred { get; set; } = new List(); - public bool IncludePreferredWhenRenaming { get; set; } - public int IndexerId { get; set; } - public IReadOnlyCollection Tags { get; set; } = new List(); - } +[UsedImplicitly(ImplicitUseKindFlags.Assign, ImplicitUseTargetFlags.Members)] +public class SonarrReleaseProfile +{ + public int Id { get; set; } + public bool Enabled { get; set; } + public string Name { get; set; } = ""; + public IReadOnlyCollection Required { get; set; } = new List(); + public IReadOnlyCollection Ignored { get; set; } = new List(); + public IReadOnlyCollection Preferred { get; set; } = new List(); + public bool IncludePreferredWhenRenaming { get; set; } + public int IndexerId { get; set; } + public IReadOnlyCollection Tags { get; set; } = new List(); } diff --git a/src/TrashLib/Sonarr/Api/Objects/SonarrTag.cs b/src/TrashLib/Sonarr/Api/Objects/SonarrTag.cs index 27f2e596..efffe40e 100644 --- a/src/TrashLib/Sonarr/Api/Objects/SonarrTag.cs +++ b/src/TrashLib/Sonarr/Api/Objects/SonarrTag.cs @@ -1,8 +1,7 @@ -namespace TrashLib.Sonarr.Api.Objects +namespace TrashLib.Sonarr.Api.Objects; + +public class SonarrTag { - public class SonarrTag - { - public string Label { get; set; } = ""; - public int Id { get; set; } - } + public string Label { get; set; } = ""; + public int Id { get; set; } } diff --git a/src/TrashLib/Sonarr/Api/Schemas/SonarrReleaseProfileSchema.cs b/src/TrashLib/Sonarr/Api/Schemas/SonarrReleaseProfileSchema.cs index 20a471af..455ac018 100644 --- a/src/TrashLib/Sonarr/Api/Schemas/SonarrReleaseProfileSchema.cs +++ b/src/TrashLib/Sonarr/Api/Schemas/SonarrReleaseProfileSchema.cs @@ -1,8 +1,8 @@ -namespace TrashLib.Sonarr.Api.Schemas +namespace TrashLib.Sonarr.Api.Schemas; + +public static class SonarrReleaseProfileSchema { - public static class SonarrReleaseProfileSchema - { - public static string V1 => @"{ + public static string V1 => @"{ 'definitions': { 'SonarrPreferredTerm': { 'type': [ @@ -75,7 +75,7 @@ } }"; - public static string V2 => @"{ + public static string V2 => @"{ 'definitions': { 'SonarrPreferredTerm': { 'type': [ @@ -160,5 +160,4 @@ } } "; - } } diff --git a/src/TrashLib/Sonarr/Api/SonarrApi.cs b/src/TrashLib/Sonarr/Api/SonarrApi.cs index 098e921d..b2fa2831 100644 --- a/src/TrashLib/Sonarr/Api/SonarrApi.cs +++ b/src/TrashLib/Sonarr/Api/SonarrApi.cs @@ -8,85 +8,84 @@ using TrashLib.Config; using TrashLib.Extensions; using TrashLib.Sonarr.Api.Objects; -namespace TrashLib.Sonarr.Api -{ - public class SonarrApi : ISonarrApi - { - private readonly ILogger _log; - private readonly ISonarrReleaseProfileCompatibilityHandler _profileHandler; - private readonly IServerInfo _serverInfo; +namespace TrashLib.Sonarr.Api; - public SonarrApi( - IServerInfo serverInfo, - ISonarrReleaseProfileCompatibilityHandler profileHandler, - ILogger log) - { - _serverInfo = serverInfo; - _profileHandler = profileHandler; - _log = log; - } +public class SonarrApi : ISonarrApi +{ + private readonly ILogger _log; + private readonly ISonarrReleaseProfileCompatibilityHandler _profileHandler; + private readonly IServerInfo _serverInfo; - public async Task> GetTags() - { - return await BaseUrl() - .AppendPathSegment("tag") - .GetJsonAsync>(); - } + public SonarrApi( + IServerInfo serverInfo, + ISonarrReleaseProfileCompatibilityHandler profileHandler, + ILogger log) + { + _serverInfo = serverInfo; + _profileHandler = profileHandler; + _log = log; + } - public async Task CreateTag(string tag) - { - return await BaseUrl() - .AppendPathSegment("tag") - .PostJsonAsync(new {label = tag}) - .ReceiveJson(); - } + public async Task> GetTags() + { + return await BaseUrl() + .AppendPathSegment("tag") + .GetJsonAsync>(); + } - public async Task> GetReleaseProfiles() - { - var response = await BaseUrl() - .AppendPathSegment("releaseprofile") - .GetJsonAsync>(); + public async Task CreateTag(string tag) + { + return await BaseUrl() + .AppendPathSegment("tag") + .PostJsonAsync(new {label = tag}) + .ReceiveJson(); + } - return response - .Select(_profileHandler.CompatibleReleaseProfileForReceiving) - .ToList(); - } + public async Task> GetReleaseProfiles() + { + var response = await BaseUrl() + .AppendPathSegment("releaseprofile") + .GetJsonAsync>(); - public async Task UpdateReleaseProfile(SonarrReleaseProfile profileToUpdate) - { - var profileToSend = await _profileHandler.CompatibleReleaseProfileForSendingAsync(profileToUpdate); - await BaseUrl() - .AppendPathSegment($"releaseprofile/{profileToUpdate.Id}") - .PutJsonAsync(profileToSend); - } + return response + .Select(_profileHandler.CompatibleReleaseProfileForReceiving) + .ToList(); + } - public async Task CreateReleaseProfile(SonarrReleaseProfile newProfile) - { - var profileToSend = await _profileHandler.CompatibleReleaseProfileForSendingAsync(newProfile); - var response = await BaseUrl() - .AppendPathSegment("releaseprofile") - .PostJsonAsync(profileToSend) - .ReceiveJson(); + public async Task UpdateReleaseProfile(SonarrReleaseProfile profileToUpdate) + { + var profileToSend = await _profileHandler.CompatibleReleaseProfileForSendingAsync(profileToUpdate); + await BaseUrl() + .AppendPathSegment($"releaseprofile/{profileToUpdate.Id}") + .PutJsonAsync(profileToSend); + } - return _profileHandler.CompatibleReleaseProfileForReceiving(response); - } + public async Task CreateReleaseProfile(SonarrReleaseProfile newProfile) + { + var profileToSend = await _profileHandler.CompatibleReleaseProfileForSendingAsync(newProfile); + var response = await BaseUrl() + .AppendPathSegment("releaseprofile") + .PostJsonAsync(profileToSend) + .ReceiveJson(); - public async Task> GetQualityDefinition() - { - return await BaseUrl() - .AppendPathSegment("qualitydefinition") - .GetJsonAsync>(); - } + return _profileHandler.CompatibleReleaseProfileForReceiving(response); + } - public async Task> UpdateQualityDefinition( - IReadOnlyCollection newQuality) - { - return await BaseUrl() - .AppendPathSegment("qualityDefinition/update") - .PutJsonAsync(newQuality) - .ReceiveJson>(); - } + public async Task> GetQualityDefinition() + { + return await BaseUrl() + .AppendPathSegment("qualitydefinition") + .GetJsonAsync>(); + } - private IFlurlRequest BaseUrl() => _serverInfo.BuildRequest().SanitizedLogging(_log); + public async Task> UpdateQualityDefinition( + IReadOnlyCollection newQuality) + { + return await BaseUrl() + .AppendPathSegment("qualityDefinition/update") + .PutJsonAsync(newQuality) + .ReceiveJson>(); } + + private IFlurlRequest BaseUrl() => _serverInfo.BuildRequest().SanitizedLogging(_log); } diff --git a/src/TrashLib/Sonarr/Api/SonarrReleaseProfileCompatibilityHandler.cs b/src/TrashLib/Sonarr/Api/SonarrReleaseProfileCompatibilityHandler.cs index 67961ff4..051e71cc 100644 --- a/src/TrashLib/Sonarr/Api/SonarrReleaseProfileCompatibilityHandler.cs +++ b/src/TrashLib/Sonarr/Api/SonarrReleaseProfileCompatibilityHandler.cs @@ -9,52 +9,51 @@ using Serilog; using TrashLib.Sonarr.Api.Objects; using TrashLib.Sonarr.Api.Schemas; -namespace TrashLib.Sonarr.Api +namespace TrashLib.Sonarr.Api; + +public class SonarrReleaseProfileCompatibilityHandler : ISonarrReleaseProfileCompatibilityHandler { - public class SonarrReleaseProfileCompatibilityHandler : ISonarrReleaseProfileCompatibilityHandler + private readonly ISonarrCompatibility _compatibility; + private readonly IMapper _mapper; + + public SonarrReleaseProfileCompatibilityHandler( + ISonarrCompatibility compatibility, + IMapper mapper) { - private readonly ISonarrCompatibility _compatibility; - private readonly IMapper _mapper; + _compatibility = compatibility; + _mapper = mapper; + } - public SonarrReleaseProfileCompatibilityHandler( - ISonarrCompatibility compatibility, - IMapper mapper) - { - _compatibility = compatibility; - _mapper = mapper; - } + public async Task CompatibleReleaseProfileForSendingAsync(SonarrReleaseProfile profile) + { + var capabilities = await _compatibility.Capabilities.LastAsync(); + return capabilities.ArraysNeededForReleaseProfileRequiredAndIgnored + ? profile + : _mapper.Map(profile); + } + + public SonarrReleaseProfile CompatibleReleaseProfileForReceiving(JObject profile) + { + JSchema? schema; + IList? errorMessages; - public async Task CompatibleReleaseProfileForSendingAsync(SonarrReleaseProfile profile) + schema = JSchema.Parse(SonarrReleaseProfileSchema.V2); + if (profile.IsValid(schema, out errorMessages)) { - var capabilities = await _compatibility.Capabilities.LastAsync(); - return capabilities.ArraysNeededForReleaseProfileRequiredAndIgnored - ? profile - : _mapper.Map(profile); + return profile.ToObject() + ?? throw new InvalidDataException("SonarrReleaseProfile V2 parsing failed"); } - public SonarrReleaseProfile CompatibleReleaseProfileForReceiving(JObject profile) + Log.Debug("SonarrReleaseProfile is not a match for V2, proceeding to V1: {Reasons}", errorMessages); + + schema = JSchema.Parse(SonarrReleaseProfileSchema.V1); + if (profile.IsValid(schema, out errorMessages)) { - JSchema? schema; - IList? errorMessages; - - schema = JSchema.Parse(SonarrReleaseProfileSchema.V2); - if (profile.IsValid(schema, out errorMessages)) - { - return profile.ToObject() - ?? throw new InvalidDataException("SonarrReleaseProfile V2 parsing failed"); - } - - Log.Debug("SonarrReleaseProfile is not a match for V2, proceeding to V1: {Reasons}", errorMessages); - - schema = JSchema.Parse(SonarrReleaseProfileSchema.V1); - if (profile.IsValid(schema, out errorMessages)) - { - // This will throw if there's an issue during mapping. - return _mapper.Map(profile.ToObject()); - } - - throw new InvalidDataException( - $"SonarrReleaseProfile expected, but no supported schema detected: {errorMessages}"); + // This will throw if there's an issue during mapping. + return _mapper.Map(profile.ToObject()); } + + throw new InvalidDataException( + $"SonarrReleaseProfile expected, but no supported schema detected: {errorMessages}"); } } diff --git a/src/TrashLib/Sonarr/Config/ISonarrValidationMessages.cs b/src/TrashLib/Sonarr/Config/ISonarrValidationMessages.cs index d5308f53..0268f3fd 100644 --- a/src/TrashLib/Sonarr/Config/ISonarrValidationMessages.cs +++ b/src/TrashLib/Sonarr/Config/ISonarrValidationMessages.cs @@ -1,9 +1,8 @@ -namespace TrashLib.Sonarr.Config +namespace TrashLib.Sonarr.Config; + +public interface ISonarrValidationMessages { - public interface ISonarrValidationMessages - { - string BaseUrl { get; } - string ApiKey { get; } - string ReleaseProfileType { get; } - } + string BaseUrl { get; } + string ApiKey { get; } + string ReleaseProfileType { get; } } diff --git a/src/TrashLib/Sonarr/Config/SonarrConfiguration.cs b/src/TrashLib/Sonarr/Config/SonarrConfiguration.cs index c966bb82..3d827d51 100644 --- a/src/TrashLib/Sonarr/Config/SonarrConfiguration.cs +++ b/src/TrashLib/Sonarr/Config/SonarrConfiguration.cs @@ -3,29 +3,28 @@ using TrashLib.Config; using TrashLib.Sonarr.QualityDefinition; using TrashLib.Sonarr.ReleaseProfile; -namespace TrashLib.Sonarr.Config +namespace TrashLib.Sonarr.Config; + +public class SonarrConfiguration : ServiceConfiguration { - public class SonarrConfiguration : ServiceConfiguration - { - public IList ReleaseProfiles { get; set; } = new List(); - public SonarrQualityDefinitionType? QualityDefinition { get; init; } - } + public IList ReleaseProfiles { get; set; } = new List(); + public SonarrQualityDefinitionType? QualityDefinition { get; init; } +} - public class ReleaseProfileConfig - { - // -1 does not map to a valid enumerator. this is to force validation to fail if it is not set from YAML - // all of this craziness is to avoid making the enum type nullable which will make using the property - // frustrating. - public ReleaseProfileType Type { get; init; } = (ReleaseProfileType) (-1); +public class ReleaseProfileConfig +{ + // -1 does not map to a valid enumerator. this is to force validation to fail if it is not set from YAML + // all of this craziness is to avoid making the enum type nullable which will make using the property + // frustrating. + public ReleaseProfileType Type { get; init; } = (ReleaseProfileType) (-1); - public bool StrictNegativeScores { get; init; } - public SonarrProfileFilterConfig Filter { get; init; } = new(); - public ICollection Tags { get; init; } = new List(); - } + public bool StrictNegativeScores { get; init; } + public SonarrProfileFilterConfig Filter { get; init; } = new(); + public ICollection Tags { get; init; } = new List(); +} - public class SonarrProfileFilterConfig - { - public bool IncludeOptional { get; set; } - // todo: Add Include & Exclude later (list of strings) - } +public class SonarrProfileFilterConfig +{ + public bool IncludeOptional { get; set; } + // todo: Add Include & Exclude later (list of strings) } diff --git a/src/TrashLib/Sonarr/Config/SonarrConfigurationValidator.cs b/src/TrashLib/Sonarr/Config/SonarrConfigurationValidator.cs index 662e408a..32b5f52c 100644 --- a/src/TrashLib/Sonarr/Config/SonarrConfigurationValidator.cs +++ b/src/TrashLib/Sonarr/Config/SonarrConfigurationValidator.cs @@ -1,27 +1,26 @@ using FluentValidation; using JetBrains.Annotations; -namespace TrashLib.Sonarr.Config +namespace TrashLib.Sonarr.Config; + +[UsedImplicitly] +internal class SonarrConfigurationValidator : AbstractValidator { - [UsedImplicitly] - internal class SonarrConfigurationValidator : AbstractValidator + public SonarrConfigurationValidator( + ISonarrValidationMessages messages, + IValidator releaseProfileConfigValidator) { - public SonarrConfigurationValidator( - ISonarrValidationMessages messages, - IValidator releaseProfileConfigValidator) - { - RuleFor(x => x.BaseUrl).NotEmpty().WithMessage(messages.BaseUrl); - RuleFor(x => x.ApiKey).NotEmpty().WithMessage(messages.ApiKey); - RuleForEach(x => x.ReleaseProfiles).SetValidator(releaseProfileConfigValidator); - } + RuleFor(x => x.BaseUrl).NotEmpty().WithMessage(messages.BaseUrl); + RuleFor(x => x.ApiKey).NotEmpty().WithMessage(messages.ApiKey); + RuleForEach(x => x.ReleaseProfiles).SetValidator(releaseProfileConfigValidator); } +} - [UsedImplicitly] - internal class ReleaseProfileConfigValidator : AbstractValidator +[UsedImplicitly] +internal class ReleaseProfileConfigValidator : AbstractValidator +{ + public ReleaseProfileConfigValidator(ISonarrValidationMessages messages) { - public ReleaseProfileConfigValidator(ISonarrValidationMessages messages) - { - RuleFor(x => x.Type).IsInEnum().WithMessage(messages.ReleaseProfileType); - } + RuleFor(x => x.Type).IsInEnum().WithMessage(messages.ReleaseProfileType); } } diff --git a/src/TrashLib/Sonarr/Config/SonarrValidationMessages.cs b/src/TrashLib/Sonarr/Config/SonarrValidationMessages.cs index 1fd75e1f..12610550 100644 --- a/src/TrashLib/Sonarr/Config/SonarrValidationMessages.cs +++ b/src/TrashLib/Sonarr/Config/SonarrValidationMessages.cs @@ -1,17 +1,16 @@ using JetBrains.Annotations; -namespace TrashLib.Sonarr.Config +namespace TrashLib.Sonarr.Config; + +[UsedImplicitly] +internal class SonarrValidationMessages : ISonarrValidationMessages { - [UsedImplicitly] - internal class SonarrValidationMessages : ISonarrValidationMessages - { - public string BaseUrl => - "Property 'base_url' is required"; + public string BaseUrl => + "Property 'base_url' is required"; - public string ApiKey => - "Property 'api_key' is required"; + public string ApiKey => + "Property 'api_key' is required"; - public string ReleaseProfileType => - "'type' is required for 'release_profiles' elements"; - } + public string ReleaseProfileType => + "'type' is required for 'release_profiles' elements"; } diff --git a/src/TrashLib/Sonarr/ISonarrCompatibility.cs b/src/TrashLib/Sonarr/ISonarrCompatibility.cs index 96f84a1f..9bb0258f 100644 --- a/src/TrashLib/Sonarr/ISonarrCompatibility.cs +++ b/src/TrashLib/Sonarr/ISonarrCompatibility.cs @@ -1,10 +1,9 @@ using System; -namespace TrashLib.Sonarr +namespace TrashLib.Sonarr; + +public interface ISonarrCompatibility { - public interface ISonarrCompatibility - { - IObservable Capabilities { get; } - Version MinimumVersion { get; } - } + IObservable Capabilities { get; } + Version MinimumVersion { get; } } diff --git a/src/TrashLib/Sonarr/QualityDefinition/ISonarrQualityDefinitionGuideParser.cs b/src/TrashLib/Sonarr/QualityDefinition/ISonarrQualityDefinitionGuideParser.cs index 48d77eab..7ebc6ca9 100644 --- a/src/TrashLib/Sonarr/QualityDefinition/ISonarrQualityDefinitionGuideParser.cs +++ b/src/TrashLib/Sonarr/QualityDefinition/ISonarrQualityDefinitionGuideParser.cs @@ -1,11 +1,10 @@ using System.Collections.Generic; using System.Threading.Tasks; -namespace TrashLib.Sonarr.QualityDefinition +namespace TrashLib.Sonarr.QualityDefinition; + +public interface ISonarrQualityDefinitionGuideParser { - public interface ISonarrQualityDefinitionGuideParser - { - Task GetMarkdownData(); - IDictionary> ParseMarkdown(string markdown); - } + Task GetMarkdownData(); + IDictionary> ParseMarkdown(string markdown); } diff --git a/src/TrashLib/Sonarr/QualityDefinition/ISonarrQualityDefinitionUpdater.cs b/src/TrashLib/Sonarr/QualityDefinition/ISonarrQualityDefinitionUpdater.cs index cf9ed1d4..fff0d1d3 100644 --- a/src/TrashLib/Sonarr/QualityDefinition/ISonarrQualityDefinitionUpdater.cs +++ b/src/TrashLib/Sonarr/QualityDefinition/ISonarrQualityDefinitionUpdater.cs @@ -1,10 +1,9 @@ using System.Threading.Tasks; using TrashLib.Sonarr.Config; -namespace TrashLib.Sonarr.QualityDefinition +namespace TrashLib.Sonarr.QualityDefinition; + +public interface ISonarrQualityDefinitionUpdater { - public interface ISonarrQualityDefinitionUpdater - { - Task Process(bool isPreview, SonarrConfiguration config); - } + Task Process(bool isPreview, SonarrConfiguration config); } diff --git a/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityData.cs b/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityData.cs index 983ccd5a..cb753b3e 100644 --- a/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityData.cs +++ b/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityData.cs @@ -1,40 +1,39 @@ using System.Globalization; using System.Text; -namespace TrashLib.Sonarr.QualityDefinition +namespace TrashLib.Sonarr.QualityDefinition; + +public class SonarrQualityData { - public class SonarrQualityData - { - public const decimal MaxUnlimitedThreshold = 400; + public const decimal MaxUnlimitedThreshold = 400; - public string Name { get; set; } = ""; - public decimal Min { get; set; } - public decimal Max { get; set; } + public string Name { get; set; } = ""; + public decimal Min { get; set; } + public decimal Max { get; set; } - public decimal? MaxForApi => Max < MaxUnlimitedThreshold ? Max : null; - public decimal MinForApi => Min; + public decimal? MaxForApi => Max < MaxUnlimitedThreshold ? Max : null; + public decimal MinForApi => Min; - public string AnnotatedMin => Min.ToString(CultureInfo.InvariantCulture); - public string AnnotatedMax => AnnotatedValue(Max, MaxUnlimitedThreshold); + public string AnnotatedMin => Min.ToString(CultureInfo.InvariantCulture); + public string AnnotatedMax => AnnotatedValue(Max, MaxUnlimitedThreshold); - protected static string AnnotatedValue(decimal value, decimal threshold) + protected static string AnnotatedValue(decimal value, decimal threshold) + { + var builder = new StringBuilder(value.ToString(CultureInfo.InvariantCulture)); + if (value >= threshold) { - var builder = new StringBuilder(value.ToString(CultureInfo.InvariantCulture)); - if (value >= threshold) - { - builder.Append(" (Unlimited)"); - } - - return builder.ToString(); + builder.Append(" (Unlimited)"); } - public bool IsMinDifferent(decimal serviceValue) => serviceValue != Min; + return builder.ToString(); + } - public bool IsMaxDifferent(decimal? serviceValue) - { - return serviceValue == null - ? MaxUnlimitedThreshold != Max - : serviceValue != Max || MaxUnlimitedThreshold == Max; - } + public bool IsMinDifferent(decimal serviceValue) => serviceValue != Min; + + public bool IsMaxDifferent(decimal? serviceValue) + { + return serviceValue == null + ? MaxUnlimitedThreshold != Max + : serviceValue != Max || MaxUnlimitedThreshold == Max; } } diff --git a/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityDefinitionGuideParser.cs b/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityDefinitionGuideParser.cs index e02c2041..41bf481b 100644 --- a/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityDefinitionGuideParser.cs +++ b/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityDefinitionGuideParser.cs @@ -5,69 +5,68 @@ using System.Threading.Tasks; using Common.Extensions; using Flurl.Http; -namespace TrashLib.Sonarr.QualityDefinition +namespace TrashLib.Sonarr.QualityDefinition; + +internal class SonarrQualityDefinitionGuideParser : ISonarrQualityDefinitionGuideParser { - internal class SonarrQualityDefinitionGuideParser : ISonarrQualityDefinitionGuideParser - { - private readonly Regex _regexHeader = new(@"^#+", RegexOptions.Compiled); + private readonly Regex _regexHeader = new(@"^#+", RegexOptions.Compiled); - private readonly Regex _regexTableRow = - new(@"\| *(.*?) *\| *([\d.]+) *\| *([\d.]+) *\|", RegexOptions.Compiled); + private readonly Regex _regexTableRow = + new(@"\| *(.*?) *\| *([\d.]+) *\| *([\d.]+) *\|", RegexOptions.Compiled); - public async Task GetMarkdownData() - { - return await - "https://raw.githubusercontent.com/TRaSH-/Guides/master/docs/Sonarr/Sonarr-Quality-Settings-File-Size.md" - .GetStringAsync(); - } + public async Task GetMarkdownData() + { + return await + "https://raw.githubusercontent.com/TRaSH-/Guides/master/docs/Sonarr/Sonarr-Quality-Settings-File-Size.md" + .GetStringAsync(); + } - public IDictionary> ParseMarkdown(string markdown) - { - var results = new Dictionary>(); - List? table = null; + public IDictionary> ParseMarkdown(string markdown) + { + var results = new Dictionary>(); + List? table = null; - var reader = new StringReader(markdown); - for (var line = reader.ReadLine(); line != null; line = reader.ReadLine()) + var reader = new StringReader(markdown); + for (var line = reader.ReadLine(); line != null; line = reader.ReadLine()) + { + if (string.IsNullOrEmpty(line)) { - if (string.IsNullOrEmpty(line)) - { - continue; - } + continue; + } - var match = _regexHeader.Match(line); - if (match.Success) - { - var type = line.ContainsIgnoreCase("anime") - ? SonarrQualityDefinitionType.Anime - : SonarrQualityDefinitionType.Series; + var match = _regexHeader.Match(line); + if (match.Success) + { + var type = line.ContainsIgnoreCase("anime") + ? SonarrQualityDefinitionType.Anime + : SonarrQualityDefinitionType.Series; - table = results.GetOrCreate(type); + table = results.GetOrCreate(type); - // If we grab a table that isn't empty, that means for whatever reason *another* table - // in the markdown is trying to modify a previous table's data. For example, maybe there - // are two "Series" quality tables. That would be a weird edge case, but handle that - // here just in case. - if (table.Count > 0) - { - table = null; - } + // If we grab a table that isn't empty, that means for whatever reason *another* table + // in the markdown is trying to modify a previous table's data. For example, maybe there + // are two "Series" quality tables. That would be a weird edge case, but handle that + // here just in case. + if (table.Count > 0) + { + table = null; } - else if (table != null) + } + else if (table != null) + { + match = _regexTableRow.Match(line); + if (match.Success) { - match = _regexTableRow.Match(line); - if (match.Success) + table.Add(new SonarrQualityData { - table.Add(new SonarrQualityData - { - Name = match.Groups[1].Value, - Min = match.Groups[2].Value.ToDecimal(), - Max = match.Groups[3].Value.ToDecimal() - }); - } + Name = match.Groups[1].Value, + Min = match.Groups[2].Value.ToDecimal(), + Max = match.Groups[3].Value.ToDecimal() + }); } } - - return results; } + + return results; } } diff --git a/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityDefinitionType.cs b/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityDefinitionType.cs index b048289c..87514b46 100644 --- a/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityDefinitionType.cs +++ b/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityDefinitionType.cs @@ -1,9 +1,8 @@ -namespace TrashLib.Sonarr.QualityDefinition +namespace TrashLib.Sonarr.QualityDefinition; + +public enum SonarrQualityDefinitionType { - public enum SonarrQualityDefinitionType - { - Anime, - Series, - Hybrid - } + Anime, + Series, + Hybrid } diff --git a/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityDefinitionUpdater.cs b/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityDefinitionUpdater.cs index c619267d..283fe046 100644 --- a/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityDefinitionUpdater.cs +++ b/src/TrashLib/Sonarr/QualityDefinition/SonarrQualityDefinitionUpdater.cs @@ -8,146 +8,145 @@ using TrashLib.Sonarr.Api; using TrashLib.Sonarr.Api.Objects; using TrashLib.Sonarr.Config; -namespace TrashLib.Sonarr.QualityDefinition +namespace TrashLib.Sonarr.QualityDefinition; + +internal class SonarrQualityDefinitionUpdater : ISonarrQualityDefinitionUpdater { - internal class SonarrQualityDefinitionUpdater : ISonarrQualityDefinitionUpdater + private readonly ISonarrApi _api; + private readonly ISonarrQualityDefinitionGuideParser _parser; + private readonly Regex _regexHybrid = new(@"720|1080", RegexOptions.Compiled); + + public SonarrQualityDefinitionUpdater(ILogger logger, ISonarrQualityDefinitionGuideParser parser, + ISonarrApi api) { - private readonly ISonarrApi _api; - private readonly ISonarrQualityDefinitionGuideParser _parser; - private readonly Regex _regexHybrid = new(@"720|1080", RegexOptions.Compiled); + Log = logger; + _parser = parser; + _api = api; + } - public SonarrQualityDefinitionUpdater(ILogger logger, ISonarrQualityDefinitionGuideParser parser, - ISonarrApi api) + private ILogger Log { get; } + + public async Task Process(bool isPreview, SonarrConfiguration config) + { + Log.Information("Processing Quality Definition: {QualityDefinition}", config.QualityDefinition); + var qualityDefinitions = _parser.ParseMarkdown(await _parser.GetMarkdownData()); + List selectedQuality; + + if (config.QualityDefinition == SonarrQualityDefinitionType.Hybrid) { - Log = logger; - _parser = parser; - _api = api; + selectedQuality = BuildHybridQuality(qualityDefinitions[SonarrQualityDefinitionType.Anime], + qualityDefinitions[SonarrQualityDefinitionType.Series]); + } + else + { + selectedQuality = qualityDefinitions[config.QualityDefinition!.Value]; } - private ILogger Log { get; } - - public async Task Process(bool isPreview, SonarrConfiguration config) + if (isPreview) { - Log.Information("Processing Quality Definition: {QualityDefinition}", config.QualityDefinition); - var qualityDefinitions = _parser.ParseMarkdown(await _parser.GetMarkdownData()); - List selectedQuality; + PrintQualityPreview(selectedQuality); + return; + } - if (config.QualityDefinition == SonarrQualityDefinitionType.Hybrid) - { - selectedQuality = BuildHybridQuality(qualityDefinitions[SonarrQualityDefinitionType.Anime], - qualityDefinitions[SonarrQualityDefinitionType.Series]); - } - else + await ProcessQualityDefinition(selectedQuality); + } + + private List BuildHybridQuality(IReadOnlyCollection anime, + IEnumerable series) + { + // todo Verify anime & series are the same length? Probably not, because we might not care about some rows anyway. + Log.Information( + "Notice: Hybrid only functions on 720/1080 qualities and uses non-anime values for the rest (e.g. 2160)"); + + var hybrid = new List(); + foreach (var left in series) + { + // Any qualities that anime doesn't care about get immediately added from Series quality + var match = _regexHybrid.Match(left.Name); + if (!match.Success) { - selectedQuality = qualityDefinitions[config.QualityDefinition!.Value]; + Log.Debug("Using 'Series' Quality For: {QualityName}", left.Name); + hybrid.Add(left); + continue; } - if (isPreview) + // If there's a quality in Series that Anime doesn't know about, we add the Series quality + var right = anime.FirstOrDefault(row => row.Name == left.Name); + if (right == null) { - PrintQualityPreview(selectedQuality); - return; + Log.Error("Could not find matching anime quality for series quality named {QualityName}", + left.Name); + hybrid.Add(left); + continue; } - await ProcessQualityDefinition(selectedQuality); + hybrid.Add(new SonarrQualityData + { + Name = left.Name, + Min = Math.Min(left.Min, right.Min), + Max = Math.Max(left.Max, right.Max) + }); } - private List BuildHybridQuality(IReadOnlyCollection anime, - IEnumerable series) - { - // todo Verify anime & series are the same length? Probably not, because we might not care about some rows anyway. - Log.Information( - "Notice: Hybrid only functions on 720/1080 qualities and uses non-anime values for the rest (e.g. 2160)"); - - var hybrid = new List(); - foreach (var left in series) - { - // Any qualities that anime doesn't care about get immediately added from Series quality - var match = _regexHybrid.Match(left.Name); - if (!match.Success) - { - Log.Debug("Using 'Series' Quality For: {QualityName}", left.Name); - hybrid.Add(left); - continue; - } - - // If there's a quality in Series that Anime doesn't know about, we add the Series quality - var right = anime.FirstOrDefault(row => row.Name == left.Name); - if (right == null) - { - Log.Error("Could not find matching anime quality for series quality named {QualityName}", - left.Name); - hybrid.Add(left); - continue; - } - - hybrid.Add(new SonarrQualityData - { - Name = left.Name, - Min = Math.Min(left.Min, right.Min), - Max = Math.Max(left.Max, right.Max) - }); - } + return hybrid; + } - return hybrid; - } + private static void PrintQualityPreview(IEnumerable quality) + { + Console.WriteLine(""); + const string format = "{0,-20} {1,-10} {2,-15}"; + Console.WriteLine(format, "Quality", "Min", "Max"); + Console.WriteLine(format, "-------", "---", "---"); - private static void PrintQualityPreview(IEnumerable quality) + foreach (var q in quality) { - Console.WriteLine(""); - const string format = "{0,-20} {1,-10} {2,-15}"; - Console.WriteLine(format, "Quality", "Min", "Max"); - Console.WriteLine(format, "-------", "---", "---"); + Console.WriteLine(format, q.Name, q.AnnotatedMin, q.AnnotatedMax); + } - foreach (var q in quality) - { - Console.WriteLine(format, q.Name, q.AnnotatedMin, q.AnnotatedMax); - } + Console.WriteLine(""); + } - Console.WriteLine(""); - } + private async Task ProcessQualityDefinition(IEnumerable guideQuality) + { + var serverQuality = await _api.GetQualityDefinition(); + await UpdateQualityDefinition(serverQuality, guideQuality); + } - private async Task ProcessQualityDefinition(IEnumerable guideQuality) + private async Task UpdateQualityDefinition(IReadOnlyCollection serverQuality, + IEnumerable guideQuality) + { + static bool QualityIsDifferent(SonarrQualityDefinitionItem a, SonarrQualityData b) { - var serverQuality = await _api.GetQualityDefinition(); - await UpdateQualityDefinition(serverQuality, guideQuality); + return b.IsMinDifferent(a.MinSize) || + b.IsMaxDifferent(a.MaxSize); } - private async Task UpdateQualityDefinition(IReadOnlyCollection serverQuality, - IEnumerable guideQuality) + var newQuality = new List(); + foreach (var qualityData in guideQuality) { - static bool QualityIsDifferent(SonarrQualityDefinitionItem a, SonarrQualityData b) + var entry = serverQuality.FirstOrDefault(q => q.Quality?.Name == qualityData.Name); + if (entry == null) { - return b.IsMinDifferent(a.MinSize) || - b.IsMaxDifferent(a.MaxSize); + Log.Warning("Server lacks quality definition for {Quality}; it will be skipped", qualityData.Name); + continue; } - var newQuality = new List(); - foreach (var qualityData in guideQuality) + if (!QualityIsDifferent(entry, qualityData)) { - var entry = serverQuality.FirstOrDefault(q => q.Quality?.Name == qualityData.Name); - if (entry == null) - { - Log.Warning("Server lacks quality definition for {Quality}; it will be skipped", qualityData.Name); - continue; - } - - if (!QualityIsDifferent(entry, qualityData)) - { - continue; - } - - // Not using the original list again, so it's OK to modify the definition reftype objects in-place. - entry.MinSize = qualityData.MinForApi; - entry.MaxSize = qualityData.MaxForApi; - newQuality.Add(entry); - - Log.Debug("Setting Quality " + - "[Name: {Name}] [Source: {Source}] [Min: {Min}] [Max: {Max}]", - entry.Quality?.Name, entry.Quality?.Source, entry.MinSize, entry.MaxSize); + continue; } - await _api.UpdateQualityDefinition(newQuality); - Log.Information("Number of updated qualities: {Count}", newQuality.Count); + // Not using the original list again, so it's OK to modify the definition reftype objects in-place. + entry.MinSize = qualityData.MinForApi; + entry.MaxSize = qualityData.MaxForApi; + newQuality.Add(entry); + + Log.Debug("Setting Quality " + + "[Name: {Name}] [Source: {Source}] [Min: {Min}] [Max: {Max}]", + entry.Quality?.Name, entry.Quality?.Source, entry.MinSize, entry.MaxSize); } + + await _api.UpdateQualityDefinition(newQuality); + Log.Information("Number of updated qualities: {Count}", newQuality.Count); } } diff --git a/src/TrashLib/Sonarr/ReleaseProfile/FilteredProfileData.cs b/src/TrashLib/Sonarr/ReleaseProfile/FilteredProfileData.cs index 7738bb61..838961ab 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/FilteredProfileData.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/FilteredProfileData.cs @@ -2,34 +2,33 @@ using System.Linq; using TrashLib.Sonarr.Config; -namespace TrashLib.Sonarr.ReleaseProfile +namespace TrashLib.Sonarr.ReleaseProfile; + +public class FilteredProfileData { - public class FilteredProfileData - { - private readonly ReleaseProfileConfig _config; - private readonly ProfileData _profileData; + private readonly ReleaseProfileConfig _config; + private readonly ProfileData _profileData; - public FilteredProfileData(ProfileData profileData, ReleaseProfileConfig config) - { - _profileData = profileData; - _config = config; - } + public FilteredProfileData(ProfileData profileData, ReleaseProfileConfig config) + { + _profileData = profileData; + _config = config; + } - public IEnumerable Required => _config.Filter.IncludeOptional - ? _profileData.Required.Concat(_profileData.Optional.Required).ToList() - : _profileData.Required; + public IEnumerable Required => _config.Filter.IncludeOptional + ? _profileData.Required.Concat(_profileData.Optional.Required).ToList() + : _profileData.Required; - public IEnumerable Ignored => _config.Filter.IncludeOptional - ? _profileData.Ignored.Concat(_profileData.Optional.Ignored).ToList() - : _profileData.Ignored; + public IEnumerable Ignored => _config.Filter.IncludeOptional + ? _profileData.Ignored.Concat(_profileData.Optional.Ignored).ToList() + : _profileData.Ignored; - public IDictionary> Preferred => _config.Filter.IncludeOptional - ? _profileData.Preferred - .Union(_profileData.Optional.Preferred) - .GroupBy(kvp => kvp.Key) - .ToDictionary(grp => grp.Key, grp => new List(grp.SelectMany(l => l.Value))) - : _profileData.Preferred; + public IDictionary> Preferred => _config.Filter.IncludeOptional + ? _profileData.Preferred + .Union(_profileData.Optional.Preferred) + .GroupBy(kvp => kvp.Key) + .ToDictionary(grp => grp.Key, grp => new List(grp.SelectMany(l => l.Value))) + : _profileData.Preferred; - public bool? IncludePreferredWhenRenaming => _profileData.IncludePreferredWhenRenaming; - } + public bool? IncludePreferredWhenRenaming => _profileData.IncludePreferredWhenRenaming; } diff --git a/src/TrashLib/Sonarr/ReleaseProfile/IReleaseProfileGuideParser.cs b/src/TrashLib/Sonarr/ReleaseProfile/IReleaseProfileGuideParser.cs index 8b806013..1b65fd39 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/IReleaseProfileGuideParser.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/IReleaseProfileGuideParser.cs @@ -2,11 +2,10 @@ using System.Threading.Tasks; using TrashLib.Sonarr.Config; -namespace TrashLib.Sonarr.ReleaseProfile +namespace TrashLib.Sonarr.ReleaseProfile; + +public interface IReleaseProfileGuideParser { - public interface IReleaseProfileGuideParser - { - Task GetMarkdownData(ReleaseProfileType profileName); - IDictionary ParseMarkdown(ReleaseProfileConfig config, string markdown); - } + Task GetMarkdownData(ReleaseProfileType profileName); + IDictionary ParseMarkdown(ReleaseProfileConfig config, string markdown); } diff --git a/src/TrashLib/Sonarr/ReleaseProfile/IReleaseProfileUpdater.cs b/src/TrashLib/Sonarr/ReleaseProfile/IReleaseProfileUpdater.cs index 01fb05d9..cd1ab2dd 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/IReleaseProfileUpdater.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/IReleaseProfileUpdater.cs @@ -1,10 +1,9 @@ using System.Threading.Tasks; using TrashLib.Sonarr.Config; -namespace TrashLib.Sonarr.ReleaseProfile +namespace TrashLib.Sonarr.ReleaseProfile; + +public interface IReleaseProfileUpdater { - public interface IReleaseProfileUpdater - { - Task Process(bool isPreview, SonarrConfiguration config); - } + Task Process(bool isPreview, SonarrConfiguration config); } diff --git a/src/TrashLib/Sonarr/ReleaseProfile/ParserState.cs b/src/TrashLib/Sonarr/ReleaseProfile/ParserState.cs index 42c2ab72..b28cd7e4 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/ParserState.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/ParserState.cs @@ -3,78 +3,77 @@ using System.Collections.Generic; using Common.Extensions; using Serilog; -namespace TrashLib.Sonarr.ReleaseProfile +namespace TrashLib.Sonarr.ReleaseProfile; + +public enum TermCategory { - public enum TermCategory - { - Required, - Ignored, - Preferred - } + Required, + Ignored, + Preferred +} - public class ParserState +public class ParserState +{ + public ParserState(ILogger logger) { - public ParserState(ILogger logger) - { - Log = logger; - ResetParserState(); - } + Log = logger; + ResetParserState(); + } - private ILogger Log { get; } - public string? ProfileName { get; set; } - public int? Score { get; set; } - public ScopedState CurrentCategory { get; } = new(); - public bool InsideCodeBlock { get; set; } - public int ProfileHeaderDepth { get; set; } - public int CurrentHeaderDepth { get; set; } - public int LineNumber { get; set; } - public IDictionary Results { get; } = new Dictionary(); + private ILogger Log { get; } + public string? ProfileName { get; set; } + public int? Score { get; set; } + public ScopedState CurrentCategory { get; } = new(); + public bool InsideCodeBlock { get; set; } + public int ProfileHeaderDepth { get; set; } + public int CurrentHeaderDepth { get; set; } + public int LineNumber { get; set; } + public IDictionary Results { get; } = new Dictionary(); - // If null, then terms are not considered optional - public ScopedState TermsAreOptional { get; } = new(); + // If null, then terms are not considered optional + public ScopedState TermsAreOptional { get; } = new(); - public bool IsValid => ProfileName != null && CurrentCategory.Value != null && - // If category is preferred, we also require a score - (CurrentCategory.Value != TermCategory.Preferred || Score != null); + public bool IsValid => ProfileName != null && CurrentCategory.Value != null && + // If category is preferred, we also require a score + (CurrentCategory.Value != TermCategory.Preferred || Score != null); - public ICollection IgnoredTerms - => TermsAreOptional.Value ? GetProfile().Optional.Ignored : GetProfile().Ignored; + public ICollection IgnoredTerms + => TermsAreOptional.Value ? GetProfile().Optional.Ignored : GetProfile().Ignored; - public ICollection RequiredTerms - => TermsAreOptional.Value ? GetProfile().Optional.Required : GetProfile().Required; + public ICollection RequiredTerms + => TermsAreOptional.Value ? GetProfile().Optional.Required : GetProfile().Required; - public IDictionary> PreferredTerms - => TermsAreOptional.Value ? GetProfile().Optional.Preferred : GetProfile().Preferred; + public IDictionary> PreferredTerms + => TermsAreOptional.Value ? GetProfile().Optional.Preferred : GetProfile().Preferred; - public ProfileData GetProfile() + public ProfileData GetProfile() + { + if (ProfileName == null) { - if (ProfileName == null) - { - throw new NullReferenceException(); - } - - return Results.GetOrCreate(ProfileName); + throw new NullReferenceException(); } - public void ResetParserState() + return Results.GetOrCreate(ProfileName); + } + + public void ResetParserState() + { + ProfileName = null; + Score = null; + InsideCodeBlock = false; + ProfileHeaderDepth = -1; + } + + public void ResetScopeState(int scope) + { + if (CurrentCategory.Reset(scope)) { - ProfileName = null; - Score = null; - InsideCodeBlock = false; - ProfileHeaderDepth = -1; + Log.Debug(" - Reset Category State for Scope: {Scope}", scope); } - public void ResetScopeState(int scope) + if (TermsAreOptional.Reset(scope)) { - if (CurrentCategory.Reset(scope)) - { - Log.Debug(" - Reset Category State for Scope: {Scope}", scope); - } - - if (TermsAreOptional.Reset(scope)) - { - Log.Debug(" - Reset Optional State for Scope: {Scope}", scope); - } + Log.Debug(" - Reset Optional State for Scope: {Scope}", scope); } } } diff --git a/src/TrashLib/Sonarr/ReleaseProfile/ProfileData.cs b/src/TrashLib/Sonarr/ReleaseProfile/ProfileData.cs index 0ec2c6cd..9f637cd0 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/ProfileData.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/ProfileData.cs @@ -1,26 +1,25 @@ using System.Collections.Generic; -namespace TrashLib.Sonarr.ReleaseProfile +namespace TrashLib.Sonarr.ReleaseProfile; + +public class ProfileDataOptional { - public class ProfileDataOptional - { - public ICollection Required { get; init; } = new List(); - public ICollection Ignored { get; init; } = new List(); - public IDictionary> Preferred { get; init; } = new Dictionary>(); - } + public ICollection Required { get; init; } = new List(); + public ICollection Ignored { get; init; } = new List(); + public IDictionary> Preferred { get; init; } = new Dictionary>(); +} - public class ProfileData - { - public ICollection Required { get; init; } = new List(); - public ICollection Ignored { get; init; } = new List(); - public IDictionary> Preferred { get; init; } = new Dictionary>(); +public class ProfileData +{ + public ICollection Required { get; init; } = new List(); + public ICollection Ignored { get; init; } = new List(); + public IDictionary> Preferred { get; init; } = new Dictionary>(); - // We use 'null' here to represent no explicit mention of the "include preferred" string - // found in the markdown. We use this to control whether or not the corresponding profile - // section gets printed in the first place, or if we modify the existing setting for - // existing profiles on the server. - public bool? IncludePreferredWhenRenaming { get; set; } + // We use 'null' here to represent no explicit mention of the "include preferred" string + // found in the markdown. We use this to control whether or not the corresponding profile + // section gets printed in the first place, or if we modify the existing setting for + // existing profiles on the server. + public bool? IncludePreferredWhenRenaming { get; set; } - public ProfileDataOptional Optional { get; init; } = new(); - } + public ProfileDataOptional Optional { get; init; } = new(); } diff --git a/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileGuideParser.cs b/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileGuideParser.cs index adab29a6..9a9927c7 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileGuideParser.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileGuideParser.cs @@ -9,325 +9,324 @@ using Flurl.Http; using Serilog; using TrashLib.Sonarr.Config; -namespace TrashLib.Sonarr.ReleaseProfile +namespace TrashLib.Sonarr.ReleaseProfile; + +internal class ReleaseProfileGuideParser : IReleaseProfileGuideParser { - internal class ReleaseProfileGuideParser : IReleaseProfileGuideParser + private readonly Dictionary _markdownDocNames = new() { - private readonly Dictionary _markdownDocNames = new() - { - {ReleaseProfileType.Anime, "Sonarr-Release-Profile-RegEx-Anime"}, - {ReleaseProfileType.Series, "Sonarr-Release-Profile-RegEx"} - }; + {ReleaseProfileType.Anime, "Sonarr-Release-Profile-RegEx-Anime"}, + {ReleaseProfileType.Series, "Sonarr-Release-Profile-RegEx"} + }; - private readonly (TermCategory, Regex)[] _regexCategories = - { - (TermCategory.Required, BuildRegex(@"must contain")), - (TermCategory.Ignored, BuildRegex(@"must not contain")), - (TermCategory.Preferred, BuildRegex(@"preferred")) - }; + private readonly (TermCategory, Regex)[] _regexCategories = + { + (TermCategory.Required, BuildRegex(@"must contain")), + (TermCategory.Ignored, BuildRegex(@"must not contain")), + (TermCategory.Preferred, BuildRegex(@"preferred")) + }; - private readonly Regex _regexHeader = new(@"^(#+)\s(.+?)\s*$", RegexOptions.Compiled); - private readonly Regex _regexHeaderReleaseProfile = BuildRegex(@"release profile"); - private readonly Regex _regexPotentialScore = BuildRegex(@"\[(-?[\d]+)\]"); - private readonly Regex _regexScore = BuildRegex(@"score.*?\[(-?[\d]+)\]"); + private readonly Regex _regexHeader = new(@"^(#+)\s(.+?)\s*$", RegexOptions.Compiled); + private readonly Regex _regexHeaderReleaseProfile = BuildRegex(@"release profile"); + private readonly Regex _regexPotentialScore = BuildRegex(@"\[(-?[\d]+)\]"); + private readonly Regex _regexScore = BuildRegex(@"score.*?\[(-?[\d]+)\]"); - public ReleaseProfileGuideParser(ILogger logger) - { - Log = logger; - } + public ReleaseProfileGuideParser(ILogger logger) + { + Log = logger; + } - private ILogger Log { get; } + private ILogger Log { get; } - public async Task GetMarkdownData(ReleaseProfileType profileName) - { - return await BuildUrl(profileName).GetStringAsync(); - } + public async Task GetMarkdownData(ReleaseProfileType profileName) + { + return await BuildUrl(profileName).GetStringAsync(); + } - public IDictionary ParseMarkdown(ReleaseProfileConfig config, string markdown) - { - var state = new ParserState(Log); + public IDictionary ParseMarkdown(ReleaseProfileConfig config, string markdown) + { + var state = new ParserState(Log); - var reader = new StringReader(markdown); - for (var line = reader.ReadLine(); line != null; line = reader.ReadLine()) + var reader = new StringReader(markdown); + for (var line = reader.ReadLine(); line != null; line = reader.ReadLine()) + { + state.LineNumber++; + if (string.IsNullOrEmpty(line)) { - state.LineNumber++; - if (string.IsNullOrEmpty(line)) - { - continue; - } + continue; + } - // Always check if we're starting a fenced code block. Whether we are inside one or not greatly affects - // the logic we use. - if (line.StartsWith("```")) - { - state.InsideCodeBlock = !state.InsideCodeBlock; - continue; - } + // Always check if we're starting a fenced code block. Whether we are inside one or not greatly affects + // the logic we use. + if (line.StartsWith("```")) + { + state.InsideCodeBlock = !state.InsideCodeBlock; + continue; + } - // Not inside brackets - if (!state.InsideCodeBlock) + // Not inside brackets + if (!state.InsideCodeBlock) + { + OutsideFence_ParseMarkdown(line, state); + } + // Inside brackets + else + { + if (!state.IsValid) { - OutsideFence_ParseMarkdown(line, state); + Log.Debug(" - !! Inside bracket with invalid state; skipping! " + + "[Profile Name: {ProfileName}] " + + "[Category: {Category}] " + "[Score: {Score}] " + "[Line: {Line}] ", + state.ProfileName, + state.CurrentCategory.Value, state.Score, line); } - // Inside brackets else { - if (!state.IsValid) - { - Log.Debug(" - !! Inside bracket with invalid state; skipping! " + - "[Profile Name: {ProfileName}] " + - "[Category: {Category}] " + "[Score: {Score}] " + "[Line: {Line}] ", - state.ProfileName, - state.CurrentCategory.Value, state.Score, line); - } - else - { - InsideFence_ParseMarkdown(config, line, state); - } + InsideFence_ParseMarkdown(config, line, state); } } - - Log.Debug("\n"); - return state.Results; } - private bool IsSkippableLine(string line) - { - // Skip lines with leading whitespace (i.e. indentation). - // These lines will almost always be `!!! attention` blocks of some kind and won't contain useful data. - if (char.IsWhiteSpace(line, 0)) - { - Log.Debug(" - Skip Indented Line: {Line}", line); - return true; - } - - // Lines that begin with `???` or `!!!` are admonition syntax (extended markdown supported by Python) - if (line.StartsWith("!!!") || line.StartsWith("???")) - { - Log.Debug(" - Skip Admonition: {Line}", line); - return true; - } - - return false; - } + Log.Debug("\n"); + return state.Results; + } - private static Regex BuildRegex(string regex) + private bool IsSkippableLine(string line) + { + // Skip lines with leading whitespace (i.e. indentation). + // These lines will almost always be `!!! attention` blocks of some kind and won't contain useful data. + if (char.IsWhiteSpace(line, 0)) { - return new Regex(regex, RegexOptions.Compiled | RegexOptions.IgnoreCase); + Log.Debug(" - Skip Indented Line: {Line}", line); + return true; } - private Url BuildUrl(ReleaseProfileType profileName) + // Lines that begin with `???` or `!!!` are admonition syntax (extended markdown supported by Python) + if (line.StartsWith("!!!") || line.StartsWith("???")) { - return "https://raw.githubusercontent.com/TRaSH-/Guides/master/docs/Sonarr".AppendPathSegment( - $"{_markdownDocNames[profileName]}.md"); + Log.Debug(" - Skip Admonition: {Line}", line); + return true; } - private void InsideFence_ParseMarkdown(ReleaseProfileConfig config, string line, ParserState state) - { - // Sometimes a comma is present at the end of these lines, because when it's - // pasted into Sonarr it acts as a delimiter. However, when using them with the - // API we do not need them. - line = line.TrimEnd(','); + return false; + } - var category = state.CurrentCategory.Value; - switch (category!.Value) - { - case TermCategory.Preferred: - { - Log.Debug(" + Capture Term " + - "[Category: {CurrentCategory}] " + - "[Optional: {Optional}] " + - "[Score: {Score}] " + - "[Strict: {StrictNegativeScores}] " + - "[Term: {Line}]", - category.Value, state.TermsAreOptional.Value, state.Score, config.StrictNegativeScores, line); - - if (config.StrictNegativeScores && state.Score < 0) - { - state.IgnoredTerms.Add(line); - } - else - { - // Score is already checked for null prior to the method being invoked. - var prefList = state.PreferredTerms.GetOrCreate(state.Score!.Value); - prefList.Add(line); - } - - break; - } + private static Regex BuildRegex(string regex) + { + return new Regex(regex, RegexOptions.Compiled | RegexOptions.IgnoreCase); + } + + private Url BuildUrl(ReleaseProfileType profileName) + { + return "https://raw.githubusercontent.com/TRaSH-/Guides/master/docs/Sonarr".AppendPathSegment( + $"{_markdownDocNames[profileName]}.md"); + } - case TermCategory.Ignored: + private void InsideFence_ParseMarkdown(ReleaseProfileConfig config, string line, ParserState state) + { + // Sometimes a comma is present at the end of these lines, because when it's + // pasted into Sonarr it acts as a delimiter. However, when using them with the + // API we do not need them. + line = line.TrimEnd(','); + + var category = state.CurrentCategory.Value; + switch (category!.Value) + { + case TermCategory.Preferred: + { + Log.Debug(" + Capture Term " + + "[Category: {CurrentCategory}] " + + "[Optional: {Optional}] " + + "[Score: {Score}] " + + "[Strict: {StrictNegativeScores}] " + + "[Term: {Line}]", + category.Value, state.TermsAreOptional.Value, state.Score, config.StrictNegativeScores, line); + + if (config.StrictNegativeScores && state.Score < 0) { state.IgnoredTerms.Add(line); - Log.Debug(" + Capture Term " + - "[Category: {Category}] " + - "[Optional: {Optional}] " + - "[Term: {Line}]", - category.Value, state.TermsAreOptional.Value, line); - break; } - - case TermCategory.Required: + else { - state.RequiredTerms.Add(line); - Log.Debug(" + Capture Term " + - "[Category: {Category}] " + - "[Optional: {Optional}] " + - "[Term: {Line}]", - category.Value, state.TermsAreOptional.Value, line); - break; + // Score is already checked for null prior to the method being invoked. + var prefList = state.PreferredTerms.GetOrCreate(state.Score!.Value); + prefList.Add(line); } - default: - { - throw new ArgumentOutOfRangeException($"Unknown term category: {category.Value}"); - } + break; } - } - private void OutsideFence_ParseMarkdown(string line, ParserState state) - { - // ReSharper disable once InlineOutVariableDeclaration - Match match; - - // Header Processing. Never do any additional processing to headers, so return after processing it - if (_regexHeader.Match(line, out match)) + case TermCategory.Ignored: { - OutsideFence_ParseHeader(state, match); - return; + state.IgnoredTerms.Add(line); + Log.Debug(" + Capture Term " + + "[Category: {Category}] " + + "[Optional: {Optional}] " + + "[Term: {Line}]", + category.Value, state.TermsAreOptional.Value, line); + break; } - // Until we find a header that defines a profile, we don't care about anything under it. - if (string.IsNullOrEmpty(state.ProfileName)) + case TermCategory.Required: { - return; + state.RequiredTerms.Add(line); + Log.Debug(" + Capture Term " + + "[Category: {Category}] " + + "[Optional: {Optional}] " + + "[Term: {Line}]", + category.Value, state.TermsAreOptional.Value, line); + break; } - // These are often found in admonition (indented) blocks, so we check for it before we - // run the IsSkippableLine() check. - if (line.ContainsIgnoreCase("include preferred")) + default: { - state.GetProfile().IncludePreferredWhenRenaming = !line.ContainsIgnoreCase("not"); - Log.Debug(" - 'Include Preferred' found [Value: {IncludePreferredWhenRenaming}] [Line: {Line}]", - state.GetProfile().IncludePreferredWhenRenaming, line); - return; + throw new ArgumentOutOfRangeException($"Unknown term category: {category.Value}"); } + } + } - if (IsSkippableLine(line)) - { - return; - } + private void OutsideFence_ParseMarkdown(string line, ParserState state) + { + // ReSharper disable once InlineOutVariableDeclaration + Match match; - OutsideFence_ParseInformationOnSameLine(line, state); + // Header Processing. Never do any additional processing to headers, so return after processing it + if (_regexHeader.Match(line, out match)) + { + OutsideFence_ParseHeader(state, match); + return; } - private void OutsideFence_ParseHeader(ParserState state, Match match) + // Until we find a header that defines a profile, we don't care about anything under it. + if (string.IsNullOrEmpty(state.ProfileName)) { - var headerDepth = match.Groups[1].Length; - var headerText = match.Groups[2].Value; - state.CurrentHeaderDepth = headerDepth; + return; + } - // Always reset the scope-based state any time we see a header, regardless of depth or phrasing. - // Each header "resets" scope-based state, even if it's entering into a nested header, which usually will - // not reset as much state. - state.ResetScopeState(headerDepth); + // These are often found in admonition (indented) blocks, so we check for it before we + // run the IsSkippableLine() check. + if (line.ContainsIgnoreCase("include preferred")) + { + state.GetProfile().IncludePreferredWhenRenaming = !line.ContainsIgnoreCase("not"); + Log.Debug(" - 'Include Preferred' found [Value: {IncludePreferredWhenRenaming}] [Line: {Line}]", + state.GetProfile().IncludePreferredWhenRenaming, line); + return; + } - Log.Debug("> Parsing Header [Nested: {Nested}] [Depth: {HeaderDepth}] [Text: {HeaderText}]", - headerDepth > state.ProfileHeaderDepth, headerDepth, headerText); + if (IsSkippableLine(line)) + { + return; + } - // Profile name (always reset previous state here) - if (_regexHeaderReleaseProfile.Match(headerText).Success) - { - state.ResetParserState(); - state.ProfileName = headerText; - state.ProfileHeaderDepth = headerDepth; - Log.Debug(" - New Profile [Text: {HeaderText}]", headerText); - } - else if (headerDepth <= state.ProfileHeaderDepth) - { - Log.Debug(" - !! Non-nested, non-profile header found; resetting all state"); - state.ResetParserState(); - } + OutsideFence_ParseInformationOnSameLine(line, state); + } - // If a single header can be parsed with multiple phrases, add more if conditions below this comment. - // In order to make sure all checks happen as needed, do not return from the condition (to allow conditions - // below it to be executed) + private void OutsideFence_ParseHeader(ParserState state, Match match) + { + var headerDepth = match.Groups[1].Length; + var headerText = match.Groups[2].Value; + state.CurrentHeaderDepth = headerDepth; - // Another note: Any "state" set by headers has longer lasting effects. That state will remain in effect - // until the next header. That means multiple fenced code blocks will be impacted. + // Always reset the scope-based state any time we see a header, regardless of depth or phrasing. + // Each header "resets" scope-based state, even if it's entering into a nested header, which usually will + // not reset as much state. + state.ResetScopeState(headerDepth); - ParseAndSetOptional(headerText, state); - ParseAndSetCategory(headerText, state); - } + Log.Debug("> Parsing Header [Nested: {Nested}] [Depth: {HeaderDepth}] [Text: {HeaderText}]", + headerDepth > state.ProfileHeaderDepth, headerDepth, headerText); - private void OutsideFence_ParseInformationOnSameLine(string line, ParserState state) + // Profile name (always reset previous state here) + if (_regexHeaderReleaseProfile.Match(headerText).Success) + { + state.ResetParserState(); + state.ProfileName = headerText; + state.ProfileHeaderDepth = headerDepth; + Log.Debug(" - New Profile [Text: {HeaderText}]", headerText); + } + else if (headerDepth <= state.ProfileHeaderDepth) { - // ReSharper disable once InlineOutVariableDeclaration - Match match; + Log.Debug(" - !! Non-nested, non-profile header found; resetting all state"); + state.ResetParserState(); + } - ParseAndSetOptional(line, state); - ParseAndSetCategory(line, state); + // If a single header can be parsed with multiple phrases, add more if conditions below this comment. + // In order to make sure all checks happen as needed, do not return from the condition (to allow conditions + // below it to be executed) - if (_regexScore.Match(line, out match)) - { - // As a convenience, if we find a score, we obviously should set the category to Preferred even if - // the guide didn't explicitly mention that. - state.CurrentCategory.PushValue(TermCategory.Preferred, state.CurrentHeaderDepth); + // Another note: Any "state" set by headers has longer lasting effects. That state will remain in effect + // until the next header. That means multiple fenced code blocks will be impacted. - state.Score = int.Parse(match.Groups[1].Value); - Log.Debug(" - Score [Value: {Score}]", state.Score); - } - else if (_regexPotentialScore.Match(line, out match)) - { - Log.Warning("Found a potential score on line #{Line} that will be ignored because the " + - "word 'score' is missing (This is probably a bug in the guide itself): {ScoreMatch}", - state.LineNumber, match.Groups[0].Value); - } + ParseAndSetOptional(headerText, state); + ParseAndSetCategory(headerText, state); + } + + private void OutsideFence_ParseInformationOnSameLine(string line, ParserState state) + { + // ReSharper disable once InlineOutVariableDeclaration + Match match; + + ParseAndSetOptional(line, state); + ParseAndSetCategory(line, state); + + if (_regexScore.Match(line, out match)) + { + // As a convenience, if we find a score, we obviously should set the category to Preferred even if + // the guide didn't explicitly mention that. + state.CurrentCategory.PushValue(TermCategory.Preferred, state.CurrentHeaderDepth); + + state.Score = int.Parse(match.Groups[1].Value); + Log.Debug(" - Score [Value: {Score}]", state.Score); + } + else if (_regexPotentialScore.Match(line, out match)) + { + Log.Warning("Found a potential score on line #{Line} that will be ignored because the " + + "word 'score' is missing (This is probably a bug in the guide itself): {ScoreMatch}", + state.LineNumber, match.Groups[0].Value); } + } - private void ParseAndSetCategory(string line, ParserState state) + private void ParseAndSetCategory(string line, ParserState state) + { + var category = ParseCategory(line); + if (category == null) { - var category = ParseCategory(line); - if (category == null) - { - return; - } + return; + } + + state.CurrentCategory.PushValue(category.Value, state.CurrentHeaderDepth); - state.CurrentCategory.PushValue(category.Value, state.CurrentHeaderDepth); + Log.Debug(" - Category Set " + + "[Scope: {Scope}] " + + "[Name: {Category}] " + + "[Stack Size: {StackSize}] " + + "[Line: {Line}]", + category.Value, state.CurrentHeaderDepth, state.CurrentCategory.StackSize, line); + } - Log.Debug(" - Category Set " + + private void ParseAndSetOptional(string line, ParserState state) + { + if (line.ContainsIgnoreCase("optional")) + { + state.TermsAreOptional.PushValue(true, state.CurrentHeaderDepth); + + Log.Debug(" - Optional Set " + "[Scope: {Scope}] " + - "[Name: {Category}] " + "[Stack Size: {StackSize}] " + "[Line: {Line}]", - category.Value, state.CurrentHeaderDepth, state.CurrentCategory.StackSize, line); + state.CurrentHeaderDepth, state.CurrentCategory.StackSize, line); } + } - private void ParseAndSetOptional(string line, ParserState state) + private TermCategory? ParseCategory(string line) + { + foreach (var (category, regex) in _regexCategories) { - if (line.ContainsIgnoreCase("optional")) + if (regex.Match(line).Success) { - state.TermsAreOptional.PushValue(true, state.CurrentHeaderDepth); - - Log.Debug(" - Optional Set " + - "[Scope: {Scope}] " + - "[Stack Size: {StackSize}] " + - "[Line: {Line}]", - state.CurrentHeaderDepth, state.CurrentCategory.StackSize, line); + return category; } } - private TermCategory? ParseCategory(string line) - { - foreach (var (category, regex) in _regexCategories) - { - if (regex.Match(line).Success) - { - return category; - } - } - - return null; - } + return null; } } diff --git a/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileType.cs b/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileType.cs index e90c92fb..99922a89 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileType.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileType.cs @@ -1,8 +1,7 @@ -namespace TrashLib.Sonarr.ReleaseProfile +namespace TrashLib.Sonarr.ReleaseProfile; + +public enum ReleaseProfileType { - public enum ReleaseProfileType - { - Anime, - Series - } + Anime, + Series } diff --git a/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileUpdater.cs b/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileUpdater.cs index 0b88515d..9e31ed5f 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileUpdater.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileUpdater.cs @@ -9,168 +9,167 @@ using TrashLib.Sonarr.Api; using TrashLib.Sonarr.Api.Objects; using TrashLib.Sonarr.Config; -namespace TrashLib.Sonarr.ReleaseProfile +namespace TrashLib.Sonarr.ReleaseProfile; + +internal class ReleaseProfileUpdater : IReleaseProfileUpdater { - internal class ReleaseProfileUpdater : IReleaseProfileUpdater + private readonly ISonarrApi _api; + private readonly ISonarrCompatibility _compatibility; + private readonly IReleaseProfileGuideParser _parser; + + public ReleaseProfileUpdater( + ILogger logger, + IReleaseProfileGuideParser parser, + ISonarrApi api, + ISonarrCompatibility compatibility) { - private readonly ISonarrApi _api; - private readonly ISonarrCompatibility _compatibility; - private readonly IReleaseProfileGuideParser _parser; - - public ReleaseProfileUpdater( - ILogger logger, - IReleaseProfileGuideParser parser, - ISonarrApi api, - ISonarrCompatibility compatibility) - { - Log = logger; - _parser = parser; - _api = api; - _compatibility = compatibility; - } + Log = logger; + _parser = parser; + _api = api; + _compatibility = compatibility; + } - private ILogger Log { get; } + private ILogger Log { get; } - public async Task Process(bool isPreview, SonarrConfiguration config) + public async Task Process(bool isPreview, SonarrConfiguration config) + { + foreach (var profile in config.ReleaseProfiles) { - foreach (var profile in config.ReleaseProfiles) - { - Log.Information("Processing Release Profile: {ProfileName}", profile.Type); - var markdown = await _parser.GetMarkdownData(profile.Type); - var profiles = Utils.FilterProfiles(_parser.ParseMarkdown(profile, markdown)); - - if (profile.Filter.IncludeOptional) - { - Log.Information("Configuration is set to allow optional terms to be synchronized"); - } - - if (isPreview) - { - Utils.PrintTermsAndScores(profiles); - continue; - } - - await ProcessReleaseProfiles(profiles, profile); - } - } + Log.Information("Processing Release Profile: {ProfileName}", profile.Type); + var markdown = await _parser.GetMarkdownData(profile.Type); + var profiles = Utils.FilterProfiles(_parser.ParseMarkdown(profile, markdown)); - private async Task DoVersionEnforcement() - { - // _compatibility.Capabilities - // .Where(x => !x.SupportsNamedReleaseProfiles) - // .Subscribe(x => throw new VersionException( - // $"Your Sonarr version {x.Version} does not meet the minimum " + - // $"required version of {_compatibility.MinimumVersion} to use this program")); - - var capabilities = await _compatibility.Capabilities.LastAsync(); - if (!capabilities.SupportsNamedReleaseProfiles) + if (profile.Filter.IncludeOptional) { - throw new VersionException( - $"Your Sonarr version {capabilities.Version} does not meet the minimum " + - $"required version of {_compatibility.MinimumVersion} to use this program"); + Log.Information("Configuration is set to allow optional terms to be synchronized"); } - } - private async Task CreateMissingTags(ICollection sonarrTags, IEnumerable configTags) - { - var missingTags = configTags.Where(t => !sonarrTags.Any(t2 => t2.Label.EqualsIgnoreCase(t))); - foreach (var tag in missingTags) + if (isPreview) { - Log.Debug("Creating Tag: {Tag}", tag); - var newTag = await _api.CreateTag(tag); - sonarrTags.Add(newTag); + Utils.PrintTermsAndScores(profiles); + continue; } + + await ProcessReleaseProfiles(profiles, profile); } + } - private string BuildProfileTitle(ReleaseProfileType profileType, string profileName) + private async Task DoVersionEnforcement() + { + // _compatibility.Capabilities + // .Where(x => !x.SupportsNamedReleaseProfiles) + // .Subscribe(x => throw new VersionException( + // $"Your Sonarr version {x.Version} does not meet the minimum " + + // $"required version of {_compatibility.MinimumVersion} to use this program")); + + var capabilities = await _compatibility.Capabilities.LastAsync(); + if (!capabilities.SupportsNamedReleaseProfiles) { - var titleType = profileType.ToString(); - return $"[Trash] {titleType} - {profileName}"; + throw new VersionException( + $"Your Sonarr version {capabilities.Version} does not meet the minimum " + + $"required version of {_compatibility.MinimumVersion} to use this program"); } + } - private static SonarrReleaseProfile? GetProfileToUpdate(IEnumerable profiles, - string profileName) + private async Task CreateMissingTags(ICollection sonarrTags, IEnumerable configTags) + { + var missingTags = configTags.Where(t => !sonarrTags.Any(t2 => t2.Label.EqualsIgnoreCase(t))); + foreach (var tag in missingTags) { - return profiles.FirstOrDefault(p => p.Name == profileName); + Log.Debug("Creating Tag: {Tag}", tag); + var newTag = await _api.CreateTag(tag); + sonarrTags.Add(newTag); } + } - private static void SetupProfileRequestObject(SonarrReleaseProfile profileToUpdate, FilteredProfileData profile, - List tagIds) - { - profileToUpdate.Preferred = profile.Preferred - .SelectMany(kvp => kvp.Value.Select(term => new SonarrPreferredTerm(kvp.Key, term))) - .ToList(); + private string BuildProfileTitle(ReleaseProfileType profileType, string profileName) + { + var titleType = profileType.ToString(); + return $"[Trash] {titleType} - {profileName}"; + } - profileToUpdate.Ignored = profile.Ignored.ToList(); //string.Join(',', profile.Ignored); - profileToUpdate.Required = profile.Required.ToList(); //string.Join(',', profile.Required); + private static SonarrReleaseProfile? GetProfileToUpdate(IEnumerable profiles, + string profileName) + { + return profiles.FirstOrDefault(p => p.Name == profileName); + } - // Null means the guide didn't specify a value for this, so we leave the existing setting intact. - if (profile.IncludePreferredWhenRenaming != null) - { - profileToUpdate.IncludePreferredWhenRenaming = profile.IncludePreferredWhenRenaming.Value; - } + private static void SetupProfileRequestObject(SonarrReleaseProfile profileToUpdate, FilteredProfileData profile, + List tagIds) + { + profileToUpdate.Preferred = profile.Preferred + .SelectMany(kvp => kvp.Value.Select(term => new SonarrPreferredTerm(kvp.Key, term))) + .ToList(); - profileToUpdate.Tags = tagIds; - } + profileToUpdate.Ignored = profile.Ignored.ToList(); //string.Join(',', profile.Ignored); + profileToUpdate.Required = profile.Required.ToList(); //string.Join(',', profile.Required); - private async Task UpdateExistingProfile(SonarrReleaseProfile profileToUpdate, FilteredProfileData profile, - List tagIds) + // Null means the guide didn't specify a value for this, so we leave the existing setting intact. + if (profile.IncludePreferredWhenRenaming != null) { - Log.Debug("Update existing profile with id {ProfileId}", profileToUpdate.Id); - SetupProfileRequestObject(profileToUpdate, profile, tagIds); - await _api.UpdateReleaseProfile(profileToUpdate); + profileToUpdate.IncludePreferredWhenRenaming = profile.IncludePreferredWhenRenaming.Value; } - private async Task CreateNewProfile(string title, FilteredProfileData profile, List tagIds) + profileToUpdate.Tags = tagIds; + } + + private async Task UpdateExistingProfile(SonarrReleaseProfile profileToUpdate, FilteredProfileData profile, + List tagIds) + { + Log.Debug("Update existing profile with id {ProfileId}", profileToUpdate.Id); + SetupProfileRequestObject(profileToUpdate, profile, tagIds); + await _api.UpdateReleaseProfile(profileToUpdate); + } + + private async Task CreateNewProfile(string title, FilteredProfileData profile, List tagIds) + { + var newProfile = new SonarrReleaseProfile { - var newProfile = new SonarrReleaseProfile - { - Name = title, - Enabled = true - }; + Name = title, + Enabled = true + }; - SetupProfileRequestObject(newProfile, profile, tagIds); - await _api.CreateReleaseProfile(newProfile); - } + SetupProfileRequestObject(newProfile, profile, tagIds); + await _api.CreateReleaseProfile(newProfile); + } + + private async Task ProcessReleaseProfiles(IDictionary profiles, + ReleaseProfileConfig config) + { + await DoVersionEnforcement(); + + List tagIds = new(); - private async Task ProcessReleaseProfiles(IDictionary profiles, - ReleaseProfileConfig config) + // If tags were provided, ensure they exist. Tags that do not exist are added first, so that we + // may specify them with the release profile request payload. + if (config.Tags.Count > 0) { - await DoVersionEnforcement(); + var sonarrTags = await _api.GetTags(); + await CreateMissingTags(sonarrTags, config.Tags); + tagIds = sonarrTags.Where(t => config.Tags.Any(ct => ct.EqualsIgnoreCase(t.Label))) + .Select(t => t.Id) + .ToList(); + } - List tagIds = new(); + // Obtain all of the existing release profiles first. If any were previously created by our program + // here, we favor replacing those instead of creating new ones, which would just be mostly duplicates + // (but with some differences, since there have likely been updates since the last run). + var existingProfiles = await _api.GetReleaseProfiles(); - // If tags were provided, ensure they exist. Tags that do not exist are added first, so that we - // may specify them with the release profile request payload. - if (config.Tags.Count > 0) + foreach (var (name, profileData) in profiles) + { + var filteredProfileData = new FilteredProfileData(profileData, config); + var title = BuildProfileTitle(config.Type, name); + var profileToUpdate = GetProfileToUpdate(existingProfiles, title); + if (profileToUpdate != null) { - var sonarrTags = await _api.GetTags(); - await CreateMissingTags(sonarrTags, config.Tags); - tagIds = sonarrTags.Where(t => config.Tags.Any(ct => ct.EqualsIgnoreCase(t.Label))) - .Select(t => t.Id) - .ToList(); + Log.Information("Update existing profile: {ProfileName}", title); + await UpdateExistingProfile(profileToUpdate, filteredProfileData, tagIds); } - - // Obtain all of the existing release profiles first. If any were previously created by our program - // here, we favor replacing those instead of creating new ones, which would just be mostly duplicates - // (but with some differences, since there have likely been updates since the last run). - var existingProfiles = await _api.GetReleaseProfiles(); - - foreach (var (name, profileData) in profiles) + else { - var filteredProfileData = new FilteredProfileData(profileData, config); - var title = BuildProfileTitle(config.Type, name); - var profileToUpdate = GetProfileToUpdate(existingProfiles, title); - if (profileToUpdate != null) - { - Log.Information("Update existing profile: {ProfileName}", title); - await UpdateExistingProfile(profileToUpdate, filteredProfileData, tagIds); - } - else - { - Log.Information("Create new profile: {ProfileName}", title); - await CreateNewProfile(title, filteredProfileData, tagIds); - } + Log.Information("Create new profile: {ProfileName}", title); + await CreateNewProfile(title, filteredProfileData, tagIds); } } } diff --git a/src/TrashLib/Sonarr/ReleaseProfile/ScopedState.cs b/src/TrashLib/Sonarr/ReleaseProfile/ScopedState.cs index 8dab755d..1c270043 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/ScopedState.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/ScopedState.cs @@ -1,61 +1,60 @@ using System.Collections.Generic; -namespace TrashLib.Sonarr.ReleaseProfile +namespace TrashLib.Sonarr.ReleaseProfile; + +public class ScopedState { - public class ScopedState + private readonly T _defaultValue; + private readonly Stack _scopeStack = new(); + + public ScopedState(T defaultValue = default!) { - private readonly T _defaultValue; - private readonly Stack _scopeStack = new(); + _defaultValue = defaultValue; + } - public ScopedState(T defaultValue = default!) - { - _defaultValue = defaultValue; - } + public T Value => _scopeStack.Count > 0 ? _scopeStack.Peek().Value : _defaultValue; - public T Value => _scopeStack.Count > 0 ? _scopeStack.Peek().Value : _defaultValue; + public int? ActiveScope => _scopeStack.Count > 0 ? _scopeStack.Peek().Scope : null; - public int? ActiveScope => _scopeStack.Count > 0 ? _scopeStack.Peek().Scope : null; + public int StackSize => _scopeStack.Count; - public int StackSize => _scopeStack.Count; + public void PushValue(T value, int scope) + { + if (_scopeStack.Count == 0 || _scopeStack.Peek().Scope < scope) + { + _scopeStack.Push(new Node(value, scope)); + } + else if (_scopeStack.Peek().Scope == scope) + { + _scopeStack.Peek().Value = value; + } + } - public void PushValue(T value, int scope) + public bool Reset(int scope) + { + if (_scopeStack.Count == 0) { - if (_scopeStack.Count == 0 || _scopeStack.Peek().Scope < scope) - { - _scopeStack.Push(new Node(value, scope)); - } - else if (_scopeStack.Peek().Scope == scope) - { - _scopeStack.Peek().Value = value; - } + return false; } - public bool Reset(int scope) + var prevCount = StackSize; + while (_scopeStack.Count > 0 && _scopeStack.Peek().Scope >= scope) { - if (_scopeStack.Count == 0) - { - return false; - } - - var prevCount = StackSize; - while (_scopeStack.Count > 0 && _scopeStack.Peek().Scope >= scope) - { - _scopeStack.Pop(); - } - - return prevCount != StackSize; + _scopeStack.Pop(); } - private class Node + return prevCount != StackSize; + } + + private class Node + { + public Node(T value, int scope) { - public Node(T value, int scope) - { - Value = value; - Scope = scope; - } - - public T Value { get; set; } - public int Scope { get; } + Value = value; + Scope = scope; } + + public T Value { get; set; } + public int Scope { get; } } } diff --git a/src/TrashLib/Sonarr/ReleaseProfile/Utils.cs b/src/TrashLib/Sonarr/ReleaseProfile/Utils.cs index 330b902c..5bb0470d 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/Utils.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/Utils.cs @@ -2,87 +2,86 @@ using System.Collections.Generic; using System.Linq; -namespace TrashLib.Sonarr.ReleaseProfile -{ - using ProfileDataCollection = IDictionary; +namespace TrashLib.Sonarr.ReleaseProfile; + +using ProfileDataCollection = IDictionary; - public static class Utils +public static class Utils +{ + public static ProfileDataCollection FilterProfiles(ProfileDataCollection profiles) { - public static ProfileDataCollection FilterProfiles(ProfileDataCollection profiles) + static bool IsEmpty(ProfileData data) { - static bool IsEmpty(ProfileData data) - { - return data.Required.Count == 0 && data.Ignored.Count == 0 && data.Preferred.Count == 0; - } - - // A few false-positive profiles are added sometimes. We filter these out by checking if they - // actually have meaningful data attached to them, such as preferred terms. If they are mostly empty, - // we remove them here. - return profiles - .Where(kv => !IsEmpty(kv.Value)) - .ToDictionary(kv => kv.Key, kv => kv.Value); + return data.Required.Count == 0 && data.Ignored.Count == 0 && data.Preferred.Count == 0; } - public static void PrintTermsAndScores(ProfileDataCollection profiles) + // A few false-positive profiles are added sometimes. We filter these out by checking if they + // actually have meaningful data attached to them, such as preferred terms. If they are mostly empty, + // we remove them here. + return profiles + .Where(kv => !IsEmpty(kv.Value)) + .ToDictionary(kv => kv.Key, kv => kv.Value); + } + + public static void PrintTermsAndScores(ProfileDataCollection profiles) + { + static void PrintPreferredTerms(string title, IDictionary> dict) { - static void PrintPreferredTerms(string title, IDictionary> dict) + if (dict.Count <= 0) { - if (dict.Count <= 0) - { - return; - } - - Console.WriteLine($" {title}:"); - foreach (var (score, terms) in dict) - { - foreach (var term in terms) - { - Console.WriteLine($" {score,-10} {term}"); - } - } - - Console.WriteLine(""); + return; } - static void PrintTerms(string title, ICollection terms) + Console.WriteLine($" {title}:"); + foreach (var (score, terms) in dict) { - if (terms.Count == 0) - { - return; - } - - Console.WriteLine($" {title}:"); foreach (var term in terms) { - Console.WriteLine($" {term}"); + Console.WriteLine($" {score,-10} {term}"); } - - Console.WriteLine(""); } Console.WriteLine(""); + } - foreach (var (name, profile) in profiles) + static void PrintTerms(string title, ICollection terms) + { + if (terms.Count == 0) { - Console.WriteLine(name); + return; + } - if (profile.IncludePreferredWhenRenaming != null) - { - Console.WriteLine(" Include Preferred when Renaming?"); - Console.WriteLine(" " + - (profile.IncludePreferredWhenRenaming.Value ? "CHECKED" : "NOT CHECKED")); - Console.WriteLine(""); - } + Console.WriteLine($" {title}:"); + foreach (var term in terms) + { + Console.WriteLine($" {term}"); + } - PrintTerms("Must Contain", profile.Required); - PrintTerms("Must Contain (Optional)", profile.Optional.Required); - PrintTerms("Must Not Contain", profile.Ignored); - PrintTerms("Must Not Contain (Optional)", profile.Optional.Ignored); - PrintPreferredTerms("Preferred", profile.Preferred); - PrintPreferredTerms("Preferred (Optional)", profile.Optional.Preferred); + Console.WriteLine(""); + } + + Console.WriteLine(""); + + foreach (var (name, profile) in profiles) + { + Console.WriteLine(name); + if (profile.IncludePreferredWhenRenaming != null) + { + Console.WriteLine(" Include Preferred when Renaming?"); + Console.WriteLine(" " + + (profile.IncludePreferredWhenRenaming.Value ? "CHECKED" : "NOT CHECKED")); Console.WriteLine(""); } + + PrintTerms("Must Contain", profile.Required); + PrintTerms("Must Contain (Optional)", profile.Optional.Required); + PrintTerms("Must Not Contain", profile.Ignored); + PrintTerms("Must Not Contain (Optional)", profile.Optional.Ignored); + PrintPreferredTerms("Preferred", profile.Preferred); + PrintPreferredTerms("Preferred (Optional)", profile.Optional.Preferred); + + Console.WriteLine(""); } } } diff --git a/src/TrashLib/Sonarr/SonarrAutofacModule.cs b/src/TrashLib/Sonarr/SonarrAutofacModule.cs index cabc0725..8367656a 100644 --- a/src/TrashLib/Sonarr/SonarrAutofacModule.cs +++ b/src/TrashLib/Sonarr/SonarrAutofacModule.cs @@ -4,28 +4,27 @@ using TrashLib.Sonarr.Config; using TrashLib.Sonarr.QualityDefinition; using TrashLib.Sonarr.ReleaseProfile; -namespace TrashLib.Sonarr +namespace TrashLib.Sonarr; + +public class SonarrAutofacModule : Module { - public class SonarrAutofacModule : Module + protected override void Load(ContainerBuilder builder) { - protected override void Load(ContainerBuilder builder) - { - builder.RegisterType().As(); - builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); - builder.RegisterType() - .As() - .SingleInstance(); + builder.RegisterType() + .As() + .SingleInstance(); - // Release Profile Support - builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType() - .As(); + // Release Profile Support + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType() + .As(); - // Quality Definition Support - builder.RegisterType().As(); - builder.RegisterType().As(); - } + // Quality Definition Support + builder.RegisterType().As(); + builder.RegisterType().As(); } } diff --git a/src/TrashLib/Sonarr/SonarrCapabilities.cs b/src/TrashLib/Sonarr/SonarrCapabilities.cs index e6b44b3e..729d0d6e 100644 --- a/src/TrashLib/Sonarr/SonarrCapabilities.cs +++ b/src/TrashLib/Sonarr/SonarrCapabilities.cs @@ -1,27 +1,26 @@ using System; -namespace TrashLib.Sonarr +namespace TrashLib.Sonarr; + +public record SonarrCapabilities { - public record SonarrCapabilities + public SonarrCapabilities() { - public SonarrCapabilities() - { - Version = new Version(); - } + Version = new Version(); + } - public SonarrCapabilities(Version version) - { - Version = version; - } + public SonarrCapabilities(Version version) + { + Version = version; + } - public Version Version { get; } + public Version Version { get; } - public bool SupportsNamedReleaseProfiles { get; init; } + public bool SupportsNamedReleaseProfiles { get; init; } - // Background: Issue #16 filed which points to a backward-breaking API - // change made in Sonarr at commit [deed85d2f]. - // - // [deed85d2f]: https://github.com/Sonarr/Sonarr/commit/deed85d2f9147e6180014507ef4f5af3695b0c61 - public bool ArraysNeededForReleaseProfileRequiredAndIgnored { get; init; } - } + // Background: Issue #16 filed which points to a backward-breaking API + // change made in Sonarr at commit [deed85d2f]. + // + // [deed85d2f]: https://github.com/Sonarr/Sonarr/commit/deed85d2f9147e6180014507ef4f5af3695b0c61 + public bool ArraysNeededForReleaseProfileRequiredAndIgnored { get; init; } } diff --git a/src/TrashLib/Sonarr/SonarrCompatibility.cs b/src/TrashLib/Sonarr/SonarrCompatibility.cs index 1a3810c1..18b4724e 100644 --- a/src/TrashLib/Sonarr/SonarrCompatibility.cs +++ b/src/TrashLib/Sonarr/SonarrCompatibility.cs @@ -4,36 +4,35 @@ using System.Reactive.Linq; using Flurl.Http; using TrashLib.Config; -namespace TrashLib.Sonarr +namespace TrashLib.Sonarr; + +public class SonarrCompatibility : ISonarrCompatibility { - public class SonarrCompatibility : ISonarrCompatibility + public SonarrCompatibility(IServerInfo serverInfo) { - public SonarrCompatibility(IServerInfo serverInfo) - { - Capabilities = Observable.FromAsync( - async () => await serverInfo.BuildRequest() - .AppendPathSegment("system/status") - .GetJsonAsync(), NewThreadScheduler.Default) - .Timeout(TimeSpan.FromSeconds(15)) - .Select(x => new Version(x.version)) - .Select(BuildCapabilitiesObject) - .Replay(1) - .AutoConnect(); - } + Capabilities = Observable.FromAsync( + async () => await serverInfo.BuildRequest() + .AppendPathSegment("system/status") + .GetJsonAsync(), NewThreadScheduler.Default) + .Timeout(TimeSpan.FromSeconds(15)) + .Select(x => new Version(x.version)) + .Select(BuildCapabilitiesObject) + .Replay(1) + .AutoConnect(); + } - public IObservable Capabilities { get; } - public Version MinimumVersion => new("3.0.4.1098"); + public IObservable Capabilities { get; } + public Version MinimumVersion => new("3.0.4.1098"); - private SonarrCapabilities BuildCapabilitiesObject(Version version) + private SonarrCapabilities BuildCapabilitiesObject(Version version) + { + return new SonarrCapabilities(version) { - return new SonarrCapabilities(version) - { - SupportsNamedReleaseProfiles = - version >= MinimumVersion, + SupportsNamedReleaseProfiles = + version >= MinimumVersion, - ArraysNeededForReleaseProfileRequiredAndIgnored = - version >= new Version("3.0.6.1355") - }; - } + ArraysNeededForReleaseProfileRequiredAndIgnored = + version >= new Version("3.0.6.1355") + }; } } diff --git a/src/TrashLib/Startup/AutoMapperConfig.cs b/src/TrashLib/Startup/AutoMapperConfig.cs index 49c2dcd7..be5bb87b 100644 --- a/src/TrashLib/Startup/AutoMapperConfig.cs +++ b/src/TrashLib/Startup/AutoMapperConfig.cs @@ -1,22 +1,21 @@ using AutoMapper; -namespace TrashLib.Startup +namespace TrashLib.Startup; + +public static class AutoMapperConfig { - public static class AutoMapperConfig + public static IMapper Setup() { - public static IMapper Setup() + // todo: consider using AutoMapper.Contrib.Autofac.DependencyInjection + var mapperConfig = new MapperConfiguration(cfg => { - // todo: consider using AutoMapper.Contrib.Autofac.DependencyInjection - var mapperConfig = new MapperConfiguration(cfg => - { - cfg.AddMaps(typeof(AutoMapperConfig)); - }); + cfg.AddMaps(typeof(AutoMapperConfig)); + }); #if DEBUG - mapperConfig.AssertConfigurationIsValid(); + mapperConfig.AssertConfigurationIsValid(); #endif - return mapperConfig.CreateMapper(); - } + return mapperConfig.CreateMapper(); } }