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]
### 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
### Fixed

@ -1,3 +1,4 @@
using FluentAssertions;
using FluentAssertions.Equivalency;
using FluentAssertions.Json;
using Newtonsoft.Json.Linq;
@ -16,7 +17,9 @@ public class JsonEquivalencyStep : IEquivalencyStep
}
((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;
}

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

@ -4,7 +4,6 @@ using FluentAssertions;
using Newtonsoft.Json.Linq;
using NSubstitute;
using NUnit.Framework;
using Serilog;
using TestLibrary.FluentAssertions;
using TrashLib.Config.Services;
using TrashLib.Services.CustomFormat.Guide;
@ -23,7 +22,7 @@ public class GuideProcessorTest
private class TestGuideProcessorSteps : IGuideProcessorSteps
{
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();
}
@ -68,7 +67,13 @@ public class GuideProcessorTest
{
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>
{
new() {Name = "profile1"},
@ -77,7 +82,11 @@ public class GuideProcessorTest
},
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>
{
new() {Name = "profile3"},
@ -97,7 +106,8 @@ public class GuideProcessorTest
NewCf.Processed("No Score", "abc")
};
guideProcessor.ProcessedCustomFormats.Should().BeEquivalentTo(expectedProcessedCustomFormatData);
guideProcessor.ProcessedCustomFormats.Should()
.BeEquivalentTo(expectedProcessedCustomFormatData, op => op.Using(new JsonEquivalencyStep()));
guideProcessor.ConfigData.Should()
.BeEquivalentTo(new List<ProcessedConfigData>

@ -4,7 +4,6 @@ using NUnit.Framework;
using TestLibrary.AutoFixture;
using TrashLib.Config.Services;
using TrashLib.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
using TrashLib.Services.CustomFormat.Processors.GuideSteps;
using TrashLib.TestLibrary;
@ -14,52 +13,20 @@ namespace TrashLib.Tests.CustomFormat.Processors.GuideSteps;
[Parallelizable(ParallelScope.All)]
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]
public void Custom_formats_missing_from_config_are_skipped(ConfigStep processor)
{
var testProcessedCfs = new List<ProcessedCustomFormatData>
{
NewCf.Processed("name1", ""),
NewCf.Processed("name2", "")
NewCf.Processed("name1", "id1"),
NewCf.Processed("name2", "id2")
};
var testConfig = new CustomFormatConfig[]
{
new()
{
Names = new List<string> {"name1"}
TrashIds = new List<string> {"id1"}
}
};
@ -72,7 +39,7 @@ public class ConfigStepTest
{
CustomFormats = new List<ProcessedCustomFormatData>
{
NewCf.Processed("name1", "")
NewCf.Processed("name1", "id1")
}
}
}, op => op
@ -85,21 +52,21 @@ public class ConfigStepTest
{
var testProcessedCfs = new List<ProcessedCustomFormatData>
{
NewCf.Processed("name1", ""),
NewCf.Processed("name2", "")
NewCf.Processed("name1", "id1"),
NewCf.Processed("name2", "id2")
};
var testConfig = new CustomFormatConfig[]
{
new()
{
Names = new List<string> {"name1", "name3"}
TrashIds = new List<string> {"id1", "id3"}
}
};
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))
.WhenTypeIs<JToken>());
processor.ConfigData.Should().BeEquivalentTo(new List<ProcessedConfigData>
@ -108,7 +75,7 @@ public class ConfigStepTest
{
CustomFormats = new List<ProcessedCustomFormatData>
{
NewCf.Processed("name1", "")
NewCf.Processed("name1", "id1")
}
}
}, op => op
@ -117,7 +84,7 @@ public class ConfigStepTest
}
[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>
{
@ -126,11 +93,7 @@ public class ConfigStepTest
var testConfig = new CustomFormatConfig[]
{
new()
{
Names = new List<string> {"name1"},
TrashIds = new List<string> {"id1"}
}
new() {TrashIds = new List<string> {"id1", "id1"}}
};
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]
public void Cfs_not_in_config_are_skipped(CustomFormatStep processor)
{
var ctx = new Context();
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.DuplicatedCustomFormats.Should().BeEmpty();
processor.CustomFormatsWithOutdatedNames.Should().BeEmpty();
processor.DeletedCustomFormatsInCache.Should().BeEmpty();
processor.ProcessedCustomFormats.Should()
.BeEquivalentTo(new List<ProcessedCustomFormatData>
@ -122,14 +51,12 @@ public class CustomFormatStepTest
var ctx = new Context();
var testConfig = new List<CustomFormatConfig>
{
new() {Names = new List<string> {"name1", "name3"}},
new() {Names = new List<string> {"name2"}}
new() {TrashIds = new List<string> {"id1", "id3"}},
new() {TrashIds = new List<string> {"id2"}}
};
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>
{
@ -150,20 +77,18 @@ public class CustomFormatStepTest
var testConfig = new List<CustomFormatConfig>
{
new() {Names = new List<string> {"name1"}}
new() {TrashIds = new List<string> {"id1"}}
};
var testCache = new CustomFormatCache
{
TrashIdMappings = new Collection<TrashIdMapping> {new("id1000", "name1")}
TrashIdMappings = new Collection<TrashIdMapping> {new("id1000")}
};
processor.Process(guideData, testConfig, testCache);
processor.DuplicatedCustomFormats.Should().BeEmpty();
processor.CustomFormatsWithOutdatedNames.Should().BeEmpty();
processor.DeletedCustomFormatsInCache.Should()
.BeEquivalentTo(new[] {new TrashIdMapping("id1000", "name1")});
.BeEquivalentTo(new[] {new TrashIdMapping("id1000")});
processor.ProcessedCustomFormats.Should().BeEquivalentTo(new List<ProcessedCustomFormatData>
{
NewCf.Processed("name1", "id1")
@ -175,7 +100,7 @@ public class CustomFormatStepTest
{
var cache = new CustomFormatCache
{
TrashIdMappings = new Collection<TrashIdMapping> {new("id1", "3D", 9)}
TrashIdMappings = new Collection<TrashIdMapping> {new("id1", 9)}
};
var guideCfs = new List<CustomFormatData>
@ -185,90 +110,10 @@ public class CustomFormatStepTest
processor.Process(guideCfs, Array.Empty<CustomFormatConfig>(), cache);
processor.DuplicatedCustomFormats.Should().BeEmpty();
processor.CustomFormatsWithOutdatedNames.Should().BeEmpty();
processor.DeletedCustomFormatsInCache.Should().BeEquivalentTo(new[] {cache.TrashIdMappings[0]});
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]
public void Match_custom_format_using_trash_id(CustomFormatStep processor)
{
@ -285,8 +130,6 @@ public class CustomFormatStepTest
processor.Process(guideData, testConfig, null);
processor.DuplicatedCustomFormats.Should().BeEmpty();
processor.CustomFormatsWithOutdatedNames.Should().BeEmpty();
processor.DeletedCustomFormatsInCache.Should().BeEmpty();
processor.ProcessedCustomFormats.Should()
.BeEquivalentTo(new List<ProcessedCustomFormatData>
@ -301,13 +144,11 @@ public class CustomFormatStepTest
var ctx = new Context();
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.DuplicatedCustomFormats.Should().BeEmpty();
processor.CustomFormatsWithOutdatedNames.Should().BeEmpty();
processor.DeletedCustomFormatsInCache.Should().BeEmpty();
processor.ProcessedCustomFormats.Should().BeEmpty();
}
@ -324,7 +165,7 @@ public class CustomFormatStepTest
{
new()
{
Names = new List<string> {"name1"},
TrashIds = new List<string> {"id1"},
QualityProfiles = new List<QualityProfileConfig>
{
new() {Name = "profile", Score = 200}
@ -334,8 +175,6 @@ public class CustomFormatStepTest
processor.Process(guideData, testConfig, null);
processor.DuplicatedCustomFormats.Should().BeEmpty();
processor.CustomFormatsWithOutdatedNames.Should().BeEmpty();
processor.DeletedCustomFormatsInCache.Should().BeEmpty();
processor.ProcessedCustomFormats.Should()
.BeEquivalentTo(new List<ProcessedCustomFormatData>

@ -14,7 +14,7 @@ public class CustomFormatApiPersistenceStepTest
{
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]
@ -24,7 +24,7 @@ public class CustomFormatApiPersistenceStepTest
transactions.NewCustomFormats.Add(QuickMakeCf("cfname1", "trashid1", 1));
transactions.UpdatedCustomFormats.Add(QuickMakeCf("cfname2", "trashid2", 2));
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>();

@ -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>
{
@ -101,7 +101,8 @@ public class JsonTransactionStepTest
[Test]
public void Combination_of_create_update_and_unchanged_and_verify_proper_json_merging()
{
const string radarrCfData = @"[{
var radarrCfs = JsonConvert.DeserializeObject<List<JObject>>(@"
[{
'id': 1,
'name': 'user_defined',
'specifications': [{
@ -135,8 +136,9 @@ public class JsonTransactionStepTest
'value': 'value1'
}]
}]
}]";
var guideCfData = JsonConvert.DeserializeObject<List<JObject>>(@"[{
}]")!;
var guideCfData = JsonConvert.DeserializeObject<List<JObject>>(@"
[{
'name': 'created',
'specifications': [{
'name': 'spec5',
@ -169,18 +171,17 @@ public class JsonTransactionStepTest
'value': 'value1'
}
}]
}]");
}]")!;
var radarrCfs = JsonConvert.DeserializeObject<List<JObject>>(radarrCfData);
var guideCfs = new List<ProcessedCustomFormatData>
{
NewCf.Processed("created", "", guideCfData![0]),
NewCf.Processed("updated_different_name", "", guideCfData[1], new TrashIdMapping("", "", 2)),
NewCf.Processed("no_change", "", guideCfData[2])
NewCf.Processed("created", "id1", guideCfData[0]),
NewCf.Processed("updated_different_name", "id2", guideCfData[1], new TrashIdMapping("id2", 2)),
NewCf.Processed("no_change", "id3", guideCfData[2], new TrashIdMapping("id3", 3))
};
var processor = new JsonTransactionStep();
processor.Process(guideCfs, radarrCfs!);
processor.Process(guideCfs, radarrCfs);
var expectedJson = new[]
{
@ -282,12 +283,12 @@ public class JsonTransactionStepTest
}");
var deletedCfsInCache = new List<TrashIdMapping>
{
new("", "") {CustomFormatId = 2}
new("") {CustomFormatId = 2}
};
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);
@ -308,7 +309,7 @@ public class JsonTransactionStepTest
}]
}";
var expectedTransactions = new CustomFormatTransactionData();
expectedTransactions.DeletedCustomFormatIds.Add(new TrashIdMapping("", "", 2));
expectedTransactions.DeletedCustomFormatIds.Add(new TrashIdMapping("", 2));
expectedTransactions.UpdatedCustomFormats.Add(guideCfs[0]);
processor.Transactions.Should().BeEquivalentTo(expectedTransactions);
@ -345,8 +346,8 @@ public class JsonTransactionStepTest
}]";
var deletedCfsInCache = new List<TrashIdMapping>
{
new("testtrashid", "testname") {CustomFormatId = 2},
new("", "not_deleted") {CustomFormatId = 3}
new("testtrashid", 2),
new("", 3)
};
var radarrCfs = JsonConvert.DeserializeObject<List<JObject>>(radarrCfData);
@ -355,7 +356,7 @@ public class JsonTransactionStepTest
processor.RecordDeletions(deletedCfsInCache, radarrCfs!);
var expectedTransactions = new CustomFormatTransactionData();
expectedTransactions.DeletedCustomFormatIds.Add(new TrashIdMapping("testtrashid", "testname", 2));
expectedTransactions.DeletedCustomFormatIds.Add(new TrashIdMapping("testtrashid", 2));
processor.Transactions.Should().BeEquivalentTo(expectedTransactions);
}
@ -414,9 +415,9 @@ public class JsonTransactionStepTest
processor.Process(guideCfs, radarrCfs!);
processor.Transactions.UpdatedCustomFormats.First().CacheEntry.Should()
.BeEquivalentTo(new TrashIdMapping("", "updated", 1));
.BeEquivalentTo(new TrashIdMapping("", 1));
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(
NewCf.Processed("", "", new TrashIdMapping("", "") {CustomFormatId = 4}), 100))
NewCf.Processed("", "", new TrashIdMapping("") {CustomFormatId = 4}), 100))
}
};
@ -109,7 +109,7 @@ public class QualityProfileApiPersistenceStepTest
{
{
"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(
// 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
new FormatMappingEntry(NewCf.Processed("", "", new TrashIdMapping("", "BR-DISK")), 101),
new FormatMappingEntry(NewCf.Processed("", "", new TrashIdMapping("")), 101),
// 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))]
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)
{
var config = new RadarrConfiguration
@ -41,7 +41,7 @@ public class RadarrConfigurationTest
BaseUrl = "required value",
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 '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'",
"'type' is required for 'quality_definition'"
};
@ -86,7 +86,7 @@ public class RadarrConfigurationTest
{
new()
{
Names = new List<string> {"required value"},
TrashIds = new List<string> {"required value"},
QualityProfiles = new List<QualityProfileConfig>
{
new() {Name = "required value"}

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

@ -120,7 +120,7 @@ internal class CustomFormatUpdater : ICustomFormatUpdater
if (deleted.Count > 0)
{
_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;
@ -138,31 +138,11 @@ internal class CustomFormatUpdater : ICustomFormatUpdater
{
_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)
{
_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 " +
"the guide! Either fix the names or remove them from your YAML config to resolve this " +
"warning");
"be skipped. Trash IDs must match what is listed in the output when using the " +
"`--list-custom-formats` option");
_log.Warning("{CfList}", _guideProcessor.CustomFormatsNotInGuide);
_console.Output.WriteLine("");
@ -220,20 +200,6 @@ internal class CustomFormatUpdater : ICustomFormatUpdater
_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;
}

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

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

@ -44,12 +44,6 @@ internal class GuideProcessor : IGuideProcessor
public IReadOnlyCollection<TrashIdMapping> 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,
IGuideService guideService)
{

@ -1,52 +1,31 @@
using Common.Extensions;
using Serilog;
using TrashLib.Config.Services;
using TrashLib.Services.CustomFormat.Models;
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
{
private readonly ILogger _log;
private readonly List<ProcessedConfigData> _configData = new();
private readonly List<string> _customFormatsNotInGuide = new();
public IReadOnlyCollection<string> CustomFormatsNotInGuide => _customFormatsNotInGuide;
public IReadOnlyCollection<ProcessedConfigData> ConfigData => _configData;
public ConfigStep(ILogger log)
{
_log = log;
}
public void Process(
IReadOnlyCollection<ProcessedCustomFormatData> processedCfs,
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)
{
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)
{
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.Services.CustomFormat.Models;
using TrashLib.Services.CustomFormat.Models.Cache;
@ -7,15 +6,11 @@ namespace TrashLib.Services.CustomFormat.Processors.GuideSteps;
public class CustomFormatStep : ICustomFormatStep
{
private readonly List<(string, string)> _customFormatsWithOutdatedNames = new();
private readonly List<ProcessedCustomFormatData> _processedCustomFormats = 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<TrashIdMapping> DeletedCustomFormatsInCache => _deletedCustomFormatsInCache;
public IDictionary<string, List<ProcessedCustomFormatData>> DuplicatedCustomFormats => _duplicatedCustomFormats;
public void Process(
IList<CustomFormatData> customFormatGuideData,
@ -36,63 +31,8 @@ public class CustomFormatStep : ICustomFormatStep
(_, cf) => cf,
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.
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,

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

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

@ -1,5 +1,4 @@
using System.Collections.ObjectModel;
using Common.Extensions;
using Newtonsoft.Json.Linq;
using TrashLib.Services.CustomFormat.Models;
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
// FindRadarrCf() specifies 'null' for name because we should never delete unless an ID is found
foreach (var del in deletedCfsInCache.Where(
del => FindServiceCf(cfs, del.CustomFormatId, null) != null))
del => FindServiceCf(cfs, del.CustomFormatId) != null))
{
Transactions.DeletedCustomFormatIds.Add(del);
}
@ -73,10 +72,10 @@ internal class JsonTransactionStep : IJsonTransactionStep
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;
@ -86,12 +85,6 @@ internal class JsonTransactionStep : IJsonTransactionStep
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;
}

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

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

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

Loading…
Cancel
Save