diff --git a/src/Recyclarr.Cli/CompositionRoot.cs b/src/Recyclarr.Cli/CompositionRoot.cs index 9cfb51bc..3921e6da 100644 --- a/src/Recyclarr.Cli/CompositionRoot.cs +++ b/src/Recyclarr.Cli/CompositionRoot.cs @@ -83,7 +83,7 @@ public static class CompositionRoot typeof(GenericSyncPipeline), typeof(CustomFormatSyncPipeline), typeof(QualityProfileSyncPipeline), - typeof(QualitySizeSyncPipeline), + typeof(GenericSyncPipeline), typeof(GenericSyncPipeline), typeof(GenericSyncPipeline)) .As() diff --git a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeApiFetchPhase.cs b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeApiFetchPhase.cs index b57ab1ef..e55dc127 100644 --- a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeApiFetchPhase.cs +++ b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeApiFetchPhase.cs @@ -1,12 +1,14 @@ +using Recyclarr.Cli.Pipelines.Generic; using Recyclarr.Config.Models; using Recyclarr.ServarrApi.QualityDefinition; namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; public class QualitySizeApiFetchPhase(IQualityDefinitionApiService api) + : IApiFetchPipelinePhase { - public async Task> Execute(IServiceConfiguration config) + public async Task Execute(QualitySizePipelineContext context, IServiceConfiguration config) { - return await api.GetQualityDefinition(config); + context.ApiFetchOutput = await api.GetQualityDefinition(config); } } diff --git a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeApiPersistencePhase.cs b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeApiPersistencePhase.cs index 429d16ad..ae1573e8 100644 --- a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeApiPersistencePhase.cs +++ b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeApiPersistencePhase.cs @@ -1,13 +1,14 @@ +using Recyclarr.Cli.Pipelines.Generic; using Recyclarr.Config.Models; using Recyclarr.ServarrApi.QualityDefinition; namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; -public class QualitySizeApiPersistencePhase(ILogger log, IQualityDefinitionApiService api) +public class QualitySizeApiPersistencePhase(IQualityDefinitionApiService api) + : IApiPersistencePipelinePhase { - public async Task Execute(IServiceConfiguration config, IList serverQuality) + public async Task Execute(QualitySizePipelineContext context, IServiceConfiguration config) { - await api.UpdateQualityDefinition(config, serverQuality); - log.Information("Number of updated qualities: {Count}", serverQuality.Count); + await api.UpdateQualityDefinition(config, context.TransactionOutput); } } diff --git a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeGuidePhase.cs b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeConfigPhase.cs similarity index 76% rename from src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeGuidePhase.cs rename to src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeConfigPhase.cs index 2ef35f50..09933c5b 100644 --- a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeGuidePhase.cs +++ b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeConfigPhase.cs @@ -1,18 +1,20 @@ +using Recyclarr.Cli.Pipelines.Generic; using Recyclarr.Common.Extensions; using Recyclarr.Config.Models; using Recyclarr.TrashGuide.QualitySize; namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; -public class QualitySizeGuidePhase(ILogger log, IQualitySizeGuideService guide) +public class QualitySizeConfigPhase(ILogger log, IQualitySizeGuideService guide) + : IConfigPipelinePhase { - public QualitySizeData? Execute(IServiceConfiguration config) + public Task Execute(QualitySizePipelineContext context, IServiceConfiguration config) { var qualityDef = config.QualityDefinition; if (qualityDef is null) { log.Debug("{Instance} has no quality definition", config.InstanceName); - return null; + return Task.CompletedTask; } var qualityDefinitions = guide.GetQualitySizeData(config.ServiceType); @@ -21,13 +23,13 @@ public class QualitySizeGuidePhase(ILogger log, IQualitySizeGuideService guide) if (selectedQuality == null) { - log.Error("The specified quality definition type does not exist: {Type}", qualityDef.Type); - return null; + context.ConfigError = $"The specified quality definition type does not exist: {qualityDef.Type}"; + return Task.CompletedTask; } - log.Information("Processing Quality Definition: {QualityDefinition}", qualityDef.Type); AdjustPreferredRatio(qualityDef, selectedQuality); - return selectedQuality; + context.ConfigOutput = selectedQuality; + return Task.CompletedTask; } private void AdjustPreferredRatio(QualityDefinitionConfig config, QualitySizeData selectedQuality) diff --git a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeLogPhase.cs b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeLogPhase.cs new file mode 100644 index 00000000..a9dc4984 --- /dev/null +++ b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeLogPhase.cs @@ -0,0 +1,29 @@ +using Recyclarr.Cli.Pipelines.Generic; + +namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; + +public class QualitySizeLogPhase(ILogger log) : ILogPipelinePhase +{ + public bool LogConfigPhaseAndExitIfNeeded(QualitySizePipelineContext context) + { + if (context.ConfigError is not null) + { + log.Error(context.ConfigError); + return true; + } + + if (context.ConfigOutput is null) + { + log.Debug("No Quality Definitions to process"); + return true; + } + + return false; + } + + public void LogPersistenceResults(QualitySizePipelineContext context) + { + // Do not check ConfigOutput for null here since that is done for us in the LogConfigPhase method + log.Information("Processed Quality Definition: {QualityDefinition}", context.ConfigOutput!.Type); + } +} diff --git a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizePreviewPhase.cs b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizePreviewPhase.cs index b91e23f2..8ca32868 100644 --- a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizePreviewPhase.cs +++ b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizePreviewPhase.cs @@ -1,11 +1,11 @@ -using Recyclarr.TrashGuide.QualitySize; +using Recyclarr.Cli.Pipelines.Generic; using Spectre.Console; namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; -public class QualitySizePreviewPhase(IAnsiConsole console) +public class QualitySizePreviewPhase(IAnsiConsole console) : IPreviewPipelinePhase { - public void Execute(QualitySizeData selectedQuality) + public void Execute(QualitySizePipelineContext context) { var table = new Table(); @@ -15,7 +15,8 @@ public class QualitySizePreviewPhase(IAnsiConsole console) table.AddColumn("[bold]Max[/]"); table.AddColumn("[bold]Preferred[/]"); - foreach (var q in selectedQuality.Qualities) + // Do not check ConfigOutput for null here since the LogPhase checks that for us + foreach (var q in context.ConfigOutput!.Qualities) { var quality = $"[dodgerblue1]{q.Quality}[/]"; table.AddRow(quality, q.AnnotatedMin, q.AnnotatedMax, q.AnnotatedPreferred); diff --git a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeTransactionPhase.cs b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeTransactionPhase.cs index 03867e05..4db1b58e 100644 --- a/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeTransactionPhase.cs +++ b/src/Recyclarr.Cli/Pipelines/QualitySize/PipelinePhases/QualitySizeTransactionPhase.cs @@ -1,15 +1,18 @@ using System.Collections.ObjectModel; +using Recyclarr.Cli.Pipelines.Generic; using Recyclarr.ServarrApi.QualityDefinition; using Recyclarr.TrashGuide.QualitySize; namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; -public class QualitySizeTransactionPhase(ILogger log) +public class QualitySizeTransactionPhase(ILogger log) : ITransactionPipelinePhase { - public Collection Execute( - IEnumerable guideQuality, - IList serverQuality) + public void Execute(QualitySizePipelineContext context) { + // Do not check ConfigOutput for null since the LogPhase does it for us + var guideQuality = context.ConfigOutput!.Qualities; + var serverQuality = context.ApiFetchOutput; + var newQuality = new Collection(); foreach (var qualityData in guideQuality) { @@ -37,7 +40,7 @@ public class QualitySizeTransactionPhase(ILogger log) serverEntry.PreferredSize); } - return newQuality; + context.TransactionOutput = newQuality; } private static bool QualityIsDifferent(ServiceQualityDefinitionItem a, QualitySizeItem b) diff --git a/src/Recyclarr.Cli/Pipelines/QualitySize/QualitySizeAutofacModule.cs b/src/Recyclarr.Cli/Pipelines/QualitySize/QualitySizeAutofacModule.cs index e9b16b0a..88990bdb 100644 --- a/src/Recyclarr.Cli/Pipelines/QualitySize/QualitySizeAutofacModule.cs +++ b/src/Recyclarr.Cli/Pipelines/QualitySize/QualitySizeAutofacModule.cs @@ -1,5 +1,4 @@ using Autofac; -using Autofac.Extras.AggregateService; using Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; namespace Recyclarr.Cli.Pipelines.QualitySize; @@ -11,11 +10,13 @@ public class QualitySizeAutofacModule : Module base.Load(builder); builder.RegisterType(); - builder.RegisterAggregateService(); - builder.RegisterType(); - builder.RegisterType(); - builder.RegisterType(); - builder.RegisterType(); - builder.RegisterType(); + builder.RegisterTypes( + typeof(QualitySizeConfigPhase), + typeof(QualitySizePreviewPhase), + typeof(QualitySizeApiFetchPhase), + typeof(QualitySizeTransactionPhase), + typeof(QualitySizeApiPersistencePhase), + typeof(QualitySizeLogPhase)) + .AsImplementedInterfaces(); } } diff --git a/src/Recyclarr.Cli/Pipelines/QualitySize/QualitySizePipelineContext.cs b/src/Recyclarr.Cli/Pipelines/QualitySize/QualitySizePipelineContext.cs new file mode 100644 index 00000000..b0c0fee0 --- /dev/null +++ b/src/Recyclarr.Cli/Pipelines/QualitySize/QualitySizePipelineContext.cs @@ -0,0 +1,24 @@ +using System.Diagnostics.CodeAnalysis; +using Recyclarr.Cli.Pipelines.Generic; +using Recyclarr.Common; +using Recyclarr.ServarrApi.QualityDefinition; +using Recyclarr.TrashGuide.QualitySize; + +namespace Recyclarr.Cli.Pipelines.QualitySize; + +[SuppressMessage("Usage", "CA2227:Collection properties should be read only", Justification = + "Context objects are similar to DTOs; for usability we want to assign not append")] +public class QualitySizePipelineContext : IPipelineContext +{ + public string PipelineDescription => "Quality Definition Pipeline"; + public IReadOnlyCollection SupportedServiceTypes { get; } = new[] + { + SupportedServices.Sonarr, + SupportedServices.Radarr + }; + + public QualitySizeData? ConfigOutput { get; set; } + public IList ApiFetchOutput { get; set; } = default!; + public IList TransactionOutput { get; set; } = default!; + public string? ConfigError { get; set; } +} diff --git a/src/Recyclarr.Cli/Pipelines/QualitySize/QualitySizeSyncPipeline.cs b/src/Recyclarr.Cli/Pipelines/QualitySize/QualitySizeSyncPipeline.cs deleted file mode 100644 index f60e3510..00000000 --- a/src/Recyclarr.Cli/Pipelines/QualitySize/QualitySizeSyncPipeline.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Recyclarr.Cli.Console.Settings; -using Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; -using Recyclarr.Config.Models; - -namespace Recyclarr.Cli.Pipelines.QualitySize; - -public interface IQualitySizePipelinePhases -{ - QualitySizeGuidePhase GuidePhase { get; } - Lazy PreviewPhase { get; } - QualitySizeApiFetchPhase ApiFetchPhase { get; } - QualitySizeTransactionPhase TransactionPhase { get; } - QualitySizeApiPersistencePhase ApiPersistencePhase { get; } -} - -public class QualitySizeSyncPipeline(ILogger log, IQualitySizePipelinePhases phases) : ISyncPipeline -{ - public async Task Execute(ISyncSettings settings, IServiceConfiguration config) - { - var selectedQuality = phases.GuidePhase.Execute(config); - if (selectedQuality is null) - { - log.Debug("No quality definition to process"); - return; - } - - if (settings.Preview) - { - phases.PreviewPhase.Value.Execute(selectedQuality); - return; - } - - var serviceData = await phases.ApiFetchPhase.Execute(config); - var transactions = phases.TransactionPhase.Execute(selectedQuality.Qualities, serviceData); - await phases.ApiPersistencePhase.Execute(config, transactions); - } -} diff --git a/tests/Recyclarr.Cli.Tests/Pipelines/QualitySize/PipelinePhases/QualitySizeGuidePhaseTest.cs b/tests/Recyclarr.Cli.Tests/Pipelines/QualitySize/PipelinePhases/QualitySizeConfigPhaseTest.cs similarity index 75% rename from tests/Recyclarr.Cli.Tests/Pipelines/QualitySize/PipelinePhases/QualitySizeGuidePhaseTest.cs rename to tests/Recyclarr.Cli.Tests/Pipelines/QualitySize/PipelinePhases/QualitySizeConfigPhaseTest.cs index bb67ab03..afbdfd80 100644 --- a/tests/Recyclarr.Cli.Tests/Pipelines/QualitySize/PipelinePhases/QualitySizeGuidePhaseTest.cs +++ b/tests/Recyclarr.Cli.Tests/Pipelines/QualitySize/PipelinePhases/QualitySizeConfigPhaseTest.cs @@ -1,4 +1,5 @@ using NSubstitute.ReturnsExtensions; +using Recyclarr.Cli.Pipelines.QualitySize; using Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; using Recyclarr.Config.Models; using Recyclarr.TrashGuide.QualitySize; @@ -6,23 +7,24 @@ using Recyclarr.TrashGuide.QualitySize; namespace Recyclarr.Cli.Tests.Pipelines.QualitySize.PipelinePhases; [TestFixture] -public class QualitySizeGuidePhaseTest +public class QualitySizeConfigPhaseTest { [Test, AutoMockData] - public void Do_nothing_if_no_quality_definition(QualitySizeGuidePhase sut) + public void Do_nothing_if_no_quality_definition(QualitySizeConfigPhase sut) { + var context = new QualitySizePipelineContext(); var config = Substitute.For(); config.QualityDefinition.ReturnsNull(); - var result = sut.Execute(config); + sut.Execute(context, config); - result.Should().BeNull(); + context.ConfigOutput.Should().BeNull(); } [Test, AutoMockData] public void Do_nothing_if_no_matching_quality_definition( [Frozen] IQualitySizeGuideService guide, - QualitySizeGuidePhase sut) + QualitySizeConfigPhase sut) { var config = Substitute.For(); config.QualityDefinition.Returns(new QualityDefinitionConfig {Type = "not_real"}); @@ -32,9 +34,11 @@ public class QualitySizeGuidePhaseTest new QualitySizeData {Type = "real"} }); - var result = sut.Execute(config); + var context = new QualitySizePipelineContext(); - result.Should().BeNull(); + sut.Execute(context, config); + + context.ConfigOutput.Should().BeNull(); } [Test] @@ -44,7 +48,7 @@ public class QualitySizeGuidePhaseTest string testPreferred, string expectedPreferred, [Frozen] IQualitySizeGuideService guide, - QualitySizeGuidePhase sut) + QualitySizeConfigPhase sut) { var config = Substitute.For(); config.QualityDefinition.Returns(new QualityDefinitionConfig @@ -58,7 +62,9 @@ public class QualitySizeGuidePhaseTest new QualitySizeData {Type = "real"} }); - _ = sut.Execute(config); + var context = new QualitySizePipelineContext(); + + sut.Execute(context, config); config.QualityDefinition.Should().NotBeNull(); config.QualityDefinition!.PreferredRatio.Should().Be(decimal.Parse(expectedPreferred)); @@ -67,7 +73,7 @@ public class QualitySizeGuidePhaseTest [Test, AutoMockData] public void Preferred_is_set_via_ratio( [Frozen] IQualitySizeGuideService guide, - QualitySizeGuidePhase sut) + QualitySizeConfigPhase sut) { var config = Substitute.For(); config.QualityDefinition.Returns(new QualityDefinitionConfig @@ -88,9 +94,12 @@ public class QualitySizeGuidePhaseTest } }); - var result = sut.Execute(config); - result.Should().NotBeNull(); - result!.Qualities.Should().BeEquivalentTo(new[] + var context = new QualitySizePipelineContext(); + + sut.Execute(context, config); + + context.ConfigOutput.Should().NotBeNull(); + context.ConfigOutput!.Qualities.Should().BeEquivalentTo(new[] { new QualitySizeItem("quality1", 0, 100, 50) }, @@ -104,7 +113,7 @@ public class QualitySizeGuidePhaseTest [Test, AutoMockData] public void Preferred_is_set_via_guide( [Frozen] IQualitySizeGuideService guide, - QualitySizeGuidePhase sut) + QualitySizeConfigPhase sut) { var config = Substitute.For(); config.QualityDefinition.Returns(new QualityDefinitionConfig @@ -124,9 +133,12 @@ public class QualitySizeGuidePhaseTest } }); - var result = sut.Execute(config); - result.Should().NotBeNull(); - result!.Qualities.Should().BeEquivalentTo(new[] + var context = new QualitySizePipelineContext(); + + sut.Execute(context, config); + + context.ConfigOutput.Should().NotBeNull(); + context.ConfigOutput!.Qualities.Should().BeEquivalentTo(new[] { new QualitySizeItem("quality1", 0, 100, 90) }, diff --git a/tests/Recyclarr.Cli.Tests/Pipelines/QualitySize/PipelinePhases/QualitySizeTransactionPhaseTest.cs b/tests/Recyclarr.Cli.Tests/Pipelines/QualitySize/PipelinePhases/QualitySizeTransactionPhaseTest.cs index 0c16f8f4..0defbcc3 100644 --- a/tests/Recyclarr.Cli.Tests/Pipelines/QualitySize/PipelinePhases/QualitySizeTransactionPhaseTest.cs +++ b/tests/Recyclarr.Cli.Tests/Pipelines/QualitySize/PipelinePhases/QualitySizeTransactionPhaseTest.cs @@ -1,3 +1,4 @@ +using Recyclarr.Cli.Pipelines.QualitySize; using Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; using Recyclarr.ServarrApi.QualityDefinition; using Recyclarr.TrashGuide.QualitySize; @@ -11,89 +12,104 @@ public class QualitySizeTransactionPhaseTest public void Skip_guide_qualities_that_do_not_exist_in_service( QualitySizeTransactionPhase sut) { - var guideData = new[] + var context = new QualitySizePipelineContext { - new QualitySizeItem("non_existent1", 0, 2, 1), - new QualitySizeItem("non_existent2", 0, 2, 1) - }; - - var serviceData = new List - { - new() + ConfigOutput = new QualitySizeData { - Quality = new ServiceQualityItem {Name = "exists"} + Qualities = new[] + { + new QualitySizeItem("non_existent1", 0, 2, 1), + new QualitySizeItem("non_existent2", 0, 2, 1) + } + }, + ApiFetchOutput = new List + { + new() + { + Quality = new ServiceQualityItem {Name = "exists"} + } } }; - var result = sut.Execute(guideData, serviceData); + sut.Execute(context); - result.Should().BeEmpty(); + context.TransactionOutput.Should().BeEmpty(); } [Test, AutoMockData] public void Skip_guide_qualities_that_are_not_different_from_service( QualitySizeTransactionPhase sut) { - var guideData = new[] + var context = new QualitySizePipelineContext { - new QualitySizeItem("same1", 0, 2, 1), - new QualitySizeItem("same2", 0, 2, 1) - }; - - var serviceData = new List - { - new() + ConfigOutput = new QualitySizeData { - Quality = new ServiceQualityItem {Name = "same1"}, - MinSize = 0, - MaxSize = 2, - PreferredSize = 1 + Qualities = new[] + { + new QualitySizeItem("same1", 0, 2, 1), + new QualitySizeItem("same2", 0, 2, 1) + } }, - new() + ApiFetchOutput = new List { - Quality = new ServiceQualityItem {Name = "same2"}, - MinSize = 0, - MaxSize = 2, - PreferredSize = 1 + new() + { + Quality = new ServiceQualityItem {Name = "same1"}, + MinSize = 0, + MaxSize = 2, + PreferredSize = 1 + }, + new() + { + Quality = new ServiceQualityItem {Name = "same2"}, + MinSize = 0, + MaxSize = 2, + PreferredSize = 1 + } } }; - var result = sut.Execute(guideData, serviceData); + sut.Execute(context); - result.Should().BeEmpty(); + context.TransactionOutput.Should().BeEmpty(); } [Test, AutoMockData] public void Sync_guide_qualities_that_are_different_from_service( QualitySizeTransactionPhase sut) { - var guideData = new[] - { - new QualitySizeItem("same1", 0, 2, 1), - new QualitySizeItem("different1", 0, 3, 1) - }; - - var serviceData = new List + var context = new QualitySizePipelineContext { - new() + ConfigOutput = new QualitySizeData { - Quality = new ServiceQualityItem {Name = "same1"}, - MinSize = 0, - MaxSize = 2, - PreferredSize = 1 + Qualities = new[] + { + new QualitySizeItem("same1", 0, 2, 1), + new QualitySizeItem("different1", 0, 3, 1) + } }, - new() + ApiFetchOutput = new List { - Quality = new ServiceQualityItem {Name = "different1"}, - MinSize = 0, - MaxSize = 2, - PreferredSize = 1 + new() + { + Quality = new ServiceQualityItem {Name = "same1"}, + MinSize = 0, + MaxSize = 2, + PreferredSize = 1 + }, + new() + { + Quality = new ServiceQualityItem {Name = "different1"}, + MinSize = 0, + MaxSize = 2, + PreferredSize = 1 + } } }; - var result = sut.Execute(guideData, serviceData); + sut.Execute(context); - result.Should().BeEquivalentTo(new List + context.TransactionOutput.Should().BeEquivalentTo(new List { new() {