refactor: IGuideService abstracts where the CF data comes from

pull/124/head
Robert Dailey 2 years ago
parent 73fb40de04
commit cafa181424

@ -33,6 +33,7 @@ internal class RadarrCommand : ServiceCommand
var customFormatUpdaterFactory = container.Resolve<Func<ICustomFormatUpdater>>();
var qualityUpdaterFactory = container.Resolve<Func<IRadarrQualityDefinitionUpdater>>();
var configLoader = container.Resolve<IConfigurationLoader<RadarrConfiguration>>();
var guideService = container.Resolve<IRadarrGuideService>();
if (ListCustomFormats)
{
@ -57,7 +58,7 @@ internal class RadarrCommand : ServiceCommand
if (config.CustomFormats.Count > 0)
{
await customFormatUpdaterFactory().Process(Preview, config.CustomFormats);
await customFormatUpdaterFactory().Process(Preview, config.CustomFormats, guideService);
}
}
}

@ -9,6 +9,7 @@ using TrashLib.Services.Sonarr;
using TrashLib.Services.Sonarr.Config;
using TrashLib.Services.Sonarr.QualityDefinition;
using TrashLib.Services.Sonarr.ReleaseProfile;
using TrashLib.Services.Sonarr.ReleaseProfile.Guide;
namespace Recyclarr.Command;
@ -47,6 +48,7 @@ public class SonarrCommand : ServiceCommand
var configLoader = container.Resolve<IConfigurationLoader<SonarrConfiguration>>();
var log = container.Resolve<ILogger>();
var customFormatUpdaterFactory = container.Resolve<Func<ICustomFormatUpdater>>();
var guideService = container.Resolve<ISonarrGuideService>();
if (ListReleaseProfiles)
{
@ -97,7 +99,7 @@ public class SonarrCommand : ServiceCommand
if (config.CustomFormats.Count > 0)
{
await customFormatUpdaterFactory().Process(Preview, config.CustomFormats);
await customFormatUpdaterFactory().Process(Preview, config.CustomFormats, guideService);
}
}
}

@ -52,7 +52,7 @@ public class GuideProcessorTest
{
var ctx = new Context();
var guideService = Substitute.For<IRadarrGuideService>();
var guideProcessor = new GuideProcessor(guideService, () => new TestGuideProcessorSteps());
var guideProcessor = new GuideProcessor(() => new TestGuideProcessorSteps());
// simulate guide data
guideService.GetCustomFormatData().Returns(new[]
@ -86,7 +86,7 @@ public class GuideProcessorTest
}
};
await guideProcessor.BuildGuideDataAsync(config, null);
await guideProcessor.BuildGuideDataAsync(config, null, guideService);
var expectedProcessedCustomFormatData = new List<ProcessedCustomFormatData>
{

@ -0,0 +1,8 @@
using TrashLib.Services.CustomFormat.Models;
namespace TrashLib.Services.Common;
public interface IGuideService
{
ICollection<CustomFormatData> GetCustomFormatData();
}

@ -2,6 +2,7 @@ using CliFx.Infrastructure;
using Common.Extensions;
using Serilog;
using TrashLib.Config.Services;
using TrashLib.Services.Common;
using TrashLib.Services.CustomFormat.Processors;
using TrashLib.Services.CustomFormat.Processors.PersistenceSteps;
@ -33,11 +34,11 @@ internal class CustomFormatUpdater : ICustomFormatUpdater
private ILogger Log { get; }
public async Task Process(bool isPreview, IEnumerable<CustomFormatConfig> customFormats)
public async Task Process(bool isPreview, IEnumerable<CustomFormatConfig> customFormats, IGuideService guideService)
{
_cache.Load();
await _guideProcessor.BuildGuideDataAsync(customFormats, _cache.CfCache);
await _guideProcessor.BuildGuideDataAsync(customFormats, _cache.CfCache, guideService);
if (!ValidateGuideDataAndCheckShouldProceed())
{
@ -129,7 +130,7 @@ internal class CustomFormatUpdater : ICustomFormatUpdater
var totalCount = created.Count + updated.Count;
if (totalCount > 0)
{
Log.Information("Total of {Count} custom formats synced to Radarr", totalCount);
Log.Information("Total of {Count} custom formats were synced", totalCount);
}
else
{
@ -195,7 +196,7 @@ internal class CustomFormatUpdater : ICustomFormatUpdater
if (_guideProcessor.CustomFormatsWithoutScore.Count > 0)
{
Log.Information("The below custom formats have no score in the guide or in your YAML config. They will " +
"still be synced to Radarr, but no score will be set in your chosen quality profiles");
"still be synced, but no score will be set in your chosen quality profiles");
foreach (var tuple in _guideProcessor.CustomFormatsWithoutScore)
{
Log.Information("{CfList}", tuple);

@ -20,10 +20,10 @@ public class CustomFormatParser : ICustomFormatParser
finalScore = (int) score;
}
// Remove any properties starting with "trash_". Those are metadata that are not meant for Radarr itself Radarr
// supposedly drops this anyway, but I prefer it to be removed. ToList() is important here since removing the
// property itself modifies the collection, and we don't want the collection to get modified while still looping
// over it.
// Remove any properties starting with "trash_". Those are metadata that are not meant for the remote service
// itself. The service supposedly drops this anyway, but I prefer it to be removed. ToList() is important here
// since removing the property itself modifies the collection, and we don't want the collection to get modified
// while still looping over it.
foreach (var trashProperty in obj.Properties().Where(x => Regex.IsMatch(x.Name, @"^trash_")).ToList())
{
trashProperty.Remove();

@ -1,8 +1,9 @@
using TrashLib.Config.Services;
using TrashLib.Services.Common;
namespace TrashLib.Services.CustomFormat;
public interface ICustomFormatUpdater
{
Task Process(bool isPreview, IEnumerable<CustomFormatConfig> config);
Task Process(bool isPreview, IEnumerable<CustomFormatConfig> config, IGuideService guideService);
}

@ -1,8 +1,8 @@
using TrashLib.Config.Services;
using TrashLib.Services.Common;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
using TrashLib.Services.CustomFormat.Processors.GuideSteps;
using TrashLib.Services.Radarr;
namespace TrashLib.Services.CustomFormat.Processors;
@ -15,14 +15,12 @@ public interface IGuideProcessorSteps
internal class GuideProcessor : IGuideProcessor
{
private readonly IRadarrGuideService _guideService;
private readonly Func<IGuideProcessorSteps> _stepsFactory;
private IList<CustomFormatData>? _guideCustomFormatJson;
private IGuideProcessorSteps _steps;
public GuideProcessor(IRadarrGuideService guideService, Func<IGuideProcessorSteps> stepsFactory)
public GuideProcessor(Func<IGuideProcessorSteps> stepsFactory)
{
_guideService = guideService;
_stepsFactory = stepsFactory;
_steps = stepsFactory();
}
@ -51,12 +49,10 @@ internal class GuideProcessor : IGuideProcessor
public IDictionary<string, List<ProcessedCustomFormatData>> DuplicatedCustomFormats
=> _steps.CustomFormat.DuplicatedCustomFormats;
public Task BuildGuideDataAsync(IEnumerable<CustomFormatConfig> config, CustomFormatCache? cache)
public Task BuildGuideDataAsync(IEnumerable<CustomFormatConfig> config, CustomFormatCache? cache,
IGuideService guideService)
{
if (_guideCustomFormatJson == null)
{
_guideCustomFormatJson = _guideService.GetCustomFormatData().ToList();
}
_guideCustomFormatJson ??= guideService.GetCustomFormatData().ToList();
var listOfConfigs = config.ToList();

@ -1,4 +1,5 @@
using TrashLib.Config.Services;
using TrashLib.Services.Common;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
@ -15,6 +16,8 @@ internal interface IGuideProcessor
IReadOnlyCollection<(string, string)> CustomFormatsWithOutdatedNames { get; }
IDictionary<string, List<ProcessedCustomFormatData>> DuplicatedCustomFormats { get; }
Task BuildGuideDataAsync(IEnumerable<CustomFormatConfig> config, CustomFormatCache? cache);
Task BuildGuideDataAsync(IEnumerable<CustomFormatConfig> config, CustomFormatCache? cache,
IGuideService guideService);
void Reset();
}

@ -52,18 +52,18 @@ internal class PersistenceProcessor : IPersistenceProcessor
IEnumerable<TrashIdMapping> deletedCfsInCache,
IDictionary<string, QualityProfileCustomFormatScoreMapping> profileScores)
{
var radarrCfs = await _customFormatService.GetCustomFormats();
var serviceCfs = 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);
_steps.JsonTransactionStep.Process(guideCfs, serviceCfs);
// Step 1.1: Optionally record deletions of custom formats in cache but not in the guide
var config = _configProvider.ActiveConfiguration;
if (config.DeleteOldCustomFormats)
{
_steps.JsonTransactionStep.RecordDeletions(deletedCfsInCache, radarrCfs);
_steps.JsonTransactionStep.RecordDeletions(deletedCfsInCache, serviceCfs);
}
// Step 2: For each merged CF, persist it to Radarr via its API. This will involve a combination of updates

@ -9,7 +9,7 @@ public interface IJsonTransactionStep
CustomFormatTransactionData Transactions { get; }
void Process(IEnumerable<ProcessedCustomFormatData> guideCfs,
IReadOnlyCollection<JObject> radarrCfs);
IReadOnlyCollection<JObject> serviceCfs);
void RecordDeletions(IEnumerable<TrashIdMapping> deletedCfsInCache, IEnumerable<JObject> radarrCfs);
void RecordDeletions(IEnumerable<TrashIdMapping> deletedCfsInCache, IEnumerable<JObject> serviceCfs);
}

@ -19,15 +19,15 @@ internal class JsonTransactionStep : IJsonTransactionStep
public CustomFormatTransactionData Transactions { get; } = new();
public void Process(IEnumerable<ProcessedCustomFormatData> guideCfs,
IReadOnlyCollection<JObject> radarrCfs)
IReadOnlyCollection<JObject> serviceCfs)
{
foreach (var (guideCf, radarrCf) in guideCfs
.Select(gcf => (GuideCf: gcf, RadarrCf: FindRadarrCf(radarrCfs, gcf))))
foreach (var (guideCf, serviceCf) in guideCfs
.Select(gcf => (GuideCf: gcf, ServiceCf: FindServiceCf(serviceCfs, gcf))))
{
var guideCfJson = BuildNewRadarrCf(guideCf.Json);
var guideCfJson = BuildNewServiceCf(guideCf.Json);
// no match; we add this CF as brand new
if (radarrCf == null)
if (serviceCf == null)
{
guideCf.Json = guideCfJson;
Transactions.NewCustomFormats.Add(guideCf);
@ -35,8 +35,8 @@ internal class JsonTransactionStep : IJsonTransactionStep
// found match in radarr CFs; update the existing CF
else
{
guideCf.Json = (JObject) radarrCf.DeepClone();
UpdateRadarrCf(guideCf.Json, guideCfJson);
guideCf.Json = (JObject) serviceCf.DeepClone();
UpdateServiceCf(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
@ -46,7 +46,7 @@ internal class JsonTransactionStep : IJsonTransactionStep
guideCf.SetCache(guideCf.Json.Value<int>("id"));
}
if (!JToken.DeepEquals(radarrCf, guideCf.Json))
if (!JToken.DeepEquals(serviceCf, guideCf.Json))
{
Transactions.UpdatedCustomFormats.Add(guideCf);
}
@ -58,96 +58,96 @@ internal class JsonTransactionStep : IJsonTransactionStep
}
}
public void RecordDeletions(IEnumerable<TrashIdMapping> deletedCfsInCache, IEnumerable<JObject> radarrCfs)
public void RecordDeletions(IEnumerable<TrashIdMapping> deletedCfsInCache, IEnumerable<JObject> serviceCfs)
{
var cfs = radarrCfs.ToList();
var cfs = serviceCfs.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))
del => FindServiceCf(cfs, del.CustomFormatId, null) != null))
{
Transactions.DeletedCustomFormatIds.Add(del);
}
}
private static JObject? FindRadarrCf(IReadOnlyCollection<JObject> radarrCfs, ProcessedCustomFormatData guideCf)
private static JObject? FindServiceCf(IReadOnlyCollection<JObject> serviceCfs, ProcessedCustomFormatData guideCf)
{
return FindRadarrCf(radarrCfs, guideCf.CacheEntry?.CustomFormatId, guideCf.Name);
return FindServiceCf(serviceCfs, guideCf.CacheEntry?.CustomFormatId, guideCf.Name);
}
private static JObject? FindRadarrCf(IReadOnlyCollection<JObject> radarrCfs, int? cfId, string? cfName)
private static JObject? FindServiceCf(IReadOnlyCollection<JObject> serviceCfs, int? cfId, string? cfName)
{
JObject? match = null;
// Try to find match in cache first
if (cfId != null)
{
match = radarrCfs.FirstOrDefault(rcf => cfId == rcf.Value<int>("id"));
match = serviceCfs.FirstOrDefault(rcf => cfId == rcf.Value<int>("id"));
}
// 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<string>("name")));
match = serviceCfs.FirstOrDefault(rcf => cfName.EqualsIgnoreCase(rcf.Value<string>("name")));
}
return match;
}
private static void UpdateRadarrCf(JObject cfToModify, JObject cfToMergeFrom)
private static void UpdateServiceCf(JObject cfToModify, JObject cfToMergeFrom)
{
MergeProperties(cfToModify, cfToMergeFrom, JTokenType.Array);
var radarrSpecs = cfToModify["specifications"]?.Children<JObject>() ?? new JEnumerable<JObject>();
var serviceSpecs = cfToModify["specifications"]?.Children<JObject>() ?? new JEnumerable<JObject>();
var guideSpecs = cfToMergeFrom["specifications"]?.Children<JObject>() ?? new JEnumerable<JObject>();
var matchedGuideSpecs = guideSpecs
.GroupBy(gs => radarrSpecs.FirstOrDefault(gss => KeyMatch(gss, gs, "name")))
.SelectMany(kvp => kvp.Select(gs => new {GuideSpec = gs, RadarrSpec = kvp.Key}));
.GroupBy(gs => serviceSpecs.FirstOrDefault(gss => KeyMatch(gss, gs, "name")))
.SelectMany(kvp => kvp.Select(gs => new {GuideSpec = gs, ServiceSpec = kvp.Key}));
var newRadarrSpecs = new JArray();
var newServiceSpecs = new JArray();
foreach (var match in matchedGuideSpecs)
{
if (match.RadarrSpec != null)
if (match.ServiceSpec != null)
{
MergeProperties(match.RadarrSpec, match.GuideSpec);
newRadarrSpecs.Add(match.RadarrSpec);
MergeProperties(match.ServiceSpec, match.GuideSpec);
newServiceSpecs.Add(match.ServiceSpec);
}
else
{
newRadarrSpecs.Add(match.GuideSpec);
newServiceSpecs.Add(match.GuideSpec);
}
}
cfToModify["specifications"] = newRadarrSpecs;
cfToModify["specifications"] = newServiceSpecs;
}
private static bool KeyMatch(JObject left, JObject right, string keyName)
=> left.Value<string>(keyName) == right.Value<string>(keyName);
private static void MergeProperties(JObject radarrCf, JObject guideCfJson,
private static void MergeProperties(JObject serviceCf, JObject guideCfJson,
JTokenType exceptType = JTokenType.None)
{
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))
serviceCf.TryGetValue(guideProp.Name, out var serviceArray))
{
((JArray) radarrArray).Merge(guideProp.Value, new JsonMergeSettings
((JArray) serviceArray).Merge(guideProp.Value, new JsonMergeSettings
{
MergeArrayHandling = MergeArrayHandling.Merge
});
}
else
{
radarrCf[guideProp.Name] = guideProp.Value;
serviceCf[guideProp.Name] = guideProp.Value;
}
}
}
private static JObject BuildNewRadarrCf(JObject jsonPayload)
private static JObject BuildNewServiceCf(JObject jsonPayload)
{
// Information on required fields from nitsua
/*

@ -16,13 +16,13 @@ internal class QualityProfileApiPersistenceStep : IQualityProfileApiPersistenceS
public async Task Process(IQualityProfileService api,
IDictionary<string, QualityProfileCustomFormatScoreMapping> cfScores)
{
var radarrProfiles = await api.GetQualityProfiles();
var serviceProfiles = 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,
var profileScores = cfScores.GroupJoin(serviceProfiles,
s => s.Key,
p => p.Value<string>("name"),
(s, p) => (s.Key, s.Value, p.SelectMany(pi => pi.Children<JObject>("formatItems")).ToList()),

@ -1,10 +1,9 @@
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.Common;
using TrashLib.Services.Radarr.QualityDefinition;
namespace TrashLib.Services.Radarr;
public interface IRadarrGuideService
public interface IRadarrGuideService : IGuideService
{
ICollection<CustomFormatData> GetCustomFormatData();
ICollection<RadarrQualityData> GetQualities();
}

@ -1,12 +1,11 @@
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.Common;
using TrashLib.Services.Sonarr.QualityDefinition;
namespace TrashLib.Services.Sonarr.ReleaseProfile.Guide;
public interface ISonarrGuideService
public interface ISonarrGuideService : IGuideService
{
IReadOnlyCollection<ReleaseProfileData> GetReleaseProfileData();
ReleaseProfileData? GetUnfilteredProfileById(string trashId);
ICollection<SonarrQualityData> GetQualities();
IEnumerable<CustomFormatData> GetCustomFormatData();
}

@ -35,7 +35,7 @@ public class LocalRepoSonarrGuideService : ISonarrGuideService
public ICollection<SonarrQualityData> GetQualities()
=> _parser.GetQualities(_pathsFactory.Create().SonarrQualityPaths);
public IEnumerable<CustomFormatData> GetCustomFormatData()
public ICollection<CustomFormatData> GetCustomFormatData()
{
var paths = _pathsFactory.Create();
return _cfLoader.LoadAllCustomFormatsAtPaths(paths.SonarrCustomFormatPaths);

Loading…
Cancel
Save