feat!: Remove names support for custom formats

BREAKING CHANGE: The `names` property is no longer supported under
`custom_formats:`. This feature was previously deprecated.
pull/138/head
Robert Dailey 2 years ago
parent 845947ada0
commit 63c3bff27a

@ -8,6 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Removed
- **BREAKING**: Completely removed support for `names` under `custom_formats` in `recyclarr.yml`.
Note that this had already been deprecated for quite some time.
## [2.6.1] - 2022-10-15 ## [2.6.1] - 2022-10-15
### Fixed ### Fixed

@ -1,3 +1,4 @@
using FluentAssertions;
using FluentAssertions.Equivalency; using FluentAssertions.Equivalency;
using FluentAssertions.Json; using FluentAssertions.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
@ -16,7 +17,9 @@ public class JsonEquivalencyStep : IEquivalencyStep
} }
((JToken) comparands.Subject!).Should().BeEquivalentTo( ((JToken) comparands.Subject!).Should().BeEquivalentTo(
(JToken) comparands.Expectation, context.Reason.FormattedMessage, context.Reason.Arguments); (JToken) comparands.Expectation,
context.Reason.FormattedMessage,
context.Reason.Arguments);
return EquivalencyResult.AssertionCompleted; return EquivalencyResult.AssertionCompleted;
} }

@ -29,15 +29,6 @@ public class CachePersisterTest
public IServiceCache ServiceCache { get; } public IServiceCache ServiceCache { get; }
} }
private static ProcessedCustomFormatData QuickMakeCf(string cfName, string trashId, int cfId)
{
var cf = NewCf.Data(cfName, trashId);
return new ProcessedCustomFormatData(cf)
{
CacheEntry = new TrashIdMapping(trashId, cfName) {CustomFormatId = cfId}
};
}
[TestCase(CustomFormatCache.LatestVersion - 1)] [TestCase(CustomFormatCache.LatestVersion - 1)]
[TestCase(CustomFormatCache.LatestVersion + 1)] [TestCase(CustomFormatCache.LatestVersion + 1)]
public void Set_loaded_cache_to_null_if_versions_mismatch(int versionToTest) public void Set_loaded_cache_to_null_if_versions_mismatch(int versionToTest)
@ -47,7 +38,7 @@ public class CachePersisterTest
var testCfObj = new CustomFormatCache var testCfObj = new CustomFormatCache
{ {
Version = versionToTest, Version = versionToTest,
TrashIdMappings = new Collection<TrashIdMapping> {new("", "", 5)} TrashIdMappings = new Collection<TrashIdMapping> {new("", 5)}
}; };
ctx.ServiceCache.Load<CustomFormatCache>().Returns(testCfObj); ctx.ServiceCache.Load<CustomFormatCache>().Returns(testCfObj);
ctx.Persister.Load(); ctx.Persister.Load();
@ -62,7 +53,7 @@ public class CachePersisterTest
var testCfObj = new CustomFormatCache var testCfObj = new CustomFormatCache
{ {
Version = CustomFormatCache.LatestVersion, Version = CustomFormatCache.LatestVersion,
TrashIdMappings = new Collection<TrashIdMapping> {new("", "", 5)} TrashIdMappings = new Collection<TrashIdMapping> {new("", 5)}
}; };
ctx.ServiceCache.Load<CustomFormatCache>().Returns(testCfObj); ctx.ServiceCache.Load<CustomFormatCache>().Returns(testCfObj);
ctx.Persister.Load(); ctx.Persister.Load();
@ -117,19 +108,18 @@ public class CachePersisterTest
// Load initial CfCache just to test that it gets replaced // Load initial CfCache just to test that it gets replaced
var testCfObj = new CustomFormatCache var testCfObj = new CustomFormatCache
{ {
TrashIdMappings = new Collection<TrashIdMapping> {new("", "") {CustomFormatId = 5}} TrashIdMappings = new Collection<TrashIdMapping> {new("") {CustomFormatId = 5}}
}; };
ctx.ServiceCache.Load<CustomFormatCache>().Returns(testCfObj); ctx.ServiceCache.Load<CustomFormatCache>().Returns(testCfObj);
ctx.Persister.Load(); ctx.Persister.Load();
// Update with new cached items // Update with new cached items
var results = new CustomFormatTransactionData(); var results = new CustomFormatTransactionData();
results.NewCustomFormats.Add(QuickMakeCf("cfname", "trashid", 10)); results.NewCustomFormats.Add(NewCf.Processed("cfname", "trashid", 10));
var customFormatData = new List<ProcessedCustomFormatData> var customFormatData = new List<ProcessedCustomFormatData>
{ {
new(NewCf.Data("", "trashid")) new(NewCf.Data("", "trashid")) {CacheEntry = new TrashIdMapping("trashid", 10)}
{CacheEntry = new TrashIdMapping("trashid", "cfname", 10)}
}; };
ctx.Persister.Update(customFormatData); ctx.Persister.Update(customFormatData);
@ -140,7 +130,7 @@ public class CachePersisterTest
customFormatData.Should().ContainSingle() customFormatData.Should().ContainSingle()
.Which.CacheEntry.Should().BeEquivalentTo( .Which.CacheEntry.Should().BeEquivalentTo(
new TrashIdMapping("trashid", "cfname") {CustomFormatId = 10}); new TrashIdMapping("trashid") {CustomFormatId = 10});
} }
[Test] [Test]

@ -4,7 +4,6 @@ using FluentAssertions;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NSubstitute; using NSubstitute;
using NUnit.Framework; using NUnit.Framework;
using Serilog;
using TestLibrary.FluentAssertions; using TestLibrary.FluentAssertions;
using TrashLib.Config.Services; using TrashLib.Config.Services;
using TrashLib.Services.CustomFormat.Guide; using TrashLib.Services.CustomFormat.Guide;
@ -23,7 +22,7 @@ public class GuideProcessorTest
private class TestGuideProcessorSteps : IGuideProcessorSteps private class TestGuideProcessorSteps : IGuideProcessorSteps
{ {
public ICustomFormatStep CustomFormat { get; } = new CustomFormatStep(); public ICustomFormatStep CustomFormat { get; } = new CustomFormatStep();
public IConfigStep Config { get; } = new ConfigStep(Substitute.For<ILogger>()); public IConfigStep Config { get; } = new ConfigStep();
public IQualityProfileStep QualityProfile { get; } = new QualityProfileStep(); public IQualityProfileStep QualityProfile { get; } = new QualityProfileStep();
} }
@ -68,7 +67,13 @@ public class GuideProcessorTest
{ {
new() new()
{ {
Names = new List<string> {"Surround SOUND", "DTS-HD/DTS:X", "no score", "not in guide 1"}, TrashIds = new List<string>
{
"43bb5f09c79641e7a22e48d440bd8868", // Surround SOUND
"4eb3c272d48db8ab43c2c85283b69744", // DTS-HD/DTS:X
"abc", // no score
"not in guide 1"
},
QualityProfiles = new List<QualityProfileConfig> QualityProfiles = new List<QualityProfileConfig>
{ {
new() {Name = "profile1"}, new() {Name = "profile1"},
@ -77,7 +82,11 @@ public class GuideProcessorTest
}, },
new() new()
{ {
Names = new List<string> {"no score", "not in guide 2"}, TrashIds = new List<string>
{
"abc", // no score
"not in guide 2"
},
QualityProfiles = new List<QualityProfileConfig> QualityProfiles = new List<QualityProfileConfig>
{ {
new() {Name = "profile3"}, new() {Name = "profile3"},
@ -97,7 +106,8 @@ public class GuideProcessorTest
NewCf.Processed("No Score", "abc") NewCf.Processed("No Score", "abc")
}; };
guideProcessor.ProcessedCustomFormats.Should().BeEquivalentTo(expectedProcessedCustomFormatData); guideProcessor.ProcessedCustomFormats.Should()
.BeEquivalentTo(expectedProcessedCustomFormatData, op => op.Using(new JsonEquivalencyStep()));
guideProcessor.ConfigData.Should() guideProcessor.ConfigData.Should()
.BeEquivalentTo(new List<ProcessedConfigData> .BeEquivalentTo(new List<ProcessedConfigData>

@ -4,7 +4,6 @@ using NUnit.Framework;
using TestLibrary.AutoFixture; using TestLibrary.AutoFixture;
using TrashLib.Config.Services; using TrashLib.Config.Services;
using TrashLib.Services.CustomFormat.Models; using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
using TrashLib.Services.CustomFormat.Processors.GuideSteps; using TrashLib.Services.CustomFormat.Processors.GuideSteps;
using TrashLib.TestLibrary; using TrashLib.TestLibrary;
@ -14,52 +13,20 @@ namespace TrashLib.Tests.CustomFormat.Processors.GuideSteps;
[Parallelizable(ParallelScope.All)] [Parallelizable(ParallelScope.All)]
public class ConfigStepTest public class ConfigStepTest
{ {
[Test, AutoMockData]
public void Cache_names_are_used_instead_of_name_in_json_data(ConfigStep processor)
{
var testProcessedCfs = new List<ProcessedCustomFormatData>
{
NewCf.Processed("name1", "id1", 100),
NewCf.Processed("name3", "id3", new TrashIdMapping("id3", "name1"))
};
var testConfig = new CustomFormatConfig[]
{
new()
{
Names = new List<string> {"name1"}
}
};
processor.Process(testProcessedCfs, testConfig);
processor.CustomFormatsNotInGuide.Should().BeEmpty();
processor.ConfigData.Should().BeEquivalentTo(new List<ProcessedConfigData>
{
new()
{
CustomFormats = new List<ProcessedCustomFormatData>
{testProcessedCfs[1]}
}
}, op => op
.Using<JToken>(jctx => jctx.Subject.Should().BeEquivalentTo(jctx.Expectation))
.WhenTypeIs<JToken>());
}
[Test, AutoMockData] [Test, AutoMockData]
public void Custom_formats_missing_from_config_are_skipped(ConfigStep processor) public void Custom_formats_missing_from_config_are_skipped(ConfigStep processor)
{ {
var testProcessedCfs = new List<ProcessedCustomFormatData> var testProcessedCfs = new List<ProcessedCustomFormatData>
{ {
NewCf.Processed("name1", ""), NewCf.Processed("name1", "id1"),
NewCf.Processed("name2", "") NewCf.Processed("name2", "id2")
}; };
var testConfig = new CustomFormatConfig[] var testConfig = new CustomFormatConfig[]
{ {
new() new()
{ {
Names = new List<string> {"name1"} TrashIds = new List<string> {"id1"}
} }
}; };
@ -72,7 +39,7 @@ public class ConfigStepTest
{ {
CustomFormats = new List<ProcessedCustomFormatData> CustomFormats = new List<ProcessedCustomFormatData>
{ {
NewCf.Processed("name1", "") NewCf.Processed("name1", "id1")
} }
} }
}, op => op }, op => op
@ -85,21 +52,21 @@ public class ConfigStepTest
{ {
var testProcessedCfs = new List<ProcessedCustomFormatData> var testProcessedCfs = new List<ProcessedCustomFormatData>
{ {
NewCf.Processed("name1", ""), NewCf.Processed("name1", "id1"),
NewCf.Processed("name2", "") NewCf.Processed("name2", "id2")
}; };
var testConfig = new CustomFormatConfig[] var testConfig = new CustomFormatConfig[]
{ {
new() new()
{ {
Names = new List<string> {"name1", "name3"} TrashIds = new List<string> {"id1", "id3"}
} }
}; };
processor.Process(testProcessedCfs, testConfig); processor.Process(testProcessedCfs, testConfig);
processor.CustomFormatsNotInGuide.Should().BeEquivalentTo(new List<string> {"name3"}, op => op processor.CustomFormatsNotInGuide.Should().BeEquivalentTo(new List<string> {"id3"}, op => op
.Using<JToken>(jctx => jctx.Subject.Should().BeEquivalentTo(jctx.Expectation)) .Using<JToken>(jctx => jctx.Subject.Should().BeEquivalentTo(jctx.Expectation))
.WhenTypeIs<JToken>()); .WhenTypeIs<JToken>());
processor.ConfigData.Should().BeEquivalentTo(new List<ProcessedConfigData> processor.ConfigData.Should().BeEquivalentTo(new List<ProcessedConfigData>
@ -108,7 +75,7 @@ public class ConfigStepTest
{ {
CustomFormats = new List<ProcessedCustomFormatData> CustomFormats = new List<ProcessedCustomFormatData>
{ {
NewCf.Processed("name1", "") NewCf.Processed("name1", "id1")
} }
} }
}, op => op }, op => op
@ -117,7 +84,7 @@ public class ConfigStepTest
} }
[Test, AutoMockData] [Test, AutoMockData]
public void Duplicate_config_name_and_id_are_ignored(ConfigStep processor) public void Duplicate_config_trash_ids_are_ignored(ConfigStep processor)
{ {
var testProcessedCfs = new List<ProcessedCustomFormatData> var testProcessedCfs = new List<ProcessedCustomFormatData>
{ {
@ -126,11 +93,7 @@ public class ConfigStepTest
var testConfig = new CustomFormatConfig[] var testConfig = new CustomFormatConfig[]
{ {
new() new() {TrashIds = new List<string> {"id1", "id1"}}
{
Names = new List<string> {"name1"},
TrashIds = new List<string> {"id1"}
}
}; };
processor.Process(testProcessedCfs, testConfig); processor.Process(testProcessedCfs, testConfig);
@ -144,67 +107,4 @@ public class ConfigStepTest
} }
}); });
} }
[Test, AutoMockData]
public void Duplicate_config_names_are_ignored(ConfigStep processor)
{
var testProcessedCfs = new List<ProcessedCustomFormatData>
{
NewCf.Processed("name1", "id1")
};
var testConfig = new CustomFormatConfig[]
{
new() {Names = new List<string> {"name1", "name1"}}
};
processor.Process(testProcessedCfs, testConfig);
processor.CustomFormatsNotInGuide.Should().BeEmpty();
processor.ConfigData.Should().BeEquivalentTo(new List<ProcessedConfigData>
{
new()
{
CustomFormats = new List<ProcessedCustomFormatData> {testProcessedCfs[0]}
}
});
}
[Test, AutoMockData]
public void Find_custom_formats_by_name_and_trash_id(ConfigStep processor)
{
var testProcessedCfs = new List<ProcessedCustomFormatData>
{
NewCf.Processed("name1", "id1", 100),
NewCf.Processed("name3", "id3"),
NewCf.Processed("name4", "id4")
};
var testConfig = new CustomFormatConfig[]
{
new()
{
Names = new List<string> {"name1", "name3"},
TrashIds = new List<string> {"id1", "id4"},
QualityProfiles = new List<QualityProfileConfig>
{
new() {Name = "profile1", Score = 50}
}
}
};
processor.Process(testProcessedCfs, testConfig);
processor.CustomFormatsNotInGuide.Should().BeEmpty();
processor.ConfigData.Should().BeEquivalentTo(new List<ProcessedConfigData>
{
new()
{
CustomFormats = testProcessedCfs,
QualityProfiles = testConfig[0].QualityProfiles
}
}, op => op
.Using<JToken>(jctx => jctx.Subject.Should().BeEquivalentTo(jctx.Expectation))
.WhenTypeIs<JToken>());
}
} }

@ -25,88 +25,17 @@ public class CustomFormatStepTest
}; };
} }
[TestCase("name1", 0)]
[TestCase("naME1", 0)]
[TestCase("DifferentName", 1)]
public void Match_cf_in_guide_with_different_name_with_cache_using_same_name_in_config(string variableCfName,
int outdatedCount)
{
var testConfig = new List<CustomFormatConfig>
{
new() {Names = new List<string> {"name1"}}
};
var testCache = new CustomFormatCache
{
TrashIdMappings = new Collection<TrashIdMapping>
{
new("id1", "name1")
}
};
var testGuideData = new List<CustomFormatData>
{
NewCf.Data(variableCfName, "id1")
};
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<ProcessedCustomFormatData>
{
NewCf.Processed(variableCfName, "id1", testCache.TrashIdMappings[0])
});
}
[Test, AutoMockData]
public void Cache_entry_is_not_set_when_id_is_different(CustomFormatStep processor)
{
var guideData = new List<CustomFormatData>
{
NewCf.Data("name1", "id1")
};
var testConfig = new List<CustomFormatConfig>
{
new() {Names = new List<string> {"name1"}}
};
var testCache = new CustomFormatCache
{
TrashIdMappings = new Collection<TrashIdMapping>
{
new("id1000", "name1")
}
};
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<ProcessedCustomFormatData>
{
NewCf.Processed("name1", "id1")
});
}
[Test, AutoMockData] [Test, AutoMockData]
public void Cfs_not_in_config_are_skipped(CustomFormatStep processor) public void Cfs_not_in_config_are_skipped(CustomFormatStep processor)
{ {
var ctx = new Context(); var ctx = new Context();
var testConfig = new List<CustomFormatConfig> var testConfig = new List<CustomFormatConfig>
{ {
new() {Names = new List<string> {"name1", "name3"}} new() {TrashIds = new List<string> {"id1", "id3"}}
}; };
processor.Process(ctx.TestGuideData, testConfig, new CustomFormatCache()); processor.Process(ctx.TestGuideData, testConfig, new CustomFormatCache());
processor.DuplicatedCustomFormats.Should().BeEmpty();
processor.CustomFormatsWithOutdatedNames.Should().BeEmpty();
processor.DeletedCustomFormatsInCache.Should().BeEmpty(); processor.DeletedCustomFormatsInCache.Should().BeEmpty();
processor.ProcessedCustomFormats.Should() processor.ProcessedCustomFormats.Should()
.BeEquivalentTo(new List<ProcessedCustomFormatData> .BeEquivalentTo(new List<ProcessedCustomFormatData>
@ -122,14 +51,12 @@ public class CustomFormatStepTest
var ctx = new Context(); var ctx = new Context();
var testConfig = new List<CustomFormatConfig> var testConfig = new List<CustomFormatConfig>
{ {
new() {Names = new List<string> {"name1", "name3"}}, new() {TrashIds = new List<string> {"id1", "id3"}},
new() {Names = new List<string> {"name2"}} new() {TrashIds = new List<string> {"id2"}}
}; };
processor.Process(ctx.TestGuideData, testConfig, new CustomFormatCache()); processor.Process(ctx.TestGuideData, testConfig, new CustomFormatCache());
processor.DuplicatedCustomFormats.Should().BeEmpty();
processor.CustomFormatsWithOutdatedNames.Should().BeEmpty();
processor.DeletedCustomFormatsInCache.Should().BeEmpty(); processor.DeletedCustomFormatsInCache.Should().BeEmpty();
processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List<ProcessedCustomFormatData> processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List<ProcessedCustomFormatData>
{ {
@ -150,20 +77,18 @@ public class CustomFormatStepTest
var testConfig = new List<CustomFormatConfig> var testConfig = new List<CustomFormatConfig>
{ {
new() {Names = new List<string> {"name1"}} new() {TrashIds = new List<string> {"id1"}}
}; };
var testCache = new CustomFormatCache var testCache = new CustomFormatCache
{ {
TrashIdMappings = new Collection<TrashIdMapping> {new("id1000", "name1")} TrashIdMappings = new Collection<TrashIdMapping> {new("id1000")}
}; };
processor.Process(guideData, testConfig, testCache); processor.Process(guideData, testConfig, testCache);
processor.DuplicatedCustomFormats.Should().BeEmpty();
processor.CustomFormatsWithOutdatedNames.Should().BeEmpty();
processor.DeletedCustomFormatsInCache.Should() processor.DeletedCustomFormatsInCache.Should()
.BeEquivalentTo(new[] {new TrashIdMapping("id1000", "name1")}); .BeEquivalentTo(new[] {new TrashIdMapping("id1000")});
processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List<ProcessedCustomFormatData> processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List<ProcessedCustomFormatData>
{ {
NewCf.Processed("name1", "id1") NewCf.Processed("name1", "id1")
@ -175,7 +100,7 @@ public class CustomFormatStepTest
{ {
var cache = new CustomFormatCache var cache = new CustomFormatCache
{ {
TrashIdMappings = new Collection<TrashIdMapping> {new("id1", "3D", 9)} TrashIdMappings = new Collection<TrashIdMapping> {new("id1", 9)}
}; };
var guideCfs = new List<CustomFormatData> var guideCfs = new List<CustomFormatData>
@ -185,90 +110,10 @@ public class CustomFormatStepTest
processor.Process(guideCfs, Array.Empty<CustomFormatConfig>(), cache); processor.Process(guideCfs, Array.Empty<CustomFormatConfig>(), cache);
processor.DuplicatedCustomFormats.Should().BeEmpty();
processor.CustomFormatsWithOutdatedNames.Should().BeEmpty();
processor.DeletedCustomFormatsInCache.Should().BeEquivalentTo(new[] {cache.TrashIdMappings[0]}); processor.DeletedCustomFormatsInCache.Should().BeEquivalentTo(new[] {cache.TrashIdMappings[0]});
processor.ProcessedCustomFormats.Should().BeEmpty(); processor.ProcessedCustomFormats.Should().BeEmpty();
} }
[Test, AutoMockData]
public void Custom_format_name_in_cache_is_updated_if_renamed_in_guide_and_config(CustomFormatStep processor)
{
var guideData = new List<CustomFormatData>
{
NewCf.Data("name2", "id1")
};
var testConfig = new List<CustomFormatConfig>
{
new() {Names = new List<string> {"name2"}}
};
var testCache = new CustomFormatCache
{
TrashIdMappings = new Collection<TrashIdMapping> {new("id1", "name1")}
};
processor.Process(guideData, testConfig, testCache);
processor.DuplicatedCustomFormats.Should().BeEmpty();
processor.CustomFormatsWithOutdatedNames.Should().BeEmpty();
processor.DeletedCustomFormatsInCache.Should().BeEmpty();
processor.ProcessedCustomFormats.Should()
.ContainSingle().Which.CacheEntry.Should()
.BeEquivalentTo(new TrashIdMapping("id1", "name2"));
}
[Test, AutoMockData]
public void Duplicates_are_recorded_and_removed_from_processed_custom_formats_list(CustomFormatStep processor)
{
var guideData = new List<CustomFormatData>
{
NewCf.Data("name1", "id1"),
NewCf.Data("name1", "id2")
};
var testConfig = new List<CustomFormatConfig>
{
new() {Names = new List<string> {"name1"}}
};
processor.Process(guideData, testConfig, null);
//Dictionary<string, List<ProcessedCustomFormatData>>
processor.DuplicatedCustomFormats.Should()
.ContainKey("name1").WhoseValue.Should()
.BeEquivalentTo(new List<ProcessedCustomFormatData>
{
NewCf.Processed("name1", "id1"),
NewCf.Processed("name1", "id2")
});
processor.CustomFormatsWithOutdatedNames.Should().BeEmpty();
processor.DeletedCustomFormatsInCache.Should().BeEmpty();
processor.ProcessedCustomFormats.Should().BeEmpty();
}
[Test, AutoMockData]
public void Match_cf_names_regardless_of_case_in_config(CustomFormatStep processor)
{
var ctx = new Context();
var testConfig = new List<CustomFormatConfig>
{
new() {Names = new List<string> {"name1", "NAME1"}}
};
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<ProcessedCustomFormatData>
{
NewCf.Processed("name1", "id1")
},
op => op.Using(new JsonEquivalencyStep()));
}
[Test, AutoMockData] [Test, AutoMockData]
public void Match_custom_format_using_trash_id(CustomFormatStep processor) public void Match_custom_format_using_trash_id(CustomFormatStep processor)
{ {
@ -285,8 +130,6 @@ public class CustomFormatStepTest
processor.Process(guideData, testConfig, null); processor.Process(guideData, testConfig, null);
processor.DuplicatedCustomFormats.Should().BeEmpty();
processor.CustomFormatsWithOutdatedNames.Should().BeEmpty();
processor.DeletedCustomFormatsInCache.Should().BeEmpty(); processor.DeletedCustomFormatsInCache.Should().BeEmpty();
processor.ProcessedCustomFormats.Should() processor.ProcessedCustomFormats.Should()
.BeEquivalentTo(new List<ProcessedCustomFormatData> .BeEquivalentTo(new List<ProcessedCustomFormatData>
@ -301,13 +144,11 @@ public class CustomFormatStepTest
var ctx = new Context(); var ctx = new Context();
var testConfig = new List<CustomFormatConfig> var testConfig = new List<CustomFormatConfig>
{ {
new() {Names = new List<string> {"doesnt_exist"}} new() {TrashIds = new List<string> {"doesnt_exist"}}
}; };
processor.Process(ctx.TestGuideData, testConfig, new CustomFormatCache()); processor.Process(ctx.TestGuideData, testConfig, new CustomFormatCache());
processor.DuplicatedCustomFormats.Should().BeEmpty();
processor.CustomFormatsWithOutdatedNames.Should().BeEmpty();
processor.DeletedCustomFormatsInCache.Should().BeEmpty(); processor.DeletedCustomFormatsInCache.Should().BeEmpty();
processor.ProcessedCustomFormats.Should().BeEmpty(); processor.ProcessedCustomFormats.Should().BeEmpty();
} }
@ -324,7 +165,7 @@ public class CustomFormatStepTest
{ {
new() new()
{ {
Names = new List<string> {"name1"}, TrashIds = new List<string> {"id1"},
QualityProfiles = new List<QualityProfileConfig> QualityProfiles = new List<QualityProfileConfig>
{ {
new() {Name = "profile", Score = 200} new() {Name = "profile", Score = 200}
@ -334,8 +175,6 @@ public class CustomFormatStepTest
processor.Process(guideData, testConfig, null); processor.Process(guideData, testConfig, null);
processor.DuplicatedCustomFormats.Should().BeEmpty();
processor.CustomFormatsWithOutdatedNames.Should().BeEmpty();
processor.DeletedCustomFormatsInCache.Should().BeEmpty(); processor.DeletedCustomFormatsInCache.Should().BeEmpty();
processor.ProcessedCustomFormats.Should() processor.ProcessedCustomFormats.Should()
.BeEquivalentTo(new List<ProcessedCustomFormatData> .BeEquivalentTo(new List<ProcessedCustomFormatData>

@ -14,7 +14,7 @@ public class CustomFormatApiPersistenceStepTest
{ {
private static ProcessedCustomFormatData QuickMakeCf(string cfName, string trashId, int cfId) private static ProcessedCustomFormatData QuickMakeCf(string cfName, string trashId, int cfId)
{ {
return NewCf.Processed(cfName, trashId, new TrashIdMapping(trashId, cfName) {CustomFormatId = cfId}); return NewCf.Processed(cfName, trashId, new TrashIdMapping(trashId) {CustomFormatId = cfId});
} }
[Test] [Test]
@ -24,7 +24,7 @@ public class CustomFormatApiPersistenceStepTest
transactions.NewCustomFormats.Add(QuickMakeCf("cfname1", "trashid1", 1)); transactions.NewCustomFormats.Add(QuickMakeCf("cfname1", "trashid1", 1));
transactions.UpdatedCustomFormats.Add(QuickMakeCf("cfname2", "trashid2", 2)); transactions.UpdatedCustomFormats.Add(QuickMakeCf("cfname2", "trashid2", 2));
transactions.UnchangedCustomFormats.Add(QuickMakeCf("cfname3", "trashid3", 3)); transactions.UnchangedCustomFormats.Add(QuickMakeCf("cfname3", "trashid3", 3));
transactions.DeletedCustomFormatIds.Add(new TrashIdMapping("trashid4", "cfname4") {CustomFormatId = 4}); transactions.DeletedCustomFormatIds.Add(new TrashIdMapping("trashid4") {CustomFormatId = 4});
var api = Substitute.For<ICustomFormatService>(); var api = Substitute.For<ICustomFormatService>();

@ -68,7 +68,7 @@ public class JsonTransactionStepTest
} }
}] }]
}"); }");
var cacheEntry = id != null ? new TrashIdMapping("", "") {CustomFormatId = id.Value} : null; var cacheEntry = id != null ? new TrashIdMapping("") {CustomFormatId = id.Value} : null;
var guideCfs = new List<ProcessedCustomFormatData> var guideCfs = new List<ProcessedCustomFormatData>
{ {
@ -101,7 +101,8 @@ public class JsonTransactionStepTest
[Test] [Test]
public void Combination_of_create_update_and_unchanged_and_verify_proper_json_merging() public void Combination_of_create_update_and_unchanged_and_verify_proper_json_merging()
{ {
const string radarrCfData = @"[{ var radarrCfs = JsonConvert.DeserializeObject<List<JObject>>(@"
[{
'id': 1, 'id': 1,
'name': 'user_defined', 'name': 'user_defined',
'specifications': [{ 'specifications': [{
@ -135,8 +136,9 @@ public class JsonTransactionStepTest
'value': 'value1' 'value': 'value1'
}] }]
}] }]
}]"; }]")!;
var guideCfData = JsonConvert.DeserializeObject<List<JObject>>(@"[{ var guideCfData = JsonConvert.DeserializeObject<List<JObject>>(@"
[{
'name': 'created', 'name': 'created',
'specifications': [{ 'specifications': [{
'name': 'spec5', 'name': 'spec5',
@ -169,18 +171,17 @@ public class JsonTransactionStepTest
'value': 'value1' 'value': 'value1'
} }
}] }]
}]"); }]")!;
var radarrCfs = JsonConvert.DeserializeObject<List<JObject>>(radarrCfData);
var guideCfs = new List<ProcessedCustomFormatData> var guideCfs = new List<ProcessedCustomFormatData>
{ {
NewCf.Processed("created", "", guideCfData![0]), NewCf.Processed("created", "id1", guideCfData[0]),
NewCf.Processed("updated_different_name", "", guideCfData[1], new TrashIdMapping("", "", 2)), NewCf.Processed("updated_different_name", "id2", guideCfData[1], new TrashIdMapping("id2", 2)),
NewCf.Processed("no_change", "", guideCfData[2]) NewCf.Processed("no_change", "id3", guideCfData[2], new TrashIdMapping("id3", 3))
}; };
var processor = new JsonTransactionStep(); var processor = new JsonTransactionStep();
processor.Process(guideCfs, radarrCfs!); processor.Process(guideCfs, radarrCfs);
var expectedJson = new[] var expectedJson = new[]
{ {
@ -282,12 +283,12 @@ public class JsonTransactionStepTest
}"); }");
var deletedCfsInCache = new List<TrashIdMapping> var deletedCfsInCache = new List<TrashIdMapping>
{ {
new("", "") {CustomFormatId = 2} new("") {CustomFormatId = 2}
}; };
var guideCfs = new List<ProcessedCustomFormatData> var guideCfs = new List<ProcessedCustomFormatData>
{ {
NewCf.Processed("updated", "", guideCfData, new TrashIdMapping("", "") {CustomFormatId = 1}) NewCf.Processed("updated", "", guideCfData, new TrashIdMapping("") {CustomFormatId = 1})
}; };
var radarrCfs = JsonConvert.DeserializeObject<List<JObject>>(radarrCfData); var radarrCfs = JsonConvert.DeserializeObject<List<JObject>>(radarrCfData);
@ -308,7 +309,7 @@ public class JsonTransactionStepTest
}] }]
}"; }";
var expectedTransactions = new CustomFormatTransactionData(); var expectedTransactions = new CustomFormatTransactionData();
expectedTransactions.DeletedCustomFormatIds.Add(new TrashIdMapping("", "", 2)); expectedTransactions.DeletedCustomFormatIds.Add(new TrashIdMapping("", 2));
expectedTransactions.UpdatedCustomFormats.Add(guideCfs[0]); expectedTransactions.UpdatedCustomFormats.Add(guideCfs[0]);
processor.Transactions.Should().BeEquivalentTo(expectedTransactions); processor.Transactions.Should().BeEquivalentTo(expectedTransactions);
@ -345,8 +346,8 @@ public class JsonTransactionStepTest
}]"; }]";
var deletedCfsInCache = new List<TrashIdMapping> var deletedCfsInCache = new List<TrashIdMapping>
{ {
new("testtrashid", "testname") {CustomFormatId = 2}, new("testtrashid", 2),
new("", "not_deleted") {CustomFormatId = 3} new("", 3)
}; };
var radarrCfs = JsonConvert.DeserializeObject<List<JObject>>(radarrCfData); var radarrCfs = JsonConvert.DeserializeObject<List<JObject>>(radarrCfData);
@ -355,7 +356,7 @@ public class JsonTransactionStepTest
processor.RecordDeletions(deletedCfsInCache, radarrCfs!); processor.RecordDeletions(deletedCfsInCache, radarrCfs!);
var expectedTransactions = new CustomFormatTransactionData(); var expectedTransactions = new CustomFormatTransactionData();
expectedTransactions.DeletedCustomFormatIds.Add(new TrashIdMapping("testtrashid", "testname", 2)); expectedTransactions.DeletedCustomFormatIds.Add(new TrashIdMapping("testtrashid", 2));
processor.Transactions.Should().BeEquivalentTo(expectedTransactions); processor.Transactions.Should().BeEquivalentTo(expectedTransactions);
} }
@ -414,9 +415,9 @@ public class JsonTransactionStepTest
processor.Process(guideCfs, radarrCfs!); processor.Process(guideCfs, radarrCfs!);
processor.Transactions.UpdatedCustomFormats.First().CacheEntry.Should() processor.Transactions.UpdatedCustomFormats.First().CacheEntry.Should()
.BeEquivalentTo(new TrashIdMapping("", "updated", 1)); .BeEquivalentTo(new TrashIdMapping("", 1));
processor.Transactions.UnchangedCustomFormats.First().CacheEntry.Should() processor.Transactions.UnchangedCustomFormats.First().CacheEntry.Should()
.BeEquivalentTo(new TrashIdMapping("", "no_change", 2)); .BeEquivalentTo(new TrashIdMapping("", 2));
} }
} }

@ -48,7 +48,7 @@ public class QualityProfileApiPersistenceStepTest
{ {
{ {
"profile1", CfTestUtils.NewMapping(new FormatMappingEntry( "profile1", CfTestUtils.NewMapping(new FormatMappingEntry(
NewCf.Processed("", "", new TrashIdMapping("", "") {CustomFormatId = 4}), 100)) NewCf.Processed("", "", new TrashIdMapping("") {CustomFormatId = 4}), 100))
} }
}; };
@ -109,7 +109,7 @@ public class QualityProfileApiPersistenceStepTest
{ {
{ {
"profile1", CfTestUtils.NewMappingWithReset( "profile1", CfTestUtils.NewMappingWithReset(
new FormatMappingEntry(NewCf.Processed("", "", new TrashIdMapping("", "", 2)), 100)) new FormatMappingEntry(NewCf.Processed("", "", new TrashIdMapping("", 2)), 100))
} }
}; };
@ -183,11 +183,11 @@ public class QualityProfileApiPersistenceStepTest
{ {
"profile1", CfTestUtils.NewMapping( "profile1", CfTestUtils.NewMapping(
// First match by ID // First match by ID
new FormatMappingEntry(NewCf.Processed("", "", new TrashIdMapping("", "", 4)), 100), new FormatMappingEntry(NewCf.Processed("", "", new TrashIdMapping("", 4)), 100),
// Should NOT match because we do not use names to assign scores // Should NOT match because we do not use names to assign scores
new FormatMappingEntry(NewCf.Processed("", "", new TrashIdMapping("", "BR-DISK")), 101), new FormatMappingEntry(NewCf.Processed("", "", new TrashIdMapping("")), 101),
// Second match by ID // Second match by ID
new FormatMappingEntry(NewCf.Processed("", "", new TrashIdMapping("", "", 1)), 102)) new FormatMappingEntry(NewCf.Processed("", "", new TrashIdMapping("", 1)), 102))
} }
}; };

@ -0,0 +1,14 @@
using System.Diagnostics.CodeAnalysis;
using FluentAssertions;
using NUnit.Framework;
[SetUpFixture]
[SuppressMessage("ReSharper", "CheckNamespace")]
public class GlobalTestSetup
{
[OneTimeSetUp]
public void Setup()
{
AssertionOptions.FormattingOptions.MaxDepth = 100;
}
}

@ -32,7 +32,7 @@ public class RadarrConfigurationTest
}; };
[TestCaseSource(nameof(NameOrIdsTestData))] [TestCaseSource(nameof(NameOrIdsTestData))]
public void Custom_format_is_valid_with_one_of_either_names_or_trash_id(Collection<string> namesList, public void Custom_format_is_valid_with_trash_id(Collection<string> namesList,
Collection<string> trashIdsList) Collection<string> trashIdsList)
{ {
var config = new RadarrConfiguration var config = new RadarrConfiguration
@ -41,7 +41,7 @@ public class RadarrConfigurationTest
BaseUrl = "required value", BaseUrl = "required value",
CustomFormats = new List<CustomFormatConfig> CustomFormats = new List<CustomFormatConfig>
{ {
new() {Names = namesList, TrashIds = trashIdsList} new() {TrashIds = trashIdsList}
} }
}; };
@ -65,7 +65,7 @@ public class RadarrConfigurationTest
{ {
"Property 'base_url' is required", "Property 'base_url' is required",
"Property 'api_key' is required", "Property 'api_key' is required",
"'custom_formats' elements must contain at least one element in either 'names' or 'trash_ids'", "'custom_formats' elements must contain at least one element under 'trash_ids'",
"'name' is required for elements under 'quality_profiles'", "'name' is required for elements under 'quality_profiles'",
"'type' is required for 'quality_definition'" "'type' is required for 'quality_definition'"
}; };
@ -86,7 +86,7 @@ public class RadarrConfigurationTest
{ {
new() new()
{ {
Names = new List<string> {"required value"}, TrashIds = new List<string> {"required value"},
QualityProfiles = new List<QualityProfileConfig> QualityProfiles = new List<QualityProfileConfig>
{ {
new() {Name = "required value"} new() {Name = "required value"}

@ -13,7 +13,6 @@ public abstract class ServiceConfiguration : IServiceConfiguration
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public class CustomFormatConfig public class CustomFormatConfig
{ {
public ICollection<string> Names { get; init; } = new List<string>();
public ICollection<string> TrashIds { get; init; } = new List<string>(); public ICollection<string> TrashIds { get; init; } = new List<string>();
public ICollection<QualityProfileConfig> QualityProfiles { get; init; } = new List<QualityProfileConfig>(); public ICollection<QualityProfileConfig> QualityProfiles { get; init; } = new List<QualityProfileConfig>();
} }

@ -120,7 +120,7 @@ internal class CustomFormatUpdater : ICustomFormatUpdater
if (deleted.Count > 0) if (deleted.Count > 0)
{ {
_log.Information("Deleted {Count} Custom Formats: {CustomFormats}", deleted.Count, _log.Information("Deleted {Count} Custom Formats: {CustomFormats}", deleted.Count,
deleted.Select(r => r.CustomFormatName)); deleted.Select(r => r.TrashId));
} }
var totalCount = created.Count + updated.Count; var totalCount = created.Count + updated.Count;
@ -138,31 +138,11 @@ internal class CustomFormatUpdater : ICustomFormatUpdater
{ {
_console.Output.WriteLine(""); _console.Output.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. Recyclarr is not able to choose which one you actually " +
"wanted. To resolve this ambiguity, use the `trash_ids` property in your YML " +
"configuration to refer to the custom format using its Trash ID instead of its name");
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.Output.WriteLine("");
}
if (_guideProcessor.CustomFormatsNotInGuide.Count > 0) if (_guideProcessor.CustomFormatsNotInGuide.Count > 0)
{ {
_log.Warning("The Custom Formats below do not exist in the guide and will " + _log.Warning("The Custom Formats below do not exist in the guide and will " +
"be skipped. Names must match the 'name' field in the actual JSON, not the header in " + "be skipped. Trash IDs must match what is listed in the output when using the " +
"the guide! Either fix the names or remove them from your YAML config to resolve this " + "`--list-custom-formats` option");
"warning");
_log.Warning("{CfList}", _guideProcessor.CustomFormatsNotInGuide); _log.Warning("{CfList}", _guideProcessor.CustomFormatsNotInGuide);
_console.Output.WriteLine(""); _console.Output.WriteLine("");
@ -220,20 +200,6 @@ internal class CustomFormatUpdater : ICustomFormatUpdater
_console.Output.WriteLine(""); _console.Output.WriteLine("");
} }
if (_guideProcessor.CustomFormatsWithOutdatedNames.Count > 0)
{
_log.Warning("One or more custom format names in your YAML config have been renamed in the guide and " +
"are outdated. Each outdated name will be listed below. These custom formats will refuse " +
"to sync if your cache is deleted. To fix this warning, rename each one to its new name");
foreach (var (oldName, newName) in _guideProcessor.CustomFormatsWithOutdatedNames)
{
_log.Warning(" - '{OldName}' -> '{NewName}'", oldName, newName);
}
_console.Output.WriteLine("");
}
return true; return true;
} }

@ -14,14 +14,12 @@ public class CustomFormatCache
public class TrashIdMapping public class TrashIdMapping
{ {
public TrashIdMapping(string trashId, string customFormatName, int customFormatId = default) public TrashIdMapping(string trashId, int customFormatId = default)
{ {
CustomFormatName = customFormatName;
TrashId = trashId; TrashId = trashId;
CustomFormatId = customFormatId; CustomFormatId = customFormatId;
} }
public string CustomFormatName { get; set; }
public string TrashId { get; } public string TrashId { get; }
public int CustomFormatId { get; set; } public int CustomFormatId { get; set; }
} }

@ -19,11 +19,10 @@ public class ProcessedCustomFormatData
public int? Score => _data.Score; public int? Score => _data.Score;
public JObject Json { get; set; } public JObject Json { get; set; }
public TrashIdMapping? CacheEntry { get; set; } public TrashIdMapping? CacheEntry { get; set; }
public string CacheAwareName => CacheEntry?.CustomFormatName ?? Name;
public void SetCache(int customFormatId) public void SetCache(int customFormatId)
{ {
CacheEntry ??= new TrashIdMapping(TrashId, Name); CacheEntry ??= new TrashIdMapping(TrashId);
CacheEntry.CustomFormatId = customFormatId; CacheEntry.CustomFormatId = customFormatId;
} }

@ -44,12 +44,6 @@ internal class GuideProcessor : IGuideProcessor
public IReadOnlyCollection<TrashIdMapping> DeletedCustomFormatsInCache public IReadOnlyCollection<TrashIdMapping> DeletedCustomFormatsInCache
=> _steps.CustomFormat.DeletedCustomFormatsInCache; => _steps.CustomFormat.DeletedCustomFormatsInCache;
public IReadOnlyCollection<(string, string)> CustomFormatsWithOutdatedNames
=> _steps.CustomFormat.CustomFormatsWithOutdatedNames;
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) IGuideService guideService)
{ {

@ -1,52 +1,31 @@
using Common.Extensions; using Common.Extensions;
using Serilog;
using TrashLib.Config.Services; using TrashLib.Config.Services;
using TrashLib.Services.CustomFormat.Models; using TrashLib.Services.CustomFormat.Models;
namespace TrashLib.Services.CustomFormat.Processors.GuideSteps; namespace TrashLib.Services.CustomFormat.Processors.GuideSteps;
/// <remarks>
/// The purpose of this step is to validate the custom format data in the configs:
///
/// - Validate that custom formats specified in the config exist in the guide.
/// - Removal of duplicates.
/// </remarks>
public class ConfigStep : IConfigStep public class ConfigStep : IConfigStep
{ {
private readonly ILogger _log;
private readonly List<ProcessedConfigData> _configData = new(); private readonly List<ProcessedConfigData> _configData = new();
private readonly List<string> _customFormatsNotInGuide = new(); private readonly List<string> _customFormatsNotInGuide = new();
public IReadOnlyCollection<string> CustomFormatsNotInGuide => _customFormatsNotInGuide; public IReadOnlyCollection<string> CustomFormatsNotInGuide => _customFormatsNotInGuide;
public IReadOnlyCollection<ProcessedConfigData> ConfigData => _configData; public IReadOnlyCollection<ProcessedConfigData> ConfigData => _configData;
public ConfigStep(ILogger log)
{
_log = log;
}
public void Process( public void Process(
IReadOnlyCollection<ProcessedCustomFormatData> processedCfs, IReadOnlyCollection<ProcessedCustomFormatData> processedCfs,
IReadOnlyCollection<CustomFormatConfig> config) IReadOnlyCollection<CustomFormatConfig> config)
{ {
if (config.SelectMany(x => x.Names).Any())
{
_log.Warning(
"`names` list for `custom_formats` is deprecated and will be removed in the future; use " +
"`trash_ids` instead");
}
foreach (var singleConfig in config) foreach (var singleConfig in config)
{ {
var validCfs = new List<ProcessedCustomFormatData>(); var validCfs = new List<ProcessedCustomFormatData>();
foreach (var name in singleConfig.Names)
{
var match = FindCustomFormatByName(processedCfs, name);
if (match == null)
{
_customFormatsNotInGuide.Add(name);
}
else
{
validCfs.Add(match);
}
}
foreach (var trashId in singleConfig.TrashIds) foreach (var trashId in singleConfig.TrashIds)
{ {
var match = processedCfs.FirstOrDefault(cf => cf.TrashId.EqualsIgnoreCase(trashId)); var match = processedCfs.FirstOrDefault(cf => cf.TrashId.EqualsIgnoreCase(trashId));
@ -69,11 +48,4 @@ public class ConfigStep : IConfigStep
}); });
} }
} }
private static ProcessedCustomFormatData? FindCustomFormatByName(
IReadOnlyCollection<ProcessedCustomFormatData> processedCfs, string name)
{
return processedCfs.FirstOrDefault(cf => cf.CacheEntry?.CustomFormatName.EqualsIgnoreCase(name) ?? false)
?? processedCfs.FirstOrDefault(cf => cf.Name.EqualsIgnoreCase(name));
}
} }

@ -1,4 +1,3 @@
using Common.Extensions;
using TrashLib.Config.Services; using TrashLib.Config.Services;
using TrashLib.Services.CustomFormat.Models; using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache; using TrashLib.Services.CustomFormat.Models.Cache;
@ -7,15 +6,11 @@ namespace TrashLib.Services.CustomFormat.Processors.GuideSteps;
public class CustomFormatStep : ICustomFormatStep public class CustomFormatStep : ICustomFormatStep
{ {
private readonly List<(string, string)> _customFormatsWithOutdatedNames = new();
private readonly List<ProcessedCustomFormatData> _processedCustomFormats = new(); private readonly List<ProcessedCustomFormatData> _processedCustomFormats = new();
private readonly List<TrashIdMapping> _deletedCustomFormatsInCache = new(); private readonly List<TrashIdMapping> _deletedCustomFormatsInCache = new();
private readonly Dictionary<string, List<ProcessedCustomFormatData>> _duplicatedCustomFormats = new();
public IReadOnlyCollection<(string, string)> CustomFormatsWithOutdatedNames => _customFormatsWithOutdatedNames;
public IReadOnlyCollection<ProcessedCustomFormatData> ProcessedCustomFormats => _processedCustomFormats; public IReadOnlyCollection<ProcessedCustomFormatData> ProcessedCustomFormats => _processedCustomFormats;
public IReadOnlyCollection<TrashIdMapping> DeletedCustomFormatsInCache => _deletedCustomFormatsInCache; public IReadOnlyCollection<TrashIdMapping> DeletedCustomFormatsInCache => _deletedCustomFormatsInCache;
public IDictionary<string, List<ProcessedCustomFormatData>> DuplicatedCustomFormats => _duplicatedCustomFormats;
public void Process( public void Process(
IList<CustomFormatData> customFormatGuideData, IList<CustomFormatData> customFormatGuideData,
@ -36,63 +31,8 @@ public class CustomFormatStep : ICustomFormatStep
(_, cf) => cf, (_, cf) => cf,
StringComparer.InvariantCultureIgnoreCase)); StringComparer.InvariantCultureIgnoreCase));
// Build a list of CF names under the `names` property in YAML. Exclude any names that
// are already provided by the `trash_ids` property.
var allConfigCfNames = config
.SelectMany(c => c.Names)
.Distinct(StringComparer.CurrentCultureIgnoreCase)
.Where(n => !ProcessedCustomFormats.Any(cf => cf.CacheAwareName.EqualsIgnoreCase(n)))
.ToList();
// Perform updates and deletions based on matches in the cache. Matches in the cache are by ID.
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));
if (configName != null)
{
if (cf.CacheEntry != null)
{
// The cache entry might be using an old name. This will happen if:
// - A user has synced this CF before, AND
// - The name of the CF in the guide changed, AND
// - The user updated the name in their config to match the name in the guide.
cf.CacheEntry.CustomFormatName = cf.Name;
}
_processedCustomFormats.Add(cf);
continue;
}
// Does the name of the CF in the cache match a name in the config? If yes, we keep it.
configName = allConfigCfNames.FirstOrDefault(n => n.EqualsIgnoreCase(cf.CacheEntry?.CustomFormatName));
if (configName != null)
{
// Config name is out of sync with the guide and should be updated
_customFormatsWithOutdatedNames.Add((configName, cf.Name));
_processedCustomFormats.Add(cf);
}
// If we get here, we can't find a match in the config using cache or guide name, so the user must have
// removed it from their config. This will get marked for deletion later.
}
// Orphaned entries in cache represent custom formats we need to delete. // Orphaned entries in cache represent custom formats we need to delete.
ProcessDeletedCustomFormats(cache); ProcessDeletedCustomFormats(cache);
// Check for multiple custom formats with the same name in the guide data (e.g. "DoVi")
ProcessDuplicates();
}
private void ProcessDuplicates()
{
_duplicatedCustomFormats.Clear();
_duplicatedCustomFormats.AddRange(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 cf, private static ProcessedCustomFormatData ProcessCustomFormatData(CustomFormatData cf,

@ -8,8 +8,6 @@ public interface ICustomFormatStep
{ {
IReadOnlyCollection<ProcessedCustomFormatData> ProcessedCustomFormats { get; } IReadOnlyCollection<ProcessedCustomFormatData> ProcessedCustomFormats { get; }
IReadOnlyCollection<TrashIdMapping> DeletedCustomFormatsInCache { get; } IReadOnlyCollection<TrashIdMapping> DeletedCustomFormatsInCache { get; }
IReadOnlyCollection<(string, string)> CustomFormatsWithOutdatedNames { get; }
IDictionary<string, List<ProcessedCustomFormatData>> DuplicatedCustomFormats { get; }
void Process(IList<CustomFormatData> customFormatGuideData, void Process(IList<CustomFormatData> customFormatGuideData,
IReadOnlyCollection<CustomFormatConfig> config, CustomFormatCache? cache); IReadOnlyCollection<CustomFormatConfig> config, CustomFormatCache? cache);

@ -13,8 +13,6 @@ internal interface IGuideProcessor
IDictionary<string, QualityProfileCustomFormatScoreMapping> ProfileScores { get; } IDictionary<string, QualityProfileCustomFormatScoreMapping> ProfileScores { get; }
IReadOnlyCollection<(string name, string trashId, string profileName)> CustomFormatsWithoutScore { get; } IReadOnlyCollection<(string name, string trashId, string profileName)> CustomFormatsWithoutScore { get; }
IReadOnlyCollection<TrashIdMapping> DeletedCustomFormatsInCache { get; } IReadOnlyCollection<TrashIdMapping> DeletedCustomFormatsInCache { get; }
IReadOnlyCollection<(string, string)> CustomFormatsWithOutdatedNames { get; }
IDictionary<string, List<ProcessedCustomFormatData>> DuplicatedCustomFormats { get; }
IReadOnlyDictionary<string, Dictionary<string, HashSet<int>>> DuplicateScores { get; } IReadOnlyDictionary<string, Dictionary<string, HashSet<int>>> DuplicateScores { get; }
Task BuildGuideDataAsync(IEnumerable<CustomFormatConfig> config, CustomFormatCache? cache, Task BuildGuideDataAsync(IEnumerable<CustomFormatConfig> config, CustomFormatCache? cache,

@ -1,5 +1,4 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using Common.Extensions;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using TrashLib.Services.CustomFormat.Models; using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache; using TrashLib.Services.CustomFormat.Models.Cache;
@ -65,7 +64,7 @@ internal class JsonTransactionStep : IJsonTransactionStep
// The 'Where' excludes cached CFs that were deleted manually by the user in Radarr // 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 // FindRadarrCf() specifies 'null' for name because we should never delete unless an ID is found
foreach (var del in deletedCfsInCache.Where( foreach (var del in deletedCfsInCache.Where(
del => FindServiceCf(cfs, del.CustomFormatId, null) != null)) del => FindServiceCf(cfs, del.CustomFormatId) != null))
{ {
Transactions.DeletedCustomFormatIds.Add(del); Transactions.DeletedCustomFormatIds.Add(del);
} }
@ -73,10 +72,10 @@ internal class JsonTransactionStep : IJsonTransactionStep
private static JObject? FindServiceCf(IReadOnlyCollection<JObject> serviceCfs, ProcessedCustomFormatData guideCf) private static JObject? FindServiceCf(IReadOnlyCollection<JObject> serviceCfs, ProcessedCustomFormatData guideCf)
{ {
return FindServiceCf(serviceCfs, guideCf.CacheEntry?.CustomFormatId, guideCf.Name); return FindServiceCf(serviceCfs, guideCf.CacheEntry?.CustomFormatId);
} }
private static JObject? FindServiceCf(IReadOnlyCollection<JObject> serviceCfs, int? cfId, string? cfName) private static JObject? FindServiceCf(IReadOnlyCollection<JObject> serviceCfs, int? cfId)
{ {
JObject? match = null; JObject? match = null;
@ -86,12 +85,6 @@ internal class JsonTransactionStep : IJsonTransactionStep
match = serviceCfs.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 = serviceCfs.FirstOrDefault(rcf => cfName.EqualsIgnoreCase(rcf.Value<string>("name")));
}
return match; return match;
} }

@ -4,7 +4,7 @@ public interface IRadarrValidationMessages
{ {
string BaseUrl { get; } string BaseUrl { get; }
string ApiKey { get; } string ApiKey { get; }
string CustomFormatNamesAndIds { get; } string CustomFormatTrashIds { get; }
string QualityProfileName { get; } string QualityProfileName { get; }
string QualityDefinitionType { get; } string QualityDefinitionType { get; }
} }

@ -27,8 +27,7 @@ internal class CustomFormatConfigValidator : AbstractValidator<CustomFormatConfi
IRadarrValidationMessages messages, IRadarrValidationMessages messages,
IValidator<QualityProfileConfig> qualityProfileConfigValidator) IValidator<QualityProfileConfig> qualityProfileConfigValidator)
{ {
RuleFor(x => x.Names).NotEmpty().When(x => x.TrashIds.Count == 0) RuleFor(x => x.TrashIds).NotEmpty().WithMessage(messages.CustomFormatTrashIds);
.WithMessage(messages.CustomFormatNamesAndIds);
RuleForEach(x => x.QualityProfiles).SetValidator(qualityProfileConfigValidator); RuleForEach(x => x.QualityProfiles).SetValidator(qualityProfileConfigValidator);
} }
} }

@ -8,8 +8,8 @@ internal class RadarrValidationMessages : IRadarrValidationMessages
public string ApiKey => public string ApiKey =>
"Property 'api_key' is required"; "Property 'api_key' is required";
public string CustomFormatNamesAndIds => public string CustomFormatTrashIds =>
"'custom_formats' elements must contain at least one element in either 'names' or 'trash_ids'"; "'custom_formats' elements must contain at least one element under 'trash_ids'";
public string QualityProfileName => public string QualityProfileName =>
"'name' is required for elements under 'quality_profiles'"; "'name' is required for elements under 'quality_profiles'";

Loading…
Cancel
Save