refactor: Make quality size pipeline generic

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

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

@ -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<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.ServarrApi.QualityDefinition;
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);
log.Information("Number of updated qualities: {Count}", serverQuality.Count);
await api.UpdateQualityDefinition(config, context.TransactionOutput);
}
}

@ -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<QualitySizePipelineContext>
{
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)

@ -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;
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();
@ -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);

@ -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<QualitySizePipelineContext>
{
public Collection<ServiceQualityDefinitionItem> Execute(
IEnumerable<QualitySizeItem> guideQuality,
IList<ServiceQualityDefinitionItem> 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<ServiceQualityDefinitionItem>();
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)

@ -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<QualitySizeDataLister>();
builder.RegisterAggregateService<IQualitySizePipelinePhases>();
builder.RegisterType<QualitySizeGuidePhase>();
builder.RegisterType<QualitySizePreviewPhase>();
builder.RegisterType<QualitySizeApiFetchPhase>();
builder.RegisterType<QualitySizeTransactionPhase>();
builder.RegisterType<QualitySizeApiPersistencePhase>();
builder.RegisterTypes(
typeof(QualitySizeConfigPhase),
typeof(QualitySizePreviewPhase),
typeof(QualitySizeApiFetchPhase),
typeof(QualitySizeTransactionPhase),
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 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<IServiceConfiguration>();
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<IServiceConfiguration>();
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<IServiceConfiguration>();
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<IServiceConfiguration>();
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<IServiceConfiguration>();
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)
},

@ -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<ServiceQualityDefinitionItem>
{
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<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]
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<ServiceQualityDefinitionItem>
{
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<ServiceQualityDefinitionItem>
{
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<ServiceQualityDefinitionItem>
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<ServiceQualityDefinitionItem>
{
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<ServiceQualityDefinitionItem>
context.TransactionOutput.Should().BeEquivalentTo(new List<ServiceQualityDefinitionItem>
{
new()
{

Loading…
Cancel
Save