refactor: Make quality size pipeline generic

spectre-console-remove-di-hacks
Robert Dailey 7 months ago
parent 647e0280ec
commit b6a53e497c

@ -83,7 +83,7 @@ public static class CompositionRoot
typeof(GenericSyncPipeline<TagPipelineContext>), typeof(GenericSyncPipeline<TagPipelineContext>),
typeof(CustomFormatSyncPipeline), typeof(CustomFormatSyncPipeline),
typeof(QualityProfileSyncPipeline), typeof(QualityProfileSyncPipeline),
typeof(QualitySizeSyncPipeline), typeof(GenericSyncPipeline<QualitySizePipelineContext>),
typeof(GenericSyncPipeline<ReleaseProfilePipelineContext>), typeof(GenericSyncPipeline<ReleaseProfilePipelineContext>),
typeof(GenericSyncPipeline<MediaNamingPipelineContext>)) typeof(GenericSyncPipeline<MediaNamingPipelineContext>))
.As<ISyncPipeline>() .As<ISyncPipeline>()

@ -1,12 +1,14 @@
using Recyclarr.Cli.Pipelines.Generic;
using Recyclarr.Config.Models; using Recyclarr.Config.Models;
using Recyclarr.ServarrApi.QualityDefinition; using Recyclarr.ServarrApi.QualityDefinition;
namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases;
public class QualitySizeApiFetchPhase(IQualityDefinitionApiService api) public class QualitySizeApiFetchPhase(IQualityDefinitionApiService api)
: IApiFetchPipelinePhase<QualitySizePipelineContext>
{ {
public async Task<IList<ServiceQualityDefinitionItem>> Execute(IServiceConfiguration config) public async Task Execute(QualitySizePipelineContext context, IServiceConfiguration config)
{ {
return await api.GetQualityDefinition(config); context.ApiFetchOutput = await api.GetQualityDefinition(config);
} }
} }

@ -1,13 +1,14 @@
using Recyclarr.Cli.Pipelines.Generic;
using Recyclarr.Config.Models; using Recyclarr.Config.Models;
using Recyclarr.ServarrApi.QualityDefinition; using Recyclarr.ServarrApi.QualityDefinition;
namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases;
public class QualitySizeApiPersistencePhase(ILogger log, IQualityDefinitionApiService api) public class QualitySizeApiPersistencePhase(IQualityDefinitionApiService api)
: IApiPersistencePipelinePhase<QualitySizePipelineContext>
{ {
public async Task Execute(IServiceConfiguration config, IList<ServiceQualityDefinitionItem> serverQuality) public async Task Execute(QualitySizePipelineContext context, IServiceConfiguration config)
{ {
await api.UpdateQualityDefinition(config, serverQuality); await api.UpdateQualityDefinition(config, context.TransactionOutput);
log.Information("Number of updated qualities: {Count}", serverQuality.Count);
} }
} }

@ -1,18 +1,20 @@
using Recyclarr.Cli.Pipelines.Generic;
using Recyclarr.Common.Extensions; using Recyclarr.Common.Extensions;
using Recyclarr.Config.Models; using Recyclarr.Config.Models;
using Recyclarr.TrashGuide.QualitySize; using Recyclarr.TrashGuide.QualitySize;
namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases;
public class QualitySizeGuidePhase(ILogger log, IQualitySizeGuideService guide) public class QualitySizeConfigPhase(ILogger log, IQualitySizeGuideService guide)
: IConfigPipelinePhase<QualitySizePipelineContext>
{ {
public QualitySizeData? Execute(IServiceConfiguration config) public Task Execute(QualitySizePipelineContext context, IServiceConfiguration config)
{ {
var qualityDef = config.QualityDefinition; var qualityDef = config.QualityDefinition;
if (qualityDef is null) if (qualityDef is null)
{ {
log.Debug("{Instance} has no quality definition", config.InstanceName); log.Debug("{Instance} has no quality definition", config.InstanceName);
return null; return Task.CompletedTask;
} }
var qualityDefinitions = guide.GetQualitySizeData(config.ServiceType); var qualityDefinitions = guide.GetQualitySizeData(config.ServiceType);
@ -21,13 +23,13 @@ public class QualitySizeGuidePhase(ILogger log, IQualitySizeGuideService guide)
if (selectedQuality == null) if (selectedQuality == null)
{ {
log.Error("The specified quality definition type does not exist: {Type}", qualityDef.Type); context.ConfigError = $"The specified quality definition type does not exist: {qualityDef.Type}";
return null; return Task.CompletedTask;
} }
log.Information("Processing Quality Definition: {QualityDefinition}", qualityDef.Type);
AdjustPreferredRatio(qualityDef, selectedQuality); AdjustPreferredRatio(qualityDef, selectedQuality);
return selectedQuality; context.ConfigOutput = selectedQuality;
return Task.CompletedTask;
} }
private void AdjustPreferredRatio(QualityDefinitionConfig config, QualitySizeData selectedQuality) private void AdjustPreferredRatio(QualityDefinitionConfig config, QualitySizeData selectedQuality)

@ -0,0 +1,29 @@
using Recyclarr.Cli.Pipelines.Generic;
namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases;
public class QualitySizeLogPhase(ILogger log) : ILogPipelinePhase<QualitySizePipelineContext>
{
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);
}
}

@ -1,11 +1,11 @@
using Recyclarr.TrashGuide.QualitySize; using Recyclarr.Cli.Pipelines.Generic;
using Spectre.Console; using Spectre.Console;
namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases;
public class QualitySizePreviewPhase(IAnsiConsole console) public class QualitySizePreviewPhase(IAnsiConsole console) : IPreviewPipelinePhase<QualitySizePipelineContext>
{ {
public void Execute(QualitySizeData selectedQuality) public void Execute(QualitySizePipelineContext context)
{ {
var table = new Table(); var table = new Table();
@ -15,7 +15,8 @@ public class QualitySizePreviewPhase(IAnsiConsole console)
table.AddColumn("[bold]Max[/]"); table.AddColumn("[bold]Max[/]");
table.AddColumn("[bold]Preferred[/]"); 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}[/]"; var quality = $"[dodgerblue1]{q.Quality}[/]";
table.AddRow(quality, q.AnnotatedMin, q.AnnotatedMax, q.AnnotatedPreferred); table.AddRow(quality, q.AnnotatedMin, q.AnnotatedMax, q.AnnotatedPreferred);

@ -1,15 +1,18 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using Recyclarr.Cli.Pipelines.Generic;
using Recyclarr.ServarrApi.QualityDefinition; using Recyclarr.ServarrApi.QualityDefinition;
using Recyclarr.TrashGuide.QualitySize; using Recyclarr.TrashGuide.QualitySize;
namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; namespace Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases;
public class QualitySizeTransactionPhase(ILogger log) public class QualitySizeTransactionPhase(ILogger log) : ITransactionPipelinePhase<QualitySizePipelineContext>
{ {
public Collection<ServiceQualityDefinitionItem> Execute( public void Execute(QualitySizePipelineContext context)
IEnumerable<QualitySizeItem> guideQuality,
IList<ServiceQualityDefinitionItem> serverQuality)
{ {
// 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<ServiceQualityDefinitionItem>(); var newQuality = new Collection<ServiceQualityDefinitionItem>();
foreach (var qualityData in guideQuality) foreach (var qualityData in guideQuality)
{ {
@ -37,7 +40,7 @@ public class QualitySizeTransactionPhase(ILogger log)
serverEntry.PreferredSize); serverEntry.PreferredSize);
} }
return newQuality; context.TransactionOutput = newQuality;
} }
private static bool QualityIsDifferent(ServiceQualityDefinitionItem a, QualitySizeItem b) private static bool QualityIsDifferent(ServiceQualityDefinitionItem a, QualitySizeItem b)

@ -1,5 +1,4 @@
using Autofac; using Autofac;
using Autofac.Extras.AggregateService;
using Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; using Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases;
namespace Recyclarr.Cli.Pipelines.QualitySize; namespace Recyclarr.Cli.Pipelines.QualitySize;
@ -11,11 +10,13 @@ public class QualitySizeAutofacModule : Module
base.Load(builder); base.Load(builder);
builder.RegisterType<QualitySizeDataLister>(); builder.RegisterType<QualitySizeDataLister>();
builder.RegisterAggregateService<IQualitySizePipelinePhases>(); builder.RegisterTypes(
builder.RegisterType<QualitySizeGuidePhase>(); typeof(QualitySizeConfigPhase),
builder.RegisterType<QualitySizePreviewPhase>(); typeof(QualitySizePreviewPhase),
builder.RegisterType<QualitySizeApiFetchPhase>(); typeof(QualitySizeApiFetchPhase),
builder.RegisterType<QualitySizeTransactionPhase>(); typeof(QualitySizeTransactionPhase),
builder.RegisterType<QualitySizeApiPersistencePhase>(); typeof(QualitySizeApiPersistencePhase),
typeof(QualitySizeLogPhase))
.AsImplementedInterfaces();
} }
} }

@ -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<SupportedServices> SupportedServiceTypes { get; } = new[]
{
SupportedServices.Sonarr,
SupportedServices.Radarr
};
public QualitySizeData? ConfigOutput { get; set; }
public IList<ServiceQualityDefinitionItem> ApiFetchOutput { get; set; } = default!;
public IList<ServiceQualityDefinitionItem> TransactionOutput { get; set; } = default!;
public string? ConfigError { get; set; }
}

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

@ -1,4 +1,5 @@
using NSubstitute.ReturnsExtensions; using NSubstitute.ReturnsExtensions;
using Recyclarr.Cli.Pipelines.QualitySize;
using Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; using Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases;
using Recyclarr.Config.Models; using Recyclarr.Config.Models;
using Recyclarr.TrashGuide.QualitySize; using Recyclarr.TrashGuide.QualitySize;
@ -6,23 +7,24 @@ using Recyclarr.TrashGuide.QualitySize;
namespace Recyclarr.Cli.Tests.Pipelines.QualitySize.PipelinePhases; namespace Recyclarr.Cli.Tests.Pipelines.QualitySize.PipelinePhases;
[TestFixture] [TestFixture]
public class QualitySizeGuidePhaseTest public class QualitySizeConfigPhaseTest
{ {
[Test, AutoMockData] [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<IServiceConfiguration>(); var config = Substitute.For<IServiceConfiguration>();
config.QualityDefinition.ReturnsNull(); config.QualityDefinition.ReturnsNull();
var result = sut.Execute(config); sut.Execute(context, config);
result.Should().BeNull(); context.ConfigOutput.Should().BeNull();
} }
[Test, AutoMockData] [Test, AutoMockData]
public void Do_nothing_if_no_matching_quality_definition( public void Do_nothing_if_no_matching_quality_definition(
[Frozen] IQualitySizeGuideService guide, [Frozen] IQualitySizeGuideService guide,
QualitySizeGuidePhase sut) QualitySizeConfigPhase sut)
{ {
var config = Substitute.For<IServiceConfiguration>(); var config = Substitute.For<IServiceConfiguration>();
config.QualityDefinition.Returns(new QualityDefinitionConfig {Type = "not_real"}); config.QualityDefinition.Returns(new QualityDefinitionConfig {Type = "not_real"});
@ -32,9 +34,11 @@ public class QualitySizeGuidePhaseTest
new QualitySizeData {Type = "real"} 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] [Test]
@ -44,7 +48,7 @@ public class QualitySizeGuidePhaseTest
string testPreferred, string testPreferred,
string expectedPreferred, string expectedPreferred,
[Frozen] IQualitySizeGuideService guide, [Frozen] IQualitySizeGuideService guide,
QualitySizeGuidePhase sut) QualitySizeConfigPhase sut)
{ {
var config = Substitute.For<IServiceConfiguration>(); var config = Substitute.For<IServiceConfiguration>();
config.QualityDefinition.Returns(new QualityDefinitionConfig config.QualityDefinition.Returns(new QualityDefinitionConfig
@ -58,7 +62,9 @@ public class QualitySizeGuidePhaseTest
new QualitySizeData {Type = "real"} new QualitySizeData {Type = "real"}
}); });
_ = sut.Execute(config); var context = new QualitySizePipelineContext();
sut.Execute(context, config);
config.QualityDefinition.Should().NotBeNull(); config.QualityDefinition.Should().NotBeNull();
config.QualityDefinition!.PreferredRatio.Should().Be(decimal.Parse(expectedPreferred)); config.QualityDefinition!.PreferredRatio.Should().Be(decimal.Parse(expectedPreferred));
@ -67,7 +73,7 @@ public class QualitySizeGuidePhaseTest
[Test, AutoMockData] [Test, AutoMockData]
public void Preferred_is_set_via_ratio( public void Preferred_is_set_via_ratio(
[Frozen] IQualitySizeGuideService guide, [Frozen] IQualitySizeGuideService guide,
QualitySizeGuidePhase sut) QualitySizeConfigPhase sut)
{ {
var config = Substitute.For<IServiceConfiguration>(); var config = Substitute.For<IServiceConfiguration>();
config.QualityDefinition.Returns(new QualityDefinitionConfig config.QualityDefinition.Returns(new QualityDefinitionConfig
@ -88,9 +94,12 @@ public class QualitySizeGuidePhaseTest
} }
}); });
var result = sut.Execute(config); var context = new QualitySizePipelineContext();
result.Should().NotBeNull();
result!.Qualities.Should().BeEquivalentTo(new[] sut.Execute(context, config);
context.ConfigOutput.Should().NotBeNull();
context.ConfigOutput!.Qualities.Should().BeEquivalentTo(new[]
{ {
new QualitySizeItem("quality1", 0, 100, 50) new QualitySizeItem("quality1", 0, 100, 50)
}, },
@ -104,7 +113,7 @@ public class QualitySizeGuidePhaseTest
[Test, AutoMockData] [Test, AutoMockData]
public void Preferred_is_set_via_guide( public void Preferred_is_set_via_guide(
[Frozen] IQualitySizeGuideService guide, [Frozen] IQualitySizeGuideService guide,
QualitySizeGuidePhase sut) QualitySizeConfigPhase sut)
{ {
var config = Substitute.For<IServiceConfiguration>(); var config = Substitute.For<IServiceConfiguration>();
config.QualityDefinition.Returns(new QualityDefinitionConfig config.QualityDefinition.Returns(new QualityDefinitionConfig
@ -124,9 +133,12 @@ public class QualitySizeGuidePhaseTest
} }
}); });
var result = sut.Execute(config); var context = new QualitySizePipelineContext();
result.Should().NotBeNull();
result!.Qualities.Should().BeEquivalentTo(new[] sut.Execute(context, config);
context.ConfigOutput.Should().NotBeNull();
context.ConfigOutput!.Qualities.Should().BeEquivalentTo(new[]
{ {
new QualitySizeItem("quality1", 0, 100, 90) new QualitySizeItem("quality1", 0, 100, 90)
}, },

@ -1,3 +1,4 @@
using Recyclarr.Cli.Pipelines.QualitySize;
using Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases; using Recyclarr.Cli.Pipelines.QualitySize.PipelinePhases;
using Recyclarr.ServarrApi.QualityDefinition; using Recyclarr.ServarrApi.QualityDefinition;
using Recyclarr.TrashGuide.QualitySize; using Recyclarr.TrashGuide.QualitySize;
@ -11,89 +12,104 @@ public class QualitySizeTransactionPhaseTest
public void Skip_guide_qualities_that_do_not_exist_in_service( public void Skip_guide_qualities_that_do_not_exist_in_service(
QualitySizeTransactionPhase sut) QualitySizeTransactionPhase sut)
{ {
var guideData = new[] var context = new QualitySizePipelineContext
{ {
new QualitySizeItem("non_existent1", 0, 2, 1), ConfigOutput = new QualitySizeData
new QualitySizeItem("non_existent2", 0, 2, 1)
};
var serviceData = new List<ServiceQualityDefinitionItem>
{
new()
{ {
Quality = new ServiceQualityItem {Name = "exists"} Qualities = new[]
{
new QualitySizeItem("non_existent1", 0, 2, 1),
new QualitySizeItem("non_existent2", 0, 2, 1)
}
},
ApiFetchOutput = new List<ServiceQualityDefinitionItem>
{
new()
{
Quality = new ServiceQualityItem {Name = "exists"}
}
} }
}; };
var result = sut.Execute(guideData, serviceData); sut.Execute(context);
result.Should().BeEmpty(); context.TransactionOutput.Should().BeEmpty();
} }
[Test, AutoMockData] [Test, AutoMockData]
public void Skip_guide_qualities_that_are_not_different_from_service( public void Skip_guide_qualities_that_are_not_different_from_service(
QualitySizeTransactionPhase sut) QualitySizeTransactionPhase sut)
{ {
var guideData = new[] var context = new QualitySizePipelineContext
{ {
new QualitySizeItem("same1", 0, 2, 1), ConfigOutput = new QualitySizeData
new QualitySizeItem("same2", 0, 2, 1)
};
var serviceData = new List<ServiceQualityDefinitionItem>
{
new()
{ {
Quality = new ServiceQualityItem {Name = "same1"}, Qualities = new[]
MinSize = 0, {
MaxSize = 2, new QualitySizeItem("same1", 0, 2, 1),
PreferredSize = 1 new QualitySizeItem("same2", 0, 2, 1)
}
}, },
new() ApiFetchOutput = new List<ServiceQualityDefinitionItem>
{ {
Quality = new ServiceQualityItem {Name = "same2"}, new()
MinSize = 0, {
MaxSize = 2, Quality = new ServiceQualityItem {Name = "same1"},
PreferredSize = 1 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] [Test, AutoMockData]
public void Sync_guide_qualities_that_are_different_from_service( public void Sync_guide_qualities_that_are_different_from_service(
QualitySizeTransactionPhase sut) QualitySizeTransactionPhase sut)
{ {
var guideData = new[] var context = new QualitySizePipelineContext
{
new QualitySizeItem("same1", 0, 2, 1),
new QualitySizeItem("different1", 0, 3, 1)
};
var serviceData = new List<ServiceQualityDefinitionItem>
{ {
new() ConfigOutput = new QualitySizeData
{ {
Quality = new ServiceQualityItem {Name = "same1"}, Qualities = new[]
MinSize = 0, {
MaxSize = 2, new QualitySizeItem("same1", 0, 2, 1),
PreferredSize = 1 new QualitySizeItem("different1", 0, 3, 1)
}
}, },
new() ApiFetchOutput = new List<ServiceQualityDefinitionItem>
{ {
Quality = new ServiceQualityItem {Name = "different1"}, new()
MinSize = 0, {
MaxSize = 2, Quality = new ServiceQualityItem {Name = "same1"},
PreferredSize = 1 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<ServiceQualityDefinitionItem> context.TransactionOutput.Should().BeEquivalentTo(new List<ServiceQualityDefinitionItem>
{ {
new() new()
{ {

Loading…
Cancel
Save