diff --git a/CHANGELOG.md b/CHANGELOG.md index a56ed23e..d42a789c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The log message listing custom formats without scores in the guide now prints information one per line (improved readability) +- Duplicate custom formats in the guide now issue a warning and get skipped. ## [1.4.0] - 2021-05-14 diff --git a/src/Trash.Tests/Cache/ServiceCacheTest.cs b/src/Trash.Tests/Cache/ServiceCacheTest.cs index ec105e91..314151f4 100644 --- a/src/Trash.Tests/Cache/ServiceCacheTest.cs +++ b/src/Trash.Tests/Cache/ServiceCacheTest.cs @@ -49,7 +49,7 @@ namespace Trash.Tests.Cache } [Test] - public void Load_NoFileExists_ReturnsNull() + public void Load_returns_null_when_file_does_not_exist() { var ctx = new Context(); ctx.Filesystem.File.Exists(Arg.Any()).Returns(false); @@ -59,7 +59,7 @@ namespace Trash.Tests.Cache } [Test] - public void Load_WithAttribute_ParsesCorrectly() + public void Loading_with_attribute_parses_correctly() { var ctx = new Context(); @@ -78,7 +78,7 @@ namespace Trash.Tests.Cache } [Test] - public void Load_WithAttributeInvalidName_ThrowsException() + public void Loading_with_invalid_object_name_throws() { var ctx = new Context(); @@ -90,7 +90,7 @@ namespace Trash.Tests.Cache } [Test] - public void Load_WithoutAttribute_Throws() + public void Loading_without_attribute_throws() { var ctx = new Context(); @@ -102,7 +102,7 @@ namespace Trash.Tests.Cache } [Test] - public void Save_WithAttribute_ParsesCorrectly() + public void Saving_with_attribute_parses_correctly() { var ctx = new Context(); @@ -120,7 +120,7 @@ namespace Trash.Tests.Cache } [Test] - public void Save_WithAttributeInvalidName_ThrowsException() + public void Saving_with_invalid_object_name_throws() { var ctx = new Context(); @@ -132,7 +132,7 @@ namespace Trash.Tests.Cache } [Test] - public void Save_WithoutAttribute_Throws() + public void Saving_without_attribute_throws() { var ctx = new Context(); diff --git a/src/Trash.Tests/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStepTest.cs b/src/Trash.Tests/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStepTest.cs index fd7f84dd..f6fe1478 100644 --- a/src/Trash.Tests/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStepTest.cs +++ b/src/Trash.Tests/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStepTest.cs @@ -85,6 +85,7 @@ namespace Trash.Tests.Radarr.CustomFormat.Processors.GuideSteps var processor = new CustomFormatStep(); processor.Process(testGuideData, testConfig, testCache); + processor.DuplicatedCustomFormats.Should().BeEmpty(); processor.CustomFormatsWithOutdatedNames.Should().HaveCount(outdatedCount); processor.DeletedCustomFormatsInCache.Should().BeEmpty(); processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List @@ -125,6 +126,7 @@ namespace Trash.Tests.Radarr.CustomFormat.Processors.GuideSteps var processor = new CustomFormatStep(); processor.Process(guideData, testConfig, testCache); + processor.DuplicatedCustomFormats.Should().BeEmpty(); processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); processor.DeletedCustomFormatsInCache.Count.Should().Be(1); processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List @@ -150,6 +152,7 @@ namespace Trash.Tests.Radarr.CustomFormat.Processors.GuideSteps var processor = new CustomFormatStep(); processor.Process(ctx.TestGuideData, testConfig, new CustomFormatCache()); + processor.DuplicatedCustomFormats.Should().BeEmpty(); processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); processor.DeletedCustomFormatsInCache.Should().BeEmpty(); processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List @@ -179,6 +182,7 @@ namespace Trash.Tests.Radarr.CustomFormat.Processors.GuideSteps var processor = new CustomFormatStep(); processor.Process(ctx.TestGuideData, testConfig, new CustomFormatCache()); + processor.DuplicatedCustomFormats.Should().BeEmpty(); processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); processor.DeletedCustomFormatsInCache.Should().BeEmpty(); processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List @@ -214,6 +218,7 @@ namespace Trash.Tests.Radarr.CustomFormat.Processors.GuideSteps var processor = new CustomFormatStep(); processor.Process(guideData, testConfig, testCache); + processor.DuplicatedCustomFormats.Should().BeEmpty(); processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); processor.DeletedCustomFormatsInCache.Should() .BeEquivalentTo(new TrashIdMapping("id1000", "name1")); @@ -240,6 +245,7 @@ namespace Trash.Tests.Radarr.CustomFormat.Processors.GuideSteps var processor = new CustomFormatStep(); processor.Process(guideCfs, Array.Empty(), cache); + processor.DuplicatedCustomFormats.Should().BeEmpty(); processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); processor.DeletedCustomFormatsInCache.Should().BeEquivalentTo(cache.TrashIdMappings[0]); processor.ProcessedCustomFormats.Should().BeEmpty(); @@ -269,6 +275,7 @@ namespace Trash.Tests.Radarr.CustomFormat.Processors.GuideSteps var processor = new CustomFormatStep(); processor.Process(guideData, testConfig, testCache); + processor.DuplicatedCustomFormats.Should().BeEmpty(); processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); processor.DeletedCustomFormatsInCache.Should().BeEmpty(); processor.ProcessedCustomFormats.Should() @@ -288,6 +295,7 @@ namespace Trash.Tests.Radarr.CustomFormat.Processors.GuideSteps var processor = new CustomFormatStep(); processor.Process(ctx.TestGuideData, testConfig, new CustomFormatCache()); + processor.DuplicatedCustomFormats.Should().BeEmpty(); processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); processor.DeletedCustomFormatsInCache.Should().BeEmpty(); processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List @@ -309,6 +317,36 @@ namespace Trash.Tests.Radarr.CustomFormat.Processors.GuideSteps var processor = new CustomFormatStep(); processor.Process(ctx.TestGuideData, testConfig, new CustomFormatCache()); + processor.DuplicatedCustomFormats.Should().BeEmpty(); + processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); + processor.DeletedCustomFormatsInCache.Should().BeEmpty(); + processor.ProcessedCustomFormats.Should().BeEmpty(); + } + + [Test] + public void Duplicates_are_recorded_and_removed_from_processed_custom_formats_list() + { + var guideData = new List + { + new() {Json = @"{'name': 'name1', 'trash_id': 'id1'}"}, + new() {Json = @"{'name': 'name1', 'trash_id': 'id2'}"} + }; + + var testConfig = new List + { + new() {Names = new List {"name1"}} + }; + + var processor = new CustomFormatStep(); + processor.Process(guideData, testConfig, null); + + //Dictionary> + processor.DuplicatedCustomFormats.Should().ContainKey("name1") + .WhichValue.Should().BeEquivalentTo(new List + { + new ("name1", "id1", JObject.Parse(@"{'name': 'name1'}")), + new ("name1", "id2", JObject.Parse(@"{'name': 'name1'}")) + }); processor.CustomFormatsWithOutdatedNames.Should().BeEmpty(); processor.DeletedCustomFormatsInCache.Should().BeEmpty(); processor.ProcessedCustomFormats.Should().BeEmpty(); diff --git a/src/Trash/Radarr/CustomFormat/CustomFormatUpdater.cs b/src/Trash/Radarr/CustomFormat/CustomFormatUpdater.cs index 5daa9fb7..973e86ee 100644 --- a/src/Trash/Radarr/CustomFormat/CustomFormatUpdater.cs +++ b/src/Trash/Radarr/CustomFormat/CustomFormatUpdater.cs @@ -137,6 +137,28 @@ namespace Trash.Radarr.CustomFormat private bool ValidateGuideDataAndCheckShouldProceed(RadarrConfiguration config) { + Console.WriteLine(""); + + if (_guideProcessor.DuplicatedCustomFormats.Count > 0) + { + Log.Warning("One or more of the custom formats you want are duplicated in the guide. These custom " + + "formats WILL BE SKIPPED. Radarr requires custom formats names to be unique. Trash Updater " + + "is not able to choose which one you actually wanted. This is a bug in the guide and you " + + "should request that it be fixed"); + + foreach (var (cfName, dupes) in _guideProcessor.DuplicatedCustomFormats) + { + Log.Warning("{CfName} is duplicated {DupeTimes} with the following Trash IDs:", cfName, + dupes.Count); + foreach (var cf in dupes) + { + Log.Warning(" - {TrashId}", cf.TrashId); + } + } + + Console.WriteLine(""); + } + if (_guideProcessor.CustomFormatsNotInGuide.Count > 0) { Log.Warning("The Custom Formats below do not exist in the guide and will " + @@ -144,6 +166,8 @@ namespace Trash.Radarr.CustomFormat "the guide! Either fix the names or remove them from your YAML config to resolve this " + "warning"); Log.Warning("{CfList}", _guideProcessor.CustomFormatsNotInGuide); + + Console.WriteLine(""); } var cfsWithoutQualityProfiles = _guideProcessor.ConfigData @@ -155,6 +179,8 @@ namespace Trash.Radarr.CustomFormat { Log.Debug("These custom formats will be uploaded but are not associated to a quality profile in the " + "config file: {UnassociatedCfs}", cfsWithoutQualityProfiles); + + Console.WriteLine(""); } // No CFs are defined in this item, or they are all invalid. Skip this whole instance. @@ -174,6 +200,8 @@ namespace Trash.Radarr.CustomFormat { Log.Warning("{CfList}", tuple); } + + Console.WriteLine(""); } if (_guideProcessor.CustomFormatsWithOutdatedNames.Count > 0) @@ -186,6 +214,8 @@ namespace Trash.Radarr.CustomFormat { Log.Warning(" - '{OldName}' -> '{NewName}'", oldName, newName); } + + Console.WriteLine(""); } return true; diff --git a/src/Trash/Radarr/CustomFormat/Processors/GuideProcessor.cs b/src/Trash/Radarr/CustomFormat/Processors/GuideProcessor.cs index 3c8162b5..32035df8 100644 --- a/src/Trash/Radarr/CustomFormat/Processors/GuideProcessor.cs +++ b/src/Trash/Radarr/CustomFormat/Processors/GuideProcessor.cs @@ -55,6 +55,9 @@ namespace Trash.Radarr.CustomFormat.Processors public List<(string, string)> CustomFormatsWithOutdatedNames => _steps.CustomFormat.CustomFormatsWithOutdatedNames; + public Dictionary> DuplicatedCustomFormats + => _steps.CustomFormat.DuplicatedCustomFormats; + public async Task BuildGuideData(IReadOnlyList config, CustomFormatCache? cache) { if (_guideData == null) diff --git a/src/Trash/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStep.cs b/src/Trash/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStep.cs index 51cff9f6..c4b2034c 100644 --- a/src/Trash/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStep.cs +++ b/src/Trash/Radarr/CustomFormat/Processors/GuideSteps/CustomFormatStep.cs @@ -15,6 +15,9 @@ namespace Trash.Radarr.CustomFormat.Processors.GuideSteps public List ProcessedCustomFormats { get; } = new(); public List DeletedCustomFormatsInCache { get; } = new(); + public Dictionary> DuplicatedCustomFormats { get; private set; } = + new(); + public void Process(IEnumerable customFormatGuideData, IEnumerable config, CustomFormatCache? cache) { @@ -28,7 +31,7 @@ namespace Trash.Radarr.CustomFormat.Processors.GuideSteps .ToList(); // Perform updates and deletions based on matches in the cache. Matches in the cache are by ID. - foreach (var cf in processedCfs) //.Where(cf => cf.CacheEntry != null)) + foreach (var cf in processedCfs) { // Does the name of the CF in the guide match a name in the config? If yes, we keep it. var configName = allConfigCfNames.FirstOrDefault(n => n.EqualsIgnoreCase(cf.Name)); @@ -63,6 +66,19 @@ namespace Trash.Radarr.CustomFormat.Processors.GuideSteps // Orphaned entries in cache represent custom formats we need to delete. ProcessDeletedCustomFormats(cache); + + // Check for multiple custom formats with the same name in the guide data (e.g. "DoVi") + ProcessDuplicates(); + } + + private void ProcessDuplicates() + { + DuplicatedCustomFormats = ProcessedCustomFormats + .GroupBy(cf => cf.Name) + .Where(grp => grp.Count() > 1) + .ToDictionary(grp => grp.Key, grp => grp.ToList()); + + ProcessedCustomFormats.RemoveAll(cf => DuplicatedCustomFormats.ContainsKey(cf.Name)); } private static ProcessedCustomFormatData ProcessCustomFormatData(CustomFormatData guideData, diff --git a/src/Trash/Radarr/CustomFormat/Processors/GuideSteps/ICustomFormatStep.cs b/src/Trash/Radarr/CustomFormat/Processors/GuideSteps/ICustomFormatStep.cs index a178fc38..e8db537a 100644 --- a/src/Trash/Radarr/CustomFormat/Processors/GuideSteps/ICustomFormatStep.cs +++ b/src/Trash/Radarr/CustomFormat/Processors/GuideSteps/ICustomFormatStep.cs @@ -10,6 +10,7 @@ namespace Trash.Radarr.CustomFormat.Processors.GuideSteps List ProcessedCustomFormats { get; } List DeletedCustomFormatsInCache { get; } List<(string, string)> CustomFormatsWithOutdatedNames { get; } + Dictionary> DuplicatedCustomFormats { get; } void Process(IEnumerable customFormatGuideData, IEnumerable config, CustomFormatCache? cache); diff --git a/src/Trash/Radarr/CustomFormat/Processors/IGuideProcessor.cs b/src/Trash/Radarr/CustomFormat/Processors/IGuideProcessor.cs index f60ace3c..c8d1da08 100644 --- a/src/Trash/Radarr/CustomFormat/Processors/IGuideProcessor.cs +++ b/src/Trash/Radarr/CustomFormat/Processors/IGuideProcessor.cs @@ -14,6 +14,7 @@ namespace Trash.Radarr.CustomFormat.Processors IReadOnlyCollection<(string name, string trashId, string profileName)> CustomFormatsWithoutScore { get; } IReadOnlyCollection DeletedCustomFormatsInCache { get; } List<(string, string)> CustomFormatsWithOutdatedNames { get; } + Dictionary> DuplicatedCustomFormats { get; } Task BuildGuideData(IReadOnlyList config, CustomFormatCache? cache); void Reset();