From 8b45379cc89b674af12796f94d291b03f37ab282 Mon Sep 17 00:00:00 2001 From: Robert Dailey Date: Wed, 30 Jun 2021 23:15:57 -0500 Subject: [PATCH] fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Add initial Blazor Server project --- src/.editorconfig | 4 +- .../Code/Radarr/CustomFormatCache.cs | 7 +- src/Recyclarr/Code/Radarr/GuideProcessor.cs | 2 +- src/TrashLib/Radarr/Config/RadarrConfig.cs | 7 +- .../CustomFormat/Api/ICustomFormatService.cs | 15 +- .../Api/Models/CustomFormatData.cs | 25 +++ .../CustomFormat/Cache/ICustomFormatCache.cs | 1 + .../Models/ProcessedCustomFormatData.cs | 39 +++-- .../GuideSteps/CustomFormatProcessor.cs | 53 +----- .../GuideSteps/ICustomFormatProcessor.cs | 3 - .../GuideSteps/QualityProfileStep.cs | 12 +- .../Processors/PersistenceProcessor.cs | 162 +++++++++--------- .../CustomFormatApiPersistenceStep.cs | 24 ++- .../ICustomFormatApiPersistenceStep.cs | 1 - .../PersistenceSteps/IJsonTransactionStep.cs | 8 - .../PersistenceSteps/JsonTransactionStep.cs | 73 ++++---- 16 files changed, 205 insertions(+), 231 deletions(-) create mode 100644 src/TrashLib/Radarr/CustomFormat/Api/Models/CustomFormatData.cs diff --git a/src/.editorconfig b/src/.editorconfig index c82719e2..4f0356d9 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -1,4 +1,4 @@ - +# noinspection EditorConfigKeyCorrectness [*] charset = utf-8 end_of_line = crlf @@ -919,7 +919,7 @@ resharper_event_handler_pattern_short = On$event$ resharper_expression_braces = inside resharper_expression_pars = inside resharper_extra_spaces = remove_all -resharper_force_attribute_style = separate +resharper_force_attribute_style = join resharper_force_chop_compound_do_expression = false resharper_force_chop_compound_if_expression = false resharper_force_chop_compound_while_expression = false diff --git a/src/Recyclarr/Code/Radarr/CustomFormatCache.cs b/src/Recyclarr/Code/Radarr/CustomFormatCache.cs index 30a44f86..89dd0820 100644 --- a/src/Recyclarr/Code/Radarr/CustomFormatCache.cs +++ b/src/Recyclarr/Code/Radarr/CustomFormatCache.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; @@ -33,9 +32,7 @@ namespace Recyclarr.Code.Radarr }); } - public void Remove(TrashIdMapping cfId) - { - _context.CustomFormatCache.Remove(cfId); - } + public void Remove(TrashIdMapping cfId) => _context.CustomFormatCache.Remove(cfId); + public void Save() => _context.SaveChanges(); } } diff --git a/src/Recyclarr/Code/Radarr/GuideProcessor.cs b/src/Recyclarr/Code/Radarr/GuideProcessor.cs index 1e8a490b..43c0ea70 100644 --- a/src/Recyclarr/Code/Radarr/GuideProcessor.cs +++ b/src/Recyclarr/Code/Radarr/GuideProcessor.cs @@ -79,7 +79,7 @@ namespace Recyclarr.Code.Radarr // 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. - _jsonTransactionStep.Process(CustomFormats, radarrCfs); + _jsonTransactionStep.Process(CustomFormats, radarrCfs, config); // Step 1.1: Optionally record deletions of custom formats in cache but not in the guide if (config.DeleteOldCustomFormats) diff --git a/src/TrashLib/Radarr/Config/RadarrConfig.cs b/src/TrashLib/Radarr/Config/RadarrConfig.cs index 6e9bf67c..b0def217 100644 --- a/src/TrashLib/Radarr/Config/RadarrConfig.cs +++ b/src/TrashLib/Radarr/Config/RadarrConfig.cs @@ -9,8 +9,8 @@ namespace TrashLib.Radarr.Config public class RadarrConfig : ServiceConfiguration { public QualityDefinitionConfig? QualityDefinition { get; init; } - public ICollection CustomFormats { get; set; } // = new(); - public ICollection QualityProfiles { get; init; } // = new(); + public List CustomFormats { get; init; } = new(); + public List QualityProfiles { get; init; } = new(); public bool DeleteOldCustomFormats { get; init; } } @@ -22,7 +22,8 @@ namespace TrashLib.Radarr.Config public class QualityProfileConfig { - public ICollection Scores { get; init; } // = new(); + public string ProfileName { get; init; } = ""; + public List Scores { get; init; } = new(); public bool ResetUnmatchedScores { get; init; } } diff --git a/src/TrashLib/Radarr/CustomFormat/Api/ICustomFormatService.cs b/src/TrashLib/Radarr/CustomFormat/Api/ICustomFormatService.cs index 244a3c5a..054afc1c 100644 --- a/src/TrashLib/Radarr/CustomFormat/Api/ICustomFormatService.cs +++ b/src/TrashLib/Radarr/CustomFormat/Api/ICustomFormatService.cs @@ -1,24 +1,13 @@ using System.Collections.Generic; using System.Threading.Tasks; -using JetBrains.Annotations; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using TrashLib.Radarr.CustomFormat.Api.Models; using TrashLib.Radarr.CustomFormat.Models; namespace TrashLib.Radarr.CustomFormat.Api { - public class RadarrCustomFormatData - { - public int Id { get; set; } - public string Name { get; set; } - - [JsonExtensionData, UsedImplicitly] - private JObject? _extraJson; - } - public interface ICustomFormatService { - Task> GetCustomFormats(); + Task> GetCustomFormats(); Task CreateCustomFormat(ProcessedCustomFormatData cf); Task UpdateCustomFormat(int formatId, ProcessedCustomFormatData cf); Task DeleteCustomFormat(int formatId); diff --git a/src/TrashLib/Radarr/CustomFormat/Api/Models/CustomFormatData.cs b/src/TrashLib/Radarr/CustomFormat/Api/Models/CustomFormatData.cs new file mode 100644 index 00000000..7e4fa1ce --- /dev/null +++ b/src/TrashLib/Radarr/CustomFormat/Api/Models/CustomFormatData.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace TrashLib.Radarr.CustomFormat.Api.Models +{ + public class CustomFormatData + { + public int Id { get; set; } + public string Name { get; set; } + public List Specifications { get; set; } = new(); + + [JsonExtensionData, UsedImplicitly] + public JObject? ExtraJson { get; init; } + } + + public class SpecificationData + { + public string Name { get; set; } + + [JsonExtensionData, UsedImplicitly] + public JObject? ExtraJson { get; init; } + } +} diff --git a/src/TrashLib/Radarr/CustomFormat/Cache/ICustomFormatCache.cs b/src/TrashLib/Radarr/CustomFormat/Cache/ICustomFormatCache.cs index 3cff6fcc..336918aa 100644 --- a/src/TrashLib/Radarr/CustomFormat/Cache/ICustomFormatCache.cs +++ b/src/TrashLib/Radarr/CustomFormat/Cache/ICustomFormatCache.cs @@ -9,5 +9,6 @@ namespace TrashLib.Radarr.CustomFormat.Cache IEnumerable Mappings { get; } void Add(int formatId, ProcessedCustomFormatData format); void Remove(TrashIdMapping cfId); + void Save(); } } diff --git a/src/TrashLib/Radarr/CustomFormat/Models/ProcessedCustomFormatData.cs b/src/TrashLib/Radarr/CustomFormat/Models/ProcessedCustomFormatData.cs index 62688d5c..0186b8a9 100644 --- a/src/TrashLib/Radarr/CustomFormat/Models/ProcessedCustomFormatData.cs +++ b/src/TrashLib/Radarr/CustomFormat/Models/ProcessedCustomFormatData.cs @@ -1,28 +1,39 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using Newtonsoft.Json.Linq; -using TrashLib.Radarr.CustomFormat.Models.Cache; +using Newtonsoft.Json; +using TrashLib.Radarr.CustomFormat.Api.Models; namespace TrashLib.Radarr.CustomFormat.Models { public class ProcessedCustomFormatData { - public ProcessedCustomFormatData(string name, string trashId, JObject json) + public ProcessedCustomFormatData(string trashId, CustomFormatData data) { - Name = name; TrashId = trashId; - Json = json; + Data = data; } - public string Name { get; } + public string Name => Data.Name; public string TrashId { get; } public int? Score { get; init; } - public JObject Json { get; set; } - // public TrashIdMapping? CacheEntry { get; set; } - // [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 CustomFormatData Data { get; } + + public static ProcessedCustomFormatData CreateFromJson(string guideData) + { + var cfData = JsonConvert.DeserializeObject(guideData); + var trashId = (string) cfData.ExtraJson["trash_id"]; + int? finalScore = null; + + if (cfData.ExtraJson.TryGetValue("trash_score", out var score)) + { + finalScore = (int) score; + cfData.ExtraJson.Property("trash_score").Remove(); + } + + // 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 + cfData.ExtraJson.Property("trash_id").Remove(); + + return new ProcessedCustomFormatData(trashId, cfData) {Score = finalScore}; + } } } diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatProcessor.cs b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatProcessor.cs index 2da866e6..bd92a5d3 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatProcessor.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatProcessor.cs @@ -1,31 +1,19 @@ using System; using System.Collections.Generic; using System.Linq; -using Newtonsoft.Json.Linq; -using TrashLib.Config; using TrashLib.Radarr.Config; -using TrashLib.Radarr.CustomFormat.Cache; using TrashLib.Radarr.CustomFormat.Models; -using TrashLib.Radarr.CustomFormat.Models.Cache; namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps { internal class CustomFormatProcessor : ICustomFormatProcessor { - private readonly ICustomFormatCache _cache; - - public CustomFormatProcessor(ICustomFormatCache cache) - { - _cache = cache; - } - public List CustomFormats { get; } = new(); - public List DeletedCustomFormatsInCache { get; } = new(); public void Process(IEnumerable customFormatGuideData, RadarrConfig config) { var processedCfs = customFormatGuideData - .Select(jsonData => ProcessCustomFormatData(jsonData, config)) + .Select(ProcessedCustomFormatData.CreateFromJson) .ToList(); // For each ID listed under the `trash_ids` YML property, match it to an existing CF @@ -37,45 +25,6 @@ namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps cf => cf.TrashId, (_, cf) => cf, StringComparer.InvariantCultureIgnoreCase)); - - // Orphaned entries in cache represent custom formats we need to delete. - ProcessDeletedCustomFormats(config); - } - - private ProcessedCustomFormatData ProcessCustomFormatData( - string guideData, - IServiceConfiguration config) - { - JObject obj = JObject.Parse(guideData); - var name = (string) obj["name"]; - var trashId = (string) obj["trash_id"]; - int? finalScore = null; - - if (obj.TryGetValue("trash_score", out var score)) - { - finalScore = (int) score; - obj.Property("trash_score").Remove(); - } - - // 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(); - - return new ProcessedCustomFormatData(name, trashId, obj) - { - Score = finalScore, - CacheEntry = _cache.Load(config).FirstOrDefault(c => c.TrashId == trashId) - }; - } - - private void ProcessDeletedCustomFormats(RadarrConfig config) - { - 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.Load(config) - .Where(c => !CustomFormats.Any(cf => MatchCfInCache(cf, c)))); } } } diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/ICustomFormatProcessor.cs b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/ICustomFormatProcessor.cs index f88a72da..ebc9e467 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/ICustomFormatProcessor.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/ICustomFormatProcessor.cs @@ -1,15 +1,12 @@ using System.Collections.Generic; using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Models; -using TrashLib.Radarr.CustomFormat.Models.Cache; namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps { public interface ICustomFormatProcessor { List CustomFormats { get; } - List DeletedCustomFormatsInCache { get; } - void Process(IEnumerable customFormatGuideData, RadarrConfig config); } } diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStep.cs index d2d975d9..2825e2aa 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStep.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Linq; +using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Models; namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps @@ -8,22 +10,22 @@ namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps public Dictionary ProfileScores { get; } = new(); public List<(string name, string trashId, string profileName)> CustomFormatsWithoutScore { get; } = new(); - public void Process(IEnumerable configData) + public void Process(RadarrConfig config, IReadOnlyCollection customFormats) { - foreach (var config in configData) + // foreach (var config in configData) foreach (var profile in config.QualityProfiles) - foreach (var cf in config.CustomFormats) + foreach (var cfScore in profile.Scores) { // 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; + var scoreToUse = profile.Scores.FirstOrDefault(s => s.TrashId == cf.TrashId); if (scoreToUse == null) { if (cf.Score == null) { - CustomFormatsWithoutScore.Add((cf.Name, cf.TrashId, profile.Name)); + CustomFormatsWithoutScore.Add((cf.Name, cf.TrashId, profile.)); } else { diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceProcessor.cs b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceProcessor.cs index 729aa60b..67a7dffc 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceProcessor.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceProcessor.cs @@ -1,81 +1,81 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using TrashLib.Radarr.Config; -using TrashLib.Radarr.CustomFormat.Api; -using TrashLib.Radarr.CustomFormat.Models; -using TrashLib.Radarr.CustomFormat.Models.Cache; -using TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps; - -namespace TrashLib.Radarr.CustomFormat.Processors -{ - public interface IPersistenceProcessorSteps - { - public IJsonTransactionStep JsonTransactionStep { get; } - public ICustomFormatApiPersistenceStep CustomFormatCustomFormatApiPersister { get; } - public IQualityProfileApiPersistenceStep ProfileQualityProfileApiPersister { get; } - } - - internal class PersistenceProcessor : IPersistenceProcessor - { - private readonly Func _customFormatServiceFactory; - private readonly Func _qualityProfileServiceFactory; - private readonly Func _stepsFactory; - private IPersistenceProcessorSteps _steps; - - public PersistenceProcessor( - Func customFormatServiceFactory, - Func qualityProfileServiceFactory, - Func stepsFactory) - { - _qualityProfileServiceFactory = qualityProfileServiceFactory; - _stepsFactory = stepsFactory; - _customFormatServiceFactory = customFormatServiceFactory; - _steps = _stepsFactory(); - } - - public CustomFormatTransactionData Transactions - => _steps.JsonTransactionStep.Transactions; - - public IDictionary> UpdatedScores - => _steps.ProfileQualityProfileApiPersister.UpdatedScores; - - public IReadOnlyCollection InvalidProfileNames - => _steps.ProfileQualityProfileApiPersister.InvalidProfileNames; - - public void Reset() - { - _steps = _stepsFactory(); - } - - public async Task PersistCustomFormats( - RadarrConfig config, - IEnumerable guideCfs, - IEnumerable deletedCfsInCache, - IDictionary profileScores) - { - var customFormatService = _customFormatServiceFactory(config.BuildUrl()); - 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 - if (config.DeleteOldCustomFormats) - { - _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 3: Update all quality profiles with the scores from the guide for the uploaded custom formats - await _steps.ProfileQualityProfileApiPersister.Process( - _qualityProfileServiceFactory(config.BuildUrl()), profileScores); - } - } -} +// using System; +// using System.Collections.Generic; +// using System.Threading.Tasks; +// using TrashLib.Radarr.Config; +// using TrashLib.Radarr.CustomFormat.Api; +// using TrashLib.Radarr.CustomFormat.Models; +// using TrashLib.Radarr.CustomFormat.Models.Cache; +// using TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps; +// +// namespace TrashLib.Radarr.CustomFormat.Processors +// { +// public interface IPersistenceProcessorSteps +// { +// public IJsonTransactionStep JsonTransactionStep { get; } +// public ICustomFormatApiPersistenceStep CustomFormatCustomFormatApiPersister { get; } +// public IQualityProfileApiPersistenceStep ProfileQualityProfileApiPersister { get; } +// } +// +// internal class PersistenceProcessor : IPersistenceProcessor +// { +// private readonly Func _customFormatServiceFactory; +// private readonly Func _qualityProfileServiceFactory; +// private readonly Func _stepsFactory; +// private IPersistenceProcessorSteps _steps; +// +// public PersistenceProcessor( +// Func customFormatServiceFactory, +// Func qualityProfileServiceFactory, +// Func stepsFactory) +// { +// _qualityProfileServiceFactory = qualityProfileServiceFactory; +// _stepsFactory = stepsFactory; +// _customFormatServiceFactory = customFormatServiceFactory; +// _steps = _stepsFactory(); +// } +// +// public CustomFormatTransactionData Transactions +// => _steps.JsonTransactionStep.Transactions; +// +// public IDictionary> UpdatedScores +// => _steps.ProfileQualityProfileApiPersister.UpdatedScores; +// +// public IReadOnlyCollection InvalidProfileNames +// => _steps.ProfileQualityProfileApiPersister.InvalidProfileNames; +// +// public void Reset() +// { +// _steps = _stepsFactory(); +// } +// +// public async Task PersistCustomFormats( +// RadarrConfig config, +// IEnumerable guideCfs, +// IEnumerable deletedCfsInCache, +// IDictionary profileScores) +// { +// var customFormatService = _customFormatServiceFactory(config.BuildUrl()); +// 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 +// if (config.DeleteOldCustomFormats) +// { +// _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 3: Update all quality profiles with the scores from the guide for the uploaded custom formats +// await _steps.ProfileQualityProfileApiPersister.Process( +// _qualityProfileServiceFactory(config.BuildUrl()), profileScores); +// } +// } +// } diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/CustomFormatApiPersistenceStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/CustomFormatApiPersistenceStep.cs index 0d371bfb..8f93448d 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/CustomFormatApiPersistenceStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/CustomFormatApiPersistenceStep.cs @@ -1,5 +1,6 @@ -using System.Linq; +using System; using System.Threading.Tasks; +using TrashLib.Config; using TrashLib.Radarr.Config; using TrashLib.Radarr.CustomFormat.Api; using TrashLib.Radarr.CustomFormat.Cache; @@ -8,31 +9,36 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps { internal class CustomFormatApiPersistenceStep : ICustomFormatApiPersistenceStep { - private readonly ICustomFormatCache _cache; + private readonly Func _cacheFactory; - public CustomFormatApiPersistenceStep(ICustomFormatCache cache) + public CustomFormatApiPersistenceStep(Func cacheFactory) { - _cache = cache; + _cacheFactory = cacheFactory; } - public async Task Process(RadarrConfig config, ICustomFormatService api, CustomFormatTransactionData transactions) + public async Task Process(RadarrConfig config, ICustomFormatService api, + CustomFormatTransactionData transactions) { + var cache = _cacheFactory(config); + foreach (var cf in transactions.NewCustomFormats) { var id = await api.CreateCustomFormat(cf); - _cache.Add(id, cf); + cache.Add(id, cf); } - foreach (var cf in transactions.UpdatedCustomFormats) + foreach (var (customFormat, id) in transactions.UpdatedCustomFormats) { - await api.UpdateCustomFormat(cf.Id, cf.CustomFormat); + await api.UpdateCustomFormat(id, customFormat); } foreach (var cfId in transactions.DeletedCustomFormatIds) { await api.DeleteCustomFormat(cfId.CustomFormatId); - _cache.Remove(cfId); + cache.Remove(cfId); } + + cache.Save(); } } } diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/ICustomFormatApiPersistenceStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/ICustomFormatApiPersistenceStep.cs index d2d01eae..b6172f30 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/ICustomFormatApiPersistenceStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/ICustomFormatApiPersistenceStep.cs @@ -5,6 +5,5 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps { public interface ICustomFormatApiPersistenceStep { - 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 ca863542..3fd7576b 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/IJsonTransactionStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/IJsonTransactionStep.cs @@ -8,13 +8,5 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps { public interface IJsonTransactionStep { - CustomFormatTransactionData Transactions { get; } - - void Process( - IEnumerable guideCfs, - IReadOnlyCollection radarrCfs, - RadarrConfig config); - - void RecordDeletions(IEnumerable deletedCfsInCache, List radarrCfs); } } diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/JsonTransactionStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/JsonTransactionStep.cs index 8af262e0..a3cdc8df 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/JsonTransactionStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/JsonTransactionStep.cs @@ -5,7 +5,7 @@ using Common.Extensions; using Newtonsoft.Json.Linq; using TrashLib.Config; using TrashLib.Radarr.Config; -using TrashLib.Radarr.CustomFormat.Api; +using TrashLib.Radarr.CustomFormat.Api.Models; using TrashLib.Radarr.CustomFormat.Cache; using TrashLib.Radarr.CustomFormat.Models; using TrashLib.Radarr.CustomFormat.Models.Cache; @@ -35,7 +35,7 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps public void Process( IEnumerable guideCfs, - IReadOnlyCollection radarrCfs, + IReadOnlyCollection radarrCfs, RadarrConfig config) { var cache = _cacheFactory(config); @@ -44,24 +44,22 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps { var mapping = cache.Mappings.FirstOrDefault(m => m.TrashId == guideCf.TrashId); var radarrCf = FindRadarrCf(radarrCfs, mapping?.CustomFormatId, guideCf.Name); - var guideCfJson = BuildNewRadarrCf(guideCf.Json); + FixupRadarrCf(guideCf.Data); // no match; we add this CF as brand new if (radarrCf == null) { - 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); + var originalRadarrJson = JObject.FromObject(radarrCf); + UpdateRadarrCf(radarrCf, guideCf.Data); - if (!JToken.DeepEquals(radarrCf, guideCf.Json)) + if (!JToken.DeepEquals(JObject.FromObject(radarrCf), originalRadarrJson)) { - Transactions.UpdatedCustomFormats.Add( - new CachedCustomFormat(guideCf, (int) guideCf.Json["id"])); + Transactions.UpdatedCustomFormats.Add(new CachedCustomFormat(guideCf, guideCf.Data.Id)); } else { @@ -72,7 +70,7 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps } public void RecordDeletions(IEnumerable guideCfs, - List radarrCfs, RadarrConfig config) + List radarrCfs, RadarrConfig config) { var cache = _cacheFactory(config); @@ -88,60 +86,64 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps } } - private static JObject? FindRadarrCf(IReadOnlyCollection radarrCfs, int? cfId, string? cfName) + private static CustomFormatData? FindRadarrCf(IReadOnlyCollection radarrCfs, + int? cfId, string? cfName) { - JObject? match = null; + CustomFormatData? match = null; // Try to find match in cache first if (cfId != null) { - match = radarrCfs.FirstOrDefault(rcf => cfId == rcf["id"].Value()); + match = radarrCfs.FirstOrDefault(rcf => cfId == rcf.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["name"].Value())); + match = radarrCfs.FirstOrDefault(rcf => cfName.EqualsIgnoreCase(rcf.Name)); } return match; } - private static void UpdateRadarrCf(JObject cfToModify, JObject cfToMergeFrom) + private static void UpdateRadarrCf(CustomFormatData cfToModify, CustomFormatData cfToMergeFrom) { - MergeProperties(cfToModify, cfToMergeFrom, JTokenType.Array); + MergeProperties(cfToModify.ExtraJson, cfToMergeFrom.ExtraJson); - var radarrSpecs = cfToModify["specifications"].Children(); - var guideSpecs = cfToMergeFrom["specifications"].Children(); + var radarrSpecs = cfToModify.Specifications; + var guideSpecs = cfToMergeFrom.Specifications; var matchedGuideSpecs = guideSpecs - .GroupBy(gs => radarrSpecs.FirstOrDefault(gss => KeyMatch(gss, gs, "name"))) + .GroupBy(gs => radarrSpecs.FirstOrDefault(gss => gss.Name == gs.Name)) .SelectMany(kvp => kvp.Select(gs => new {GuideSpec = gs, RadarrSpec = kvp.Key})); - var newRadarrSpecs = new JArray(); + cfToModify.Specifications.Clear(); foreach (var match in matchedGuideSpecs) { if (match.RadarrSpec != null) { - MergeProperties(match.RadarrSpec, match.GuideSpec); - newRadarrSpecs.Add(match.RadarrSpec); + MergeProperties(match.RadarrSpec.ExtraJson, match.GuideSpec.ExtraJson); + cfToModify.Specifications.Add(match.RadarrSpec); } else { - newRadarrSpecs.Add(match.GuideSpec); + cfToModify.Specifications.Add(match.GuideSpec); } } - - cfToModify["specifications"] = newRadarrSpecs; } - private static bool KeyMatch(JObject left, JObject right, string keyName) - => left[keyName].Value() == right[keyName].Value(); + // private static bool KeyMatch(CustomFormatData left, CustomFormatData right, string keyName) + // => left[keyName].Value() == right[keyName].Value(); - private static void MergeProperties(JObject radarrCf, JObject guideCfJson, + private static void MergeProperties(JObject? radarrCf, JObject? guideCfJson, JTokenType exceptType = JTokenType.None) { + if (radarrCf == null || guideCfJson == null) + { + return; + } + foreach (var guideProp in guideCfJson.Properties().Where(p => p.Value.Type != exceptType)) { if (guideProp.Value.Type == JTokenType.Array && @@ -159,7 +161,7 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps } } - private static JObject BuildNewRadarrCf(JObject jsonPayload) + private static void FixupRadarrCf(CustomFormatData cfData) { // Information on required fields from nitsua /* @@ -169,17 +171,20 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps everything else radarr can handle with backend logic */ - foreach (var child in jsonPayload["specifications"]) + foreach (var spec in cfData.Specifications) { + if (spec.ExtraJson is null) + { + continue; + } + // 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"]; + var field = spec.ExtraJson["fields"]; field["name"] = "value"; - child["fields"] = new JArray {field}; + spec.ExtraJson["fields"] = new JArray {field}; } - - return jsonPayload; } } }