diff --git a/src/Common/JsonNetExtensions.cs b/src/Common/JsonNetExtensions.cs new file mode 100644 index 00000000..4a19b8d5 --- /dev/null +++ b/src/Common/JsonNetExtensions.cs @@ -0,0 +1,26 @@ +using System; +using Newtonsoft.Json.Linq; + +namespace Common +{ + public static class JsonNetExtensions + { + public static JEnumerable Children(this JToken token, string key) + where T : JToken + { + return token[key]?.Children() ?? JEnumerable.Empty; + } + + public static T ValueOrThrow(this JToken token, string key) + where T : class + { + var value = token.Value(key); + if (value is null) + { + throw new ArgumentNullException(token.Path); + } + + return value; + } + } +} diff --git a/src/Trash/Command/Helpers/ServiceCommand.cs b/src/Trash/Command/Helpers/ServiceCommand.cs index 521d3466..8efb0138 100644 --- a/src/Trash/Command/Helpers/ServiceCommand.cs +++ b/src/Trash/Command/Helpers/ServiceCommand.cs @@ -18,6 +18,7 @@ namespace Trash.Command.Helpers { public abstract class ServiceCommand : ICommand, IServiceCommand { + private readonly AutofacContractResolver _contractResolver; private readonly LoggingLevelSwitch _loggingLevelSwitch; private readonly ILogJanitor _logJanitor; diff --git a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/JsonTransactionStepTest.cs b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/JsonTransactionStepTest.cs index e5d49208..26125fce 100644 --- a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/JsonTransactionStepTest.cs +++ b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/JsonTransactionStepTest.cs @@ -175,7 +175,7 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps var radarrCfs = JsonConvert.DeserializeObject>(radarrCfData); var guideCfs = new List { - new("created", "", guideCfData[0]), + new("created", "", guideCfData![0]), new("updated_different_name", "", guideCfData[1]) { CacheEntry = new TrashIdMapping("", "", 2) @@ -184,7 +184,7 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps }; var processor = new JsonTransactionStep(); - processor.Process(guideCfs, radarrCfs); + processor.Process(guideCfs, radarrCfs!); var expectedJson = new[] { @@ -297,8 +297,8 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps var radarrCfs = JsonConvert.DeserializeObject>(radarrCfData); var processor = new JsonTransactionStep(); - processor.Process(guideCfs, radarrCfs); - processor.RecordDeletions(deletedCfsInCache, radarrCfs); + processor.Process(guideCfs, radarrCfs!); + processor.RecordDeletions(deletedCfsInCache, radarrCfs!); var expectedJson = @"{ 'id': 1, @@ -356,7 +356,7 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps var radarrCfs = JsonConvert.DeserializeObject>(radarrCfData); var processor = new JsonTransactionStep(); - processor.RecordDeletions(deletedCfsInCache, radarrCfs); + processor.RecordDeletions(deletedCfsInCache, radarrCfs!); var expectedTransactions = new CustomFormatTransactionData(); expectedTransactions.DeletedCustomFormatIds.Add(new TrashIdMapping("testtrashid", "testname", 2)); @@ -410,12 +410,12 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps var radarrCfs = JsonConvert.DeserializeObject>(radarrCfData); var guideCfs = new List { - new("updated", "", guideCfData[0]), + new("updated", "", guideCfData![0]), new("no_change", "", guideCfData[1]) }; var processor = new JsonTransactionStep(); - processor.Process(guideCfs, radarrCfs); + processor.Process(guideCfs, radarrCfs!); processor.Transactions.UpdatedCustomFormats.First().CacheEntry.Should() .BeEquivalentTo(new TrashIdMapping("", "updated", 1)); diff --git a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStepTest.cs b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStepTest.cs index 2d2fa5fc..bfd20ed5 100644 --- a/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStepTest.cs +++ b/src/TrashLib.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStepTest.cs @@ -43,7 +43,7 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps }]"; var api = Substitute.For(); - api.GetQualityProfiles().Returns(JsonConvert.DeserializeObject>(radarrQualityProfileData)); + api.GetQualityProfiles()!.Returns(JsonConvert.DeserializeObject>(radarrQualityProfileData)); var cfScores = new Dictionary { @@ -68,7 +68,7 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps const string radarrQualityProfileData = @"[{'name': 'profile1'}]"; var api = Substitute.For(); - api.GetQualityProfiles().Returns(JsonConvert.DeserializeObject>(radarrQualityProfileData)); + api.GetQualityProfiles()!.Returns(JsonConvert.DeserializeObject>(radarrQualityProfileData)); var cfScores = new Dictionary { @@ -107,7 +107,7 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps }]"; var api = Substitute.For(); - api.GetQualityProfiles().Returns(JsonConvert.DeserializeObject>(radarrQualityProfileData)); + api.GetQualityProfiles()!.Returns(JsonConvert.DeserializeObject>(radarrQualityProfileData)); var cfScores = new Dictionary { @@ -134,7 +134,7 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps }); api.Received().UpdateQualityProfile( - Verify.That(j => j["formatItems"].Children().Should().HaveCount(3)), + Verify.That(j => j["formatItems"]!.Children().Should().HaveCount(3)), Arg.Any()); } @@ -183,7 +183,7 @@ namespace TrashLib.Tests.Radarr.CustomFormat.Processors.PersistenceSteps }]"; var api = Substitute.For(); - api.GetQualityProfiles().Returns(JsonConvert.DeserializeObject>(radarrQualityProfileData)); + api.GetQualityProfiles()!.Returns(JsonConvert.DeserializeObject>(radarrQualityProfileData)); var cfScores = new Dictionary { diff --git a/src/TrashLib/Radarr/CustomFormat/Api/CustomFormatService.cs b/src/TrashLib/Radarr/CustomFormat/Api/CustomFormatService.cs index c54aab1e..695e8fbb 100644 --- a/src/TrashLib/Radarr/CustomFormat/Api/CustomFormatService.cs +++ b/src/TrashLib/Radarr/CustomFormat/Api/CustomFormatService.cs @@ -33,7 +33,10 @@ namespace TrashLib.Radarr.CustomFormat.Api .PostJsonAsync(cf.Json) .ReceiveJson(); - cf.SetCache((int) response["id"]); + if (response != null) + { + cf.SetCache(response.Value("id")); + } } public async Task UpdateCustomFormat(ProcessedCustomFormatData cf) diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStep.cs index 610073e5..c00f1a07 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStep.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Common; using Common.Extensions; using Newtonsoft.Json.Linq; using TrashLib.Radarr.Config; @@ -96,19 +97,19 @@ namespace TrashLib.Radarr.CustomFormat.Processors.GuideSteps private static ProcessedCustomFormatData ProcessCustomFormatData(string guideData, CustomFormatCache? cache) { JObject obj = JObject.Parse(guideData); - var name = (string) obj["name"]; - var trashId = (string) obj["trash_id"]; + var name = obj.ValueOrThrow("name"); + var trashId = obj.ValueOrThrow("trash_id"); int? finalScore = null; if (obj.TryGetValue("trash_score", out var score)) { finalScore = (int) score; - obj.Property("trash_score").Remove(); + 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(); + obj.Property("trash_id")?.Remove(); return new ProcessedCustomFormatData(name, trashId, obj) { diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/JsonTransactionStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/JsonTransactionStep.cs index 918e21b1..9c14d57a 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/JsonTransactionStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/JsonTransactionStep.cs @@ -44,7 +44,7 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps // later). if (guideCf.CacheEntry == null) { - guideCf.SetCache((int) guideCf.Json["id"]); + guideCf.SetCache(guideCf.Json.Value("id")); } if (!JToken.DeepEquals(radarrCf, guideCf.Json)) @@ -82,13 +82,13 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps // Try to find match in cache first if (cfId != null) { - match = radarrCfs.FirstOrDefault(rcf => cfId == rcf["id"].Value()); + match = radarrCfs.FirstOrDefault(rcf => cfId == rcf.Value("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.Value("name"))); } return match; @@ -98,8 +98,8 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps { MergeProperties(cfToModify, cfToMergeFrom, JTokenType.Array); - var radarrSpecs = cfToModify["specifications"].Children(); - var guideSpecs = cfToMergeFrom["specifications"].Children(); + var radarrSpecs = cfToModify["specifications"]?.Children() ?? new JEnumerable(); + var guideSpecs = cfToMergeFrom["specifications"]?.Children() ?? new JEnumerable(); var matchedGuideSpecs = guideSpecs .GroupBy(gs => radarrSpecs.FirstOrDefault(gss => KeyMatch(gss, gs, "name"))) @@ -124,7 +124,7 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps } private static bool KeyMatch(JObject left, JObject right, string keyName) - => left[keyName].Value() == right[keyName].Value(); + => left.Value(keyName) == right.Value(keyName); private static void MergeProperties(JObject radarrCf, JObject guideCfJson, JTokenType exceptType = JTokenType.None) @@ -156,14 +156,23 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps everything else radarr can handle with backend logic */ - foreach (var child in jsonPayload["specifications"]) + var specs = jsonPayload["specifications"]; + if (specs is not null) { - // 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"]; - field["name"] = "value"; - child["fields"] = new JArray {field}; + foreach (var child in specs) + { + // 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"]; + if (field is null) + { + continue; + } + + field["name"] = "value"; + child["fields"] = new JArray {field}; + } } return jsonPayload; diff --git a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStep.cs b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStep.cs index 983dcdba..5be36652 100644 --- a/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStep.cs +++ b/src/TrashLib/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStep.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Common; using Common.Extensions; using Newtonsoft.Json.Linq; using TrashLib.Radarr.CustomFormat.Api; @@ -28,8 +29,8 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps // do not match profiles in Radarr. var profileScores = cfScores.GroupJoin(radarrProfiles, s => s.Key, - p => (string) p["name"], - (s, p) => (s.Key, s.Value, p.SelectMany(pi => pi["formatItems"].Children()).ToList()), + p => p.Value("name"), + (s, p) => (s.Key, s.Value, p.SelectMany(pi => pi.Children("formatItems")).ToList()), StringComparer.InvariantCultureIgnoreCase); foreach (var (profileName, scoreMap, formatItems) in profileScores) @@ -58,14 +59,14 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps reason = FormatScoreUpdateReason.Reset; } - if (scoreToUse == null || reason == null || (int) json["score"] == scoreToUse) + if (scoreToUse == null || reason == null || json.Value("score") == scoreToUse) { continue; } json["score"] = scoreToUse.Value; _updatedScores.GetOrCreate(profileName) - .Add(new UpdatedFormatScore((string) json["name"], scoreToUse.Value, reason.Value)); + .Add(new UpdatedFormatScore(json.ValueOrThrow("name"), scoreToUse.Value, reason.Value)); } if (!_updatedScores.TryGetValue(profileName, out var updatedScores) || updatedScores.Count == 0) @@ -75,7 +76,7 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps } var jsonRoot = (JObject) formatItems.First().Root; - await api.UpdateQualityProfile(jsonRoot, (int) jsonRoot["id"]); + await api.UpdateQualityProfile(jsonRoot, jsonRoot.Value("id")); } } @@ -84,7 +85,7 @@ namespace TrashLib.Radarr.CustomFormat.Processors.PersistenceSteps { return scoreMap.Mapping.FirstOrDefault( m => m.CustomFormat.CacheEntry != null && - (int) formatItem["format"] == m.CustomFormat.CacheEntry.CustomFormatId); + formatItem.Value("format") == m.CustomFormat.CacheEntry.CustomFormatId); } } } diff --git a/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileUpdater.cs b/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileUpdater.cs index 74933520..e2bece88 100644 --- a/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileUpdater.cs +++ b/src/TrashLib/Sonarr/ReleaseProfile/ReleaseProfileUpdater.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Common.Extensions;