diff --git a/CHANGELOG.md b/CHANGELOG.md
index 93a86d77..17f23ce6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Added
+
+- New setting `reset_unmatched_scores` under `custom_formats.quality_profiles` in YAML config which
+ allows Trash Updater to set scores to 0 if they were not in the list of custom format names or
+ listed but had no score applied (e.g. no score in guide).
+
### Changed
- Support the new custom format structure in the guide: JSON files are parsed directly now. Trash
diff --git a/src/Trash.TestLibrary/CfTestUtils.cs b/src/Trash.TestLibrary/CfTestUtils.cs
new file mode 100644
index 00000000..c50b3328
--- /dev/null
+++ b/src/Trash.TestLibrary/CfTestUtils.cs
@@ -0,0 +1,14 @@
+using System.Linq;
+using Trash.Radarr.CustomFormat.Models;
+
+namespace Trash.TestLibrary
+{
+ public static class CfTestUtils
+ {
+ public static QualityProfileCustomFormatScoreMapping NewMapping(params FormatMappingEntry[] entries)
+ => new(false) {Mapping = entries.ToList()};
+
+ public static QualityProfileCustomFormatScoreMapping NewMappingWithReset(params FormatMappingEntry[] entries)
+ => new(true) {Mapping = entries.ToList()};
+ }
+}
diff --git a/src/Trash.TestLibrary/Trash.TestLibrary.csproj b/src/Trash.TestLibrary/Trash.TestLibrary.csproj
new file mode 100644
index 00000000..18753431
--- /dev/null
+++ b/src/Trash.TestLibrary/Trash.TestLibrary.csproj
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/Trash.Tests/Radarr/CustomFormat/Processors/GuideProcessorTest.cs b/src/Trash.Tests/Radarr/CustomFormat/Processors/GuideProcessorTest.cs
index 077c542b..8d595adc 100644
--- a/src/Trash.Tests/Radarr/CustomFormat/Processors/GuideProcessorTest.cs
+++ b/src/Trash.Tests/Radarr/CustomFormat/Processors/GuideProcessorTest.cs
@@ -12,6 +12,7 @@ using Trash.Radarr.CustomFormat.Guide;
using Trash.Radarr.CustomFormat.Models;
using Trash.Radarr.CustomFormat.Processors;
using Trash.Radarr.CustomFormat.Processors.GuideSteps;
+using Trash.TestLibrary;
namespace Trash.Tests.Radarr.CustomFormat.Processors
{
@@ -144,30 +145,26 @@ namespace Trash.Tests.Radarr.CustomFormat.Processors
});
guideProcessor.ProfileScores.Should()
- .BeEquivalentTo(new Dictionary>
+ .BeEquivalentTo(new Dictionary
{
{
- "profile1", new List
- {
- new(expectedProcessedCustomFormatData[0], 500),
- new(expectedProcessedCustomFormatData[1], 480)
- }
+ "profile1", CfTestUtils.NewMapping(
+ new FormatMappingEntry(expectedProcessedCustomFormatData[0], 500),
+ new FormatMappingEntry(expectedProcessedCustomFormatData[1], 480))
},
{
- "profile2", new List
- {
- new(expectedProcessedCustomFormatData[0], -1234),
- new(expectedProcessedCustomFormatData[1], -1234),
- new(expectedProcessedCustomFormatData[2], -1234)
- }
+ "profile2", CfTestUtils.NewMapping(
+ new FormatMappingEntry(expectedProcessedCustomFormatData[0], -1234),
+ new FormatMappingEntry(expectedProcessedCustomFormatData[1], -1234),
+ new FormatMappingEntry(expectedProcessedCustomFormatData[2], -1234))
},
{
- "profile4", new List
- {
- new(expectedProcessedCustomFormatData[2], 5678)
- }
+ "profile4", CfTestUtils.NewMapping(
+ new FormatMappingEntry(expectedProcessedCustomFormatData[2], 5678))
}
- }, op => op.Using(new JsonEquivalencyStep()));
+ }, op => op
+ .Using(new JsonEquivalencyStep())
+ .ComparingByMembers());
}
}
}
diff --git a/src/Trash.Tests/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStepTest.cs b/src/Trash.Tests/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStepTest.cs
index d02fc95a..06d9f1b8 100644
--- a/src/Trash.Tests/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStepTest.cs
+++ b/src/Trash.Tests/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStepTest.cs
@@ -5,6 +5,7 @@ using NUnit.Framework;
using Trash.Radarr;
using Trash.Radarr.CustomFormat.Models;
using Trash.Radarr.CustomFormat.Processors.GuideSteps;
+using Trash.TestLibrary;
namespace Trash.Tests.Radarr.CustomFormat.Processors.GuideSteps
{
@@ -58,11 +59,9 @@ namespace Trash.Tests.Radarr.CustomFormat.Processors.GuideSteps
var processor = new QualityProfileStep();
processor.Process(testConfigData);
- processor.ProfileScores.Should().ContainKey("profile1")
- .WhichValue.Should().BeEquivalentTo(new List
- {
- new(testConfigData[0].CustomFormats[0], 50)
- });
+ processor.ProfileScores.Should()
+ .ContainKey("profile1").WhichValue.Should()
+ .BeEquivalentTo(CfTestUtils.NewMapping(new FormatMappingEntry(testConfigData[0].CustomFormats[0], 50)));
processor.CustomFormatsWithoutScore.Should().BeEmpty();
}
@@ -89,13 +88,11 @@ namespace Trash.Tests.Radarr.CustomFormat.Processors.GuideSteps
var processor = new QualityProfileStep();
processor.Process(testConfigData);
- var expectedScoreEntries = new List
- {
- new(testConfigData[0].CustomFormats[0], 100)
- };
+ var expectedScoreEntries =
+ CfTestUtils.NewMapping(new FormatMappingEntry(testConfigData[0].CustomFormats[0], 100));
processor.ProfileScores.Should().BeEquivalentTo(
- new Dictionary>
+ new Dictionary
{
{"profile1", expectedScoreEntries},
{"profile2", expectedScoreEntries}
@@ -125,11 +122,9 @@ namespace Trash.Tests.Radarr.CustomFormat.Processors.GuideSteps
var processor = new QualityProfileStep();
processor.Process(testConfigData);
- processor.ProfileScores.Should().ContainKey("profile1")
- .WhichValue.Should().BeEquivalentTo(new List
- {
- new(testConfigData[0].CustomFormats[0], 0)
- });
+ processor.ProfileScores.Should()
+ .ContainKey("profile1").WhichValue.Should()
+ .BeEquivalentTo(CfTestUtils.NewMapping(new FormatMappingEntry(testConfigData[0].CustomFormats[0], 0)));
processor.CustomFormatsWithoutScore.Should().BeEmpty();
}
diff --git a/src/Trash.Tests/Radarr/CustomFormat/Processors/PersistenceProcessorTest.cs b/src/Trash.Tests/Radarr/CustomFormat/Processors/PersistenceProcessorTest.cs
index f5e00129..3331c75f 100644
--- a/src/Trash.Tests/Radarr/CustomFormat/Processors/PersistenceProcessorTest.cs
+++ b/src/Trash.Tests/Radarr/CustomFormat/Processors/PersistenceProcessorTest.cs
@@ -29,7 +29,7 @@ namespace Trash.Tests.Radarr.CustomFormat.Processors
var guideCfs = Array.Empty();
var deletedCfsInCache = new Collection();
- var profileScores = new Dictionary>();
+ var profileScores = new Dictionary();
var processor = new PersistenceProcessor(cfApi, qpApi, configProvider, () => steps);
processor.PersistCustomFormats(guideCfs, deletedCfsInCache, profileScores);
@@ -49,7 +49,7 @@ namespace Trash.Tests.Radarr.CustomFormat.Processors
var guideCfs = Array.Empty();
var deletedCfsInCache = Array.Empty();
- var profileScores = new Dictionary>();
+ var profileScores = new Dictionary();
var processor = new PersistenceProcessor(cfApi, qpApi, configProvider, () => steps);
processor.PersistCustomFormats(guideCfs, deletedCfsInCache, profileScores);
@@ -68,7 +68,7 @@ namespace Trash.Tests.Radarr.CustomFormat.Processors
var guideCfs = Array.Empty();
var deletedCfsInCache = Array.Empty();
- var profileScores = new Dictionary>();
+ var profileScores = new Dictionary();
var processor = new PersistenceProcessor(cfApi, qpApi, configProvider, () => steps);
diff --git a/src/Trash.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStepTest.cs b/src/Trash.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStepTest.cs
index 411d7652..4e7fa162 100644
--- a/src/Trash.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStepTest.cs
+++ b/src/Trash.Tests/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStepTest.cs
@@ -1,5 +1,4 @@
using System.Collections.Generic;
-using System.Linq;
using FluentAssertions;
using FluentAssertions.Json;
using Newtonsoft.Json;
@@ -11,6 +10,7 @@ using Trash.Radarr.CustomFormat.Api;
using Trash.Radarr.CustomFormat.Models;
using Trash.Radarr.CustomFormat.Models.Cache;
using Trash.Radarr.CustomFormat.Processors.PersistenceSteps;
+using Trash.TestLibrary;
namespace Trash.Tests.Radarr.CustomFormat.Processors.PersistenceSteps
{
@@ -31,7 +31,7 @@ namespace Trash.Tests.Radarr.CustomFormat.Processors.PersistenceSteps
{
'format': 2,
'name': 'cf2',
- 'score': 2
+ 'score': 0
},
{
'format': 3,
@@ -45,16 +45,14 @@ namespace Trash.Tests.Radarr.CustomFormat.Processors.PersistenceSteps
var api = Substitute.For();
api.GetQualityProfiles().Returns(JsonConvert.DeserializeObject>(radarrQualityProfileData));
- var cfScores = new Dictionary>
+ var cfScores = new Dictionary
{
{
- "profile1", new List
- {
- new(new ProcessedCustomFormatData("", "", new JObject())
+ "profile1", CfTestUtils.NewMapping(
+ new FormatMappingEntry(new ProcessedCustomFormatData("", "", new JObject())
{
CacheEntry = new TrashIdMapping("", "") {CustomFormatId = 4}
- }, 100)
- }
+ }, 100))
}
};
@@ -72,19 +70,74 @@ namespace Trash.Tests.Radarr.CustomFormat.Processors.PersistenceSteps
var api = Substitute.For();
api.GetQualityProfiles().Returns(JsonConvert.DeserializeObject>(radarrQualityProfileData));
- var cfScores = new Dictionary>
+ var cfScores = new Dictionary
{
- {"wrong_profile_name", new List()}
+ {"wrong_profile_name", CfTestUtils.NewMapping()}
};
var processor = new QualityProfileApiPersistenceStep();
processor.Process(api, cfScores);
- api.DidNotReceive().UpdateQualityProfile(Arg.Any(), Arg.Any());
- processor.InvalidProfileNames.Should().BeEquivalentTo("wrong_profile_name");
+ processor.InvalidProfileNames.Should().Equal("wrong_profile_name");
processor.UpdatedScores.Should().BeEmpty();
}
+ [Test]
+ public void Reset_scores_for_unmatched_cfs_if_enabled()
+ {
+ const string radarrQualityProfileData = @"[{
+ 'name': 'profile1',
+ 'formatItems': [{
+ 'format': 1,
+ 'name': 'cf1',
+ 'score': 1
+ },
+ {
+ 'format': 2,
+ 'name': 'cf2',
+ 'score': 50
+ },
+ {
+ 'format': 3,
+ 'name': 'cf3',
+ 'score': 3
+ }
+ ],
+ 'id': 1
+}]";
+
+ var api = Substitute.For();
+ api.GetQualityProfiles().Returns(JsonConvert.DeserializeObject>(radarrQualityProfileData));
+
+ var cfScores = new Dictionary
+ {
+ {
+ "profile1", CfTestUtils.NewMappingWithReset(
+ new FormatMappingEntry(new ProcessedCustomFormatData("", "", new JObject())
+ {
+ CacheEntry = new TrashIdMapping("", "", 2)
+ }, 100))
+ }
+ };
+
+ var processor = new QualityProfileApiPersistenceStep();
+ processor.Process(api, cfScores);
+
+ processor.InvalidProfileNames.Should().BeEmpty();
+ processor.UpdatedScores.Should()
+ .ContainKey("profile1").WhichValue.Should()
+ .BeEquivalentTo(new List
+ {
+ new("cf1", 0, FormatScoreUpdateReason.Reset),
+ new("cf2", 100, FormatScoreUpdateReason.Updated),
+ new("cf3", 0, FormatScoreUpdateReason.Reset)
+ });
+
+ api.Received().UpdateQualityProfile(
+ Verify.That(j => j["formatItems"].Children().Should().HaveCount(3)),
+ Arg.Any());
+ }
+
[Test]
public void Scores_are_set_in_quality_profile()
{
@@ -132,27 +185,25 @@ namespace Trash.Tests.Radarr.CustomFormat.Processors.PersistenceSteps
var api = Substitute.For();
api.GetQualityProfiles().Returns(JsonConvert.DeserializeObject>(radarrQualityProfileData));
- var cfScores = new Dictionary>
+ var cfScores = new Dictionary
{
{
- "profile1", new List
- {
- new(new ProcessedCustomFormatData("", "", new JObject())
+ "profile1", CfTestUtils.NewMapping(
+ new FormatMappingEntry(new ProcessedCustomFormatData("", "", new JObject())
{
// First match by ID
- CacheEntry = new TrashIdMapping("", "") {CustomFormatId = 4}
+ CacheEntry = new TrashIdMapping("", "", 4)
}, 100),
- new(new ProcessedCustomFormatData("", "", new JObject())
+ new FormatMappingEntry(new ProcessedCustomFormatData("", "", new JObject())
{
// Should NOT match because we do not use names to assign scores
CacheEntry = new TrashIdMapping("", "BR-DISK")
}, 101),
- new(new ProcessedCustomFormatData("", "", new JObject())
+ new FormatMappingEntry(new ProcessedCustomFormatData("", "", new JObject())
{
// Second match by ID
- CacheEntry = new TrashIdMapping("", "") {CustomFormatId = 1}
- }, 102)
- }
+ CacheEntry = new TrashIdMapping("", "", 1)
+ }, 102))
}
};
@@ -203,9 +254,13 @@ namespace Trash.Tests.Radarr.CustomFormat.Processors.PersistenceSteps
api.Received()
.UpdateQualityProfile(Verify.That(a => a.Should().BeEquivalentTo(expectedProfileJson)), 1);
processor.InvalidProfileNames.Should().BeEmpty();
- processor.UpdatedScores.Should().ContainKey("profile1").WhichValue.Should().BeEquivalentTo(
- cfScores.Values.First()[0],
- cfScores.Values.First()[2]);
+ processor.UpdatedScores.Should()
+ .ContainKey("profile1").WhichValue.Should()
+ .BeEquivalentTo(new List
+ {
+ new("3D", 100, FormatScoreUpdateReason.Updated),
+ new("asdf2", 102, FormatScoreUpdateReason.Updated)
+ });
}
}
}
diff --git a/src/Trash.Tests/Trash.Tests.csproj b/src/Trash.Tests/Trash.Tests.csproj
index 15dfca36..9f984b62 100644
--- a/src/Trash.Tests/Trash.Tests.csproj
+++ b/src/Trash.Tests/Trash.Tests.csproj
@@ -5,6 +5,7 @@
+
diff --git a/src/Trash/Extensions/DictionaryExtensions.cs b/src/Trash/Extensions/DictionaryExtensions.cs
index 918992f5..a42a1193 100644
--- a/src/Trash/Extensions/DictionaryExtensions.cs
+++ b/src/Trash/Extensions/DictionaryExtensions.cs
@@ -16,16 +16,9 @@ namespace Trash.Extensions
return val;
}
- public static TValue GetOrDefault(this IDictionary dict, TKey key)
- where TValue : struct
+ public static TValue? GetOrDefault(this IDictionary dict, TKey key)
{
- if (!dict.TryGetValue(key, out var val))
- {
- val = default;
- dict.Add(key, val);
- }
-
- return val;
+ return dict.TryGetValue(key, out var val) ? val : default;
}
}
}
diff --git a/src/Trash/Radarr/CustomFormat/CustomFormatUpdater.cs b/src/Trash/Radarr/CustomFormat/CustomFormatUpdater.cs
index afacf05f..318abf1e 100644
--- a/src/Trash/Radarr/CustomFormat/CustomFormatUpdater.cs
+++ b/src/Trash/Radarr/CustomFormat/CustomFormatUpdater.cs
@@ -69,9 +69,9 @@ namespace Trash.Radarr.CustomFormat
{
Log.Debug("> Scores updated for quality profile: {ProfileName}", profileName);
- foreach (var score in scores)
+ foreach (var (customFormatName, score, reason) in scores)
{
- Log.Debug(" - {Format}: {Score}", score.CustomFormat.Name, score.Score);
+ Log.Debug(" - {Format}: {Score} ({Reason})", customFormatName, score, reason);
}
}
@@ -249,23 +249,23 @@ namespace Trash.Radarr.CustomFormat
Console.WriteLine(profileFormat, "Profile", "Custom Format", "Score");
Console.WriteLine(string.Concat(Enumerable.Repeat('-', 2 + 18 + 20 + 8)));
- foreach (var (profileName, scoreEntries) in _guideProcessor.ProfileScores)
+ foreach (var (profileName, scoreMap) in _guideProcessor.ProfileScores)
{
Console.WriteLine(profileFormat, profileName, "", "");
- foreach (var scoreEntry in scoreEntries)
+ foreach (var (customFormat, score) in scoreMap.Mapping)
{
var matchingCf = _guideProcessor.ProcessedCustomFormats
- .FirstOrDefault(cf => cf.TrashId.EqualsIgnoreCase(scoreEntry.CustomFormat.TrashId));
+ .FirstOrDefault(cf => cf.TrashId.EqualsIgnoreCase(customFormat.TrashId));
if (matchingCf == null)
{
Log.Warning("Quality Profile refers to CF not found in guide: {TrashId}",
- scoreEntry.CustomFormat.TrashId);
+ customFormat.TrashId);
continue;
}
- Console.WriteLine(profileFormat, "", matchingCf.Name, scoreEntry.Score);
+ Console.WriteLine(profileFormat, "", matchingCf.Name, score);
}
}
diff --git a/src/Trash/Radarr/CustomFormat/Models/QualityProfileCustomFormatScoreEntry.cs b/src/Trash/Radarr/CustomFormat/Models/QualityProfileCustomFormatScoreEntry.cs
deleted file mode 100644
index 48ce86ca..00000000
--- a/src/Trash/Radarr/CustomFormat/Models/QualityProfileCustomFormatScoreEntry.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-namespace Trash.Radarr.CustomFormat.Models
-{
- public class QualityProfileCustomFormatScoreEntry
- {
- public QualityProfileCustomFormatScoreEntry(ProcessedCustomFormatData customFormat, int score)
- {
- CustomFormat = customFormat;
- Score = score;
- }
-
- public ProcessedCustomFormatData CustomFormat { get; }
- public int Score { get; }
- }
-}
diff --git a/src/Trash/Radarr/CustomFormat/Models/QualityProfileCustomFormatScoreMapping.cs b/src/Trash/Radarr/CustomFormat/Models/QualityProfileCustomFormatScoreMapping.cs
new file mode 100644
index 00000000..08c0f869
--- /dev/null
+++ b/src/Trash/Radarr/CustomFormat/Models/QualityProfileCustomFormatScoreMapping.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+
+namespace Trash.Radarr.CustomFormat.Models
+{
+ public record FormatMappingEntry(ProcessedCustomFormatData CustomFormat, int Score);
+
+ public class QualityProfileCustomFormatScoreMapping
+ {
+ public QualityProfileCustomFormatScoreMapping(bool resetUnmatchedScores)
+ {
+ ResetUnmatchedScores = resetUnmatchedScores;
+ }
+
+ public bool ResetUnmatchedScores { get; }
+ public List Mapping { get; init; } = new();
+ }
+}
diff --git a/src/Trash/Radarr/CustomFormat/Models/UpdatedFormatScore.cs b/src/Trash/Radarr/CustomFormat/Models/UpdatedFormatScore.cs
new file mode 100644
index 00000000..9e850584
--- /dev/null
+++ b/src/Trash/Radarr/CustomFormat/Models/UpdatedFormatScore.cs
@@ -0,0 +1,13 @@
+namespace Trash.Radarr.CustomFormat.Models
+{
+ public enum FormatScoreUpdateReason
+ {
+ Updated,
+ Reset
+ }
+
+ public record UpdatedFormatScore(
+ string CustomFormatName,
+ int Score,
+ FormatScoreUpdateReason Reason);
+}
diff --git a/src/Trash/Radarr/CustomFormat/Processors/GuideProcessor.cs b/src/Trash/Radarr/CustomFormat/Processors/GuideProcessor.cs
index 24e087a4..16046f0f 100644
--- a/src/Trash/Radarr/CustomFormat/Processors/GuideProcessor.cs
+++ b/src/Trash/Radarr/CustomFormat/Processors/GuideProcessor.cs
@@ -43,7 +43,7 @@ namespace Trash.Radarr.CustomFormat.Processors
public IReadOnlyCollection ConfigData
=> _steps.Config.ConfigData;
- public IDictionary> ProfileScores
+ public IDictionary ProfileScores
=> _steps.QualityProfile.ProfileScores;
public IReadOnlyCollection<(string name, string trashId, string profileName)> CustomFormatsWithoutScore
diff --git a/src/Trash/Radarr/CustomFormat/Processors/GuideSteps/IQualityProfileStep.cs b/src/Trash/Radarr/CustomFormat/Processors/GuideSteps/IQualityProfileStep.cs
index f974b923..c9a25120 100644
--- a/src/Trash/Radarr/CustomFormat/Processors/GuideSteps/IQualityProfileStep.cs
+++ b/src/Trash/Radarr/CustomFormat/Processors/GuideSteps/IQualityProfileStep.cs
@@ -5,7 +5,7 @@ namespace Trash.Radarr.CustomFormat.Processors.GuideSteps
{
public interface IQualityProfileStep
{
- Dictionary> ProfileScores { get; }
+ Dictionary ProfileScores { get; }
List<(string name, string trashId, string profileName)> CustomFormatsWithoutScore { get; }
void Process(IEnumerable configData);
}
diff --git a/src/Trash/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStep.cs b/src/Trash/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStep.cs
index 46b35ba3..81a9cc57 100644
--- a/src/Trash/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStep.cs
+++ b/src/Trash/Radarr/CustomFormat/Processors/GuideSteps/QualityProfileStep.cs
@@ -1,12 +1,11 @@
using System.Collections.Generic;
-using Trash.Extensions;
using Trash.Radarr.CustomFormat.Models;
namespace Trash.Radarr.CustomFormat.Processors.GuideSteps
{
public class QualityProfileStep : IQualityProfileStep
{
- public Dictionary> ProfileScores { get; } = new();
+ public Dictionary ProfileScores { get; } = new();
public List<(string name, string trashId, string profileName)> CustomFormatsWithoutScore { get; } = new();
public void Process(IEnumerable configData)
@@ -18,7 +17,7 @@ namespace Trash.Radarr.CustomFormat.Processors.GuideSteps
// Check if there is a score we can use. Priority is:
// 1. Score from the YAML config is used. If user did not provide,
// 2. Score from the guide is used. If the guide did not have one,
- // 3. Warn the user and skip it.
+ // 3. Warn the user and
var scoreToUse = profile.Score;
if (scoreToUse == null)
{
@@ -32,11 +31,18 @@ namespace Trash.Radarr.CustomFormat.Processors.GuideSteps
}
}
- if (scoreToUse != null)
+ if (scoreToUse == null)
{
- ProfileScores.GetOrCreate(profile.Name)
- .Add(new QualityProfileCustomFormatScoreEntry(cf, scoreToUse.Value));
+ continue;
}
+
+ if (!ProfileScores.TryGetValue(profile.Name, out var mapping))
+ {
+ mapping = new QualityProfileCustomFormatScoreMapping(profile.ResetUnmatchedScores);
+ ProfileScores[profile.Name] = mapping;
+ }
+
+ mapping.Mapping.Add(new FormatMappingEntry(cf, scoreToUse.Value));
}
}
}
diff --git a/src/Trash/Radarr/CustomFormat/Processors/IGuideProcessor.cs b/src/Trash/Radarr/CustomFormat/Processors/IGuideProcessor.cs
index c8d1da08..9c2ddf72 100644
--- a/src/Trash/Radarr/CustomFormat/Processors/IGuideProcessor.cs
+++ b/src/Trash/Radarr/CustomFormat/Processors/IGuideProcessor.cs
@@ -10,7 +10,7 @@ namespace Trash.Radarr.CustomFormat.Processors
IReadOnlyCollection ProcessedCustomFormats { get; }
IReadOnlyCollection CustomFormatsNotInGuide { get; }
IReadOnlyCollection ConfigData { get; }
- IDictionary> ProfileScores { get; }
+ IDictionary ProfileScores { get; }
IReadOnlyCollection<(string name, string trashId, string profileName)> CustomFormatsWithoutScore { get; }
IReadOnlyCollection DeletedCustomFormatsInCache { get; }
List<(string, string)> CustomFormatsWithOutdatedNames { get; }
diff --git a/src/Trash/Radarr/CustomFormat/Processors/IPersistenceProcessor.cs b/src/Trash/Radarr/CustomFormat/Processors/IPersistenceProcessor.cs
index d21cf70a..ef0abcb3 100644
--- a/src/Trash/Radarr/CustomFormat/Processors/IPersistenceProcessor.cs
+++ b/src/Trash/Radarr/CustomFormat/Processors/IPersistenceProcessor.cs
@@ -8,13 +8,13 @@ namespace Trash.Radarr.CustomFormat.Processors
{
public interface IPersistenceProcessor
{
- IDictionary> UpdatedScores { get; }
+ IDictionary> UpdatedScores { get; }
IReadOnlyCollection InvalidProfileNames { get; }
CustomFormatTransactionData Transactions { get; }
Task PersistCustomFormats(IReadOnlyCollection guideCfs,
IEnumerable deletedCfsInCache,
- IDictionary> profileScores);
+ IDictionary profileScores);
void Reset();
}
diff --git a/src/Trash/Radarr/CustomFormat/Processors/PersistenceProcessor.cs b/src/Trash/Radarr/CustomFormat/Processors/PersistenceProcessor.cs
index 8a92c1df..caea4441 100644
--- a/src/Trash/Radarr/CustomFormat/Processors/PersistenceProcessor.cs
+++ b/src/Trash/Radarr/CustomFormat/Processors/PersistenceProcessor.cs
@@ -40,7 +40,7 @@ namespace Trash.Radarr.CustomFormat.Processors
public CustomFormatTransactionData Transactions
=> _steps.JsonTransactionStep.Transactions;
- public IDictionary> UpdatedScores
+ public IDictionary> UpdatedScores
=> _steps.ProfileQualityProfileApiPersister.UpdatedScores;
public IReadOnlyCollection InvalidProfileNames
@@ -53,7 +53,7 @@ namespace Trash.Radarr.CustomFormat.Processors
public async Task PersistCustomFormats(IReadOnlyCollection guideCfs,
IEnumerable deletedCfsInCache,
- IDictionary> profileScores)
+ IDictionary profileScores)
{
var radarrCfs = await _customFormatService.GetCustomFormats();
diff --git a/src/Trash/Radarr/CustomFormat/Processors/PersistenceSteps/IQualityProfileApiPersistenceStep.cs b/src/Trash/Radarr/CustomFormat/Processors/PersistenceSteps/IQualityProfileApiPersistenceStep.cs
index 40f238ac..0961fb0c 100644
--- a/src/Trash/Radarr/CustomFormat/Processors/PersistenceSteps/IQualityProfileApiPersistenceStep.cs
+++ b/src/Trash/Radarr/CustomFormat/Processors/PersistenceSteps/IQualityProfileApiPersistenceStep.cs
@@ -7,10 +7,10 @@ namespace Trash.Radarr.CustomFormat.Processors.PersistenceSteps
{
public interface IQualityProfileApiPersistenceStep
{
- IDictionary> UpdatedScores { get; }
+ IDictionary> UpdatedScores { get; }
IReadOnlyCollection InvalidProfileNames { get; }
Task Process(IQualityProfileService api,
- IDictionary> cfScores);
+ IDictionary cfScores);
}
}
diff --git a/src/Trash/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStep.cs b/src/Trash/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStep.cs
index d5ac8576..fe0110cc 100644
--- a/src/Trash/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStep.cs
+++ b/src/Trash/Radarr/CustomFormat/Processors/PersistenceSteps/QualityProfileApiPersistenceStep.cs
@@ -12,45 +12,60 @@ namespace Trash.Radarr.CustomFormat.Processors.PersistenceSteps
public class QualityProfileApiPersistenceStep : IQualityProfileApiPersistenceStep
{
private readonly List _invalidProfileNames = new();
- private readonly Dictionary> _updatedScores = new();
+ private readonly Dictionary> _updatedScores = new();
- public IDictionary> UpdatedScores => _updatedScores;
+ public IDictionary> UpdatedScores => _updatedScores;
public IReadOnlyCollection InvalidProfileNames => _invalidProfileNames;
public async Task Process(IQualityProfileService api,
- IDictionary> cfScores)
+ IDictionary cfScores)
{
- var radarrProfiles = (await api.GetQualityProfiles())
- .Select(p => (Name: p["name"].ToString(), Json: p));
+ var radarrProfiles = await api.GetQualityProfiles();
- var profileScores = cfScores
- .GroupJoin(radarrProfiles,
- s => s.Key,
- p => p.Name,
- (s, pList) => (s.Key, s.Value,
- pList.SelectMany(p => p.Json["formatItems"].Children()).ToList()),
- StringComparer.InvariantCultureIgnoreCase);
+ // Match quality profiles in Radarr to ones the user put in their config.
+ // For each match, we return a tuple including the list of custom format scores ("formatItems").
+ // Using GroupJoin() because we want a LEFT OUTER JOIN so we can list which quality profiles in config
+ // do not match profiles in Radarr.
+ var profileScores = cfScores.GroupJoin(radarrProfiles,
+ s => s.Key,
+ p => (string) p["name"],
+ (s, p) => (s.Key, s.Value, p.SelectMany(pi => pi["formatItems"].Children()).ToList()),
+ StringComparer.InvariantCultureIgnoreCase);
- foreach (var (profileName, scoreList, jsonList) in profileScores)
+ foreach (var (profileName, scoreMap, formatItems) in profileScores)
{
- if (jsonList.Count == 0)
+ if (formatItems.Count == 0)
{
_invalidProfileNames.Add(profileName);
continue;
}
- foreach (var (score, json) in scoreList
- .Select(s => (s, FindJsonScoreEntry(s, jsonList)))
- .Where(p => p.Item2 != null))
+ foreach (var json in formatItems)
{
- var currentScore = (int) json!["score"];
- if (currentScore == score.Score)
+ var map = FindScoreEntry(json, scoreMap);
+
+ int? scoreToUse = null;
+ FormatScoreUpdateReason? reason = null;
+
+ if (map != null)
+ {
+ scoreToUse = map.Score;
+ reason = FormatScoreUpdateReason.Updated;
+ }
+ else if (scoreMap.ResetUnmatchedScores)
+ {
+ scoreToUse = 0;
+ reason = FormatScoreUpdateReason.Reset;
+ }
+
+ if (scoreToUse == null || reason == null || (int) json["score"] == scoreToUse)
{
continue;
}
- json!["score"] = score.Score;
- _updatedScores.GetOrCreate(profileName).Add(score);
+ json!["score"] = scoreToUse.Value;
+ _updatedScores.GetOrCreate(profileName)
+ .Add(new UpdatedFormatScore((string) json["name"], scoreToUse.Value, reason.Value));
}
if (!_updatedScores.TryGetValue(profileName, out var updatedScores) || updatedScores.Count == 0)
@@ -59,17 +74,17 @@ namespace Trash.Radarr.CustomFormat.Processors.PersistenceSteps
continue;
}
- var jsonRoot = (JObject) jsonList.First().Root;
+ var jsonRoot = (JObject) formatItems.First().Root;
await api.UpdateQualityProfile(jsonRoot, (int) jsonRoot["id"]);
}
}
- private static JObject? FindJsonScoreEntry(QualityProfileCustomFormatScoreEntry score,
- IEnumerable jsonList)
+ private static FormatMappingEntry? FindScoreEntry(JObject formatItem,
+ QualityProfileCustomFormatScoreMapping scoreMap)
{
- return jsonList.FirstOrDefault(j
- => score.CustomFormat.CacheEntry != null &&
- (int) j["format"] == score.CustomFormat.CacheEntry.CustomFormatId);
+ return scoreMap.Mapping.FirstOrDefault(
+ m => m.CustomFormat.CacheEntry != null &&
+ (int) formatItem["format"] == m.CustomFormat.CacheEntry.CustomFormatId);
}
}
}
diff --git a/src/Trash/Radarr/RadarrConfiguration.cs b/src/Trash/Radarr/RadarrConfiguration.cs
index dac72636..75958c76 100644
--- a/src/Trash/Radarr/RadarrConfiguration.cs
+++ b/src/Trash/Radarr/RadarrConfiguration.cs
@@ -50,6 +50,7 @@ namespace Trash.Radarr
public string Name { get; init; } = "";
public int? Score { get; init; }
+ public bool ResetUnmatchedScores { get; init; }
}
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
diff --git a/src/Trash/trash-config-template.yml b/src/Trash/trash-config-template.yml
index 2c56732f..376d88c5 100644
--- a/src/Trash/trash-config-template.yml
+++ b/src/Trash/trash-config-template.yml
@@ -54,3 +54,4 @@ radarr:
# - name: Quality Profile 1
# - name: Quality Profile 2
# #score: -9999 # Optional score to assign to all CFs. Overrides scores in the guide.
+# #reset_unmatched_scores: true # Optionally set other scores to 0 if they are not listed in 'names' above.
diff --git a/src/TrashUpdater.sln b/src/TrashUpdater.sln
index 7001833a..ca9b80fa 100644
--- a/src/TrashUpdater.sln
+++ b/src/TrashUpdater.sln
@@ -19,6 +19,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csp
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Tests", "Common.Tests\Common.Tests.csproj", "{0720939D-1CA6-43D7-BBED-F8F894C4F562}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Trash.TestLibrary", "Trash.TestLibrary\Trash.TestLibrary.csproj", "{33226068-65E3-4890-8671-59A56BA3F6F0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -49,6 +51,10 @@ Global
{0720939D-1CA6-43D7-BBED-F8F894C4F562}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0720939D-1CA6-43D7-BBED-F8F894C4F562}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0720939D-1CA6-43D7-BBED-F8F894C4F562}.Release|Any CPU.Build.0 = Release|Any CPU
+ {33226068-65E3-4890-8671-59A56BA3F6F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {33226068-65E3-4890-8671-59A56BA3F6F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {33226068-65E3-4890-8671-59A56BA3F6F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {33226068-65E3-4890-8671-59A56BA3F6F0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
EndGlobalSection
diff --git a/wiki/Configuration-Examples.md b/wiki/Configuration-Examples.md
index 16c9cfcf..76289b40 100644
--- a/wiki/Configuration-Examples.md
+++ b/wiki/Configuration-Examples.md
@@ -7,6 +7,7 @@ Various scenarios supported using flexible configuration structure:
- [Manually assign different scores to multiple custom formats](#manually-assign-different-scores-to-multiple-custom-formats)
- [Assign custom format scores the same way to multiple quality profiles](#assign-custom-format-scores-the-same-way-to-multiple-quality-profiles)
- [Resolving ambiguity between custom formats with the same name](#resolving-ambiguity-between-custom-formats-with-the-same-name)
+- [Scores in a quality profile should be set to zero if it wasn't listed in config](#scores-in-a-quality-profile-should-be-set-to-zero-if-it-wasnt-listed-in-config)
## Update as much as possible in both Sonarr and Radarr with a single config
@@ -120,10 +121,8 @@ update at the same time. There's an example of how to do that in a different sec
## Synchronize a lot of custom formats for a single quality profile
-Scenario:
-
-"I want to be able to synchronize a list of custom formats to Radarr. In addition, I want the scores
-in the guide to be applied to a single quality profile."
+Scenario: *"I want to be able to synchronize a list of custom formats to Radarr. In addition, I want
+the scores in the guide to be applied to a single quality profile."*
Solution:
@@ -155,11 +154,9 @@ radarr:
## Manually assign different scores to multiple custom formats
-Scenario:
-
-"I want to synchronize custom formats to Radarr. I also do not want to use the scores in
+Scenario: *"I want to synchronize custom formats to Radarr. I also do not want to use the scores in
the guide. Instead, I want to assign my own distinct score to each custom format in a single quality
-profile."
+profile."*
Solution:
@@ -279,3 +276,42 @@ radarr:
Where do you get the Trash ID? That's from the `"trash_id"` property of the actual JSON for the
custom format in the guide.
+
+## Scores in a quality profile should be set to zero if it wasn't listed in config
+
+Scenario: *"I completely rely on Trash Updater to set scores on my quality profiles. I never plan to
+manually set scores on those profiles. If I alter which custom format scores get assigned to a
+quality profile, the old scores should be set back to 0 automatically for me."*
+
+```yml
+radarr:
+ - base_url: http://localhost:7878
+ api_key: 87674e2c316645ed85696a91a3d41988
+
+ custom_formats:
+ - names:
+ - DTS X
+ - TrueHD
+ quality_profiles:
+ - name: SD
+ reset_unmatched_scores: true
+ - name: Ultra-HD
+```
+
+Let's say you have three custom formats added to Radarr: "DTS X", "TrueHD", and "DoVi". Since only
+the first two are listed in the `names` array, what happens to "DoVi"? Since two quality profiles
+are specified above, each with a different setting for `reset_unmatched_scores`, the behavior will
+be different:
+
+- The `SD` profile will always have the score for "DoVi" set to zero (`0`).
+- The `Ultra-HD` profile's score for "DoVi" will never be altered.
+
+The `reset_unmatched_scores` setting basically determines how scores are handled for custom formats
+that exist in Radarr but are not in the list of `names` in config. As shown in the example above,
+you set it to `true` which results in unmatched scores being set to `0`, or you can set it to
+`false` (or leave it omitted) in which case Trash Updater will not alter the value.
+
+Which one should you use? That depends on how much control you want Trash Updater to have. If you
+use Trash Updater to supplement manual changes to your profiles, you probably want it set to `false`
+so it doesn't clobber your manual edits. Otherwise, set it to `true` so that scores aren't left over
+when you add/remove custom formats from a profile.
diff --git a/wiki/Configuration-Reference.md b/wiki/Configuration-Reference.md
index 8f994699..57b65174 100644
--- a/wiki/Configuration-Reference.md
+++ b/wiki/Configuration-Reference.md
@@ -284,3 +284,9 @@ Synchronization]] page.
A positive or negative number representing the score to apply to *all* custom formats listed
in the `names` list. A score of `0` is also acceptable, which effectively disables the custom
formats without having to delete them.
+
+ - `reset_unmatched_scores` (Optional; *Default: `false`*)
+ If set to `true`, enables setting scores to `0` in quality profiles where either a name was
+ not mentioned in the `names` array *or* it was in that list but did not get a score (e.g. no
+ score in guide). If `false`, scores are never altered unless it is listed in the `names` array
+ *and* has a valid score to assign.