diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js index 08d424cf0..88870aacd 100644 --- a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js @@ -125,6 +125,7 @@ class EditQualityProfileModalContent extends Component { upgradeAllowed, cutoff, minFormatScore, + minUpgradeFormatScore, cutoffFormatScore, language, items, @@ -249,6 +250,25 @@ class EditQualityProfileModalContent extends Component { } + { + upgradeAllowed.value && formatItems.value.length > 0 ? + + + {translate('MinimumCustomFormatScoreIncrement')} + + + + : + null + } + {translate('Language')} diff --git a/frontend/src/typings/QualityProfile.ts b/frontend/src/typings/QualityProfile.ts index ec4e46648..41063cb3e 100644 --- a/frontend/src/typings/QualityProfile.ts +++ b/frontend/src/typings/QualityProfile.ts @@ -16,6 +16,7 @@ interface QualityProfile { items: QualityProfileQualityItem[]; minFormatScore: number; cutoffFormatScore: number; + minUpgradeFormatScore: number; formatItems: QualityProfileFormatItem[]; id: number; } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs index d1f90577e..a3116aaf9 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs @@ -6,6 +6,7 @@ using NzbDrone.Core.CustomFormats; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Parser; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.CustomFormats; @@ -160,5 +161,95 @@ namespace NzbDrone.Core.Test.DecisionEngineTests new List()) .Should().Be(UpgradeableRejectReason.QualityCutoff); } + + [Test] + public void should_return_false_if_minimum_custom_score_is_not_met() + { + var customFormatOne = new CustomFormat + { + Id = 1, + Name = "One" + }; + + var customFormatTwo = new CustomFormat + { + Id = 2, + Name = "Two" + }; + + var profile = new QualityProfile + { + Items = Qualities.QualityFixture.GetDefaultQualities(), + UpgradeAllowed = true, + MinUpgradeFormatScore = 11, + CutoffFormatScore = 100, + FormatItems = new List + { + new ProfileFormatItem + { + Format = customFormatOne, + Score = 10 + }, + new ProfileFormatItem + { + Format = customFormatTwo, + Score = 20 + } + } + }; + + Subject.IsUpgradable( + profile, + new QualityModel(Quality.DVD), + new List { customFormatOne }, + new QualityModel(Quality.DVD), + new List { customFormatTwo }) + .Should().Be(UpgradeableRejectReason.MinCustomFormatScore); + } + + [Test] + public void should_return_true_if_minimum_custom_score_is_met() + { + var customFormatOne = new CustomFormat + { + Id = 1, + Name = "One" + }; + + var customFormatTwo = new CustomFormat + { + Id = 2, + Name = "Two" + }; + + var profile = new QualityProfile + { + Items = Qualities.QualityFixture.GetDefaultQualities(), + UpgradeAllowed = true, + MinUpgradeFormatScore = 10, + CutoffFormatScore = 100, + FormatItems = new List + { + new ProfileFormatItem + { + Format = customFormatOne, + Score = 10 + }, + new ProfileFormatItem + { + Format = customFormatTwo, + Score = 20 + } + } + }; + + Subject.IsUpgradable( + profile, + new QualityModel(Quality.DVD), + new List { customFormatOne }, + new QualityModel(Quality.DVD), + new List { customFormatTwo }) + .Should().Be(UpgradeableRejectReason.None); + } } } diff --git a/src/NzbDrone.Core/Datastore/Migration/239_add_minimum_upgrade_format_score_to_quality_profiles.cs b/src/NzbDrone.Core/Datastore/Migration/239_add_minimum_upgrade_format_score_to_quality_profiles.cs new file mode 100644 index 000000000..6c5e197fa --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/239_add_minimum_upgrade_format_score_to_quality_profiles.cs @@ -0,0 +1,14 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(239)] + public class add_minimum_upgrade_format_score_to_quality_profiles : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Alter.Table("QualityProfiles").AddColumn("MinUpgradeFormatScore").AsInt32().WithDefaultValue(1); + } + } +} diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs index af62c7f21..7ce091bbb 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs @@ -95,6 +95,17 @@ namespace NzbDrone.Core.DecisionEngine.Specifications return UpgradeableRejectReason.CustomFormatCutoff; } + if (newFormatScore < currentFormatScore + qualityProfile.MinUpgradeFormatScore) + { + _logger.Debug("New item's custom formats [{0}] ({1}) do not meet minimum custom format score increment of {3} required for upgrade, skipping. Existing: [{4}] ({5}).", + newCustomFormats.ConcatToString(), + newFormatScore, + qualityProfile.MinUpgradeFormatScore, + currentCustomFormats.ConcatToString(), + currentFormatScore); + return UpgradeableRejectReason.MinCustomFormatScore; + } + _logger.Debug("New item's custom formats [{0}] ({1}) improve on [{2}] ({3}), accepting", newCustomFormats.ConcatToString(), newFormatScore, diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs index e6a159dbc..3056f768c 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs @@ -78,6 +78,9 @@ namespace NzbDrone.Core.DecisionEngine.Specifications case UpgradeableRejectReason.CustomFormatScore: return Decision.Reject("Existing file on disk has a equal or higher custom format score: {0}", qualityProfile.CalculateCustomFormatScore(customFormats)); + + case UpgradeableRejectReason.MinCustomFormatScore: + return Decision.Reject("Existing file differential between new release does not meet minimum Custom Format score increment: {0}", qualityProfile.MinFormatScore); } return Decision.Accept(); diff --git a/src/NzbDrone.Core/DecisionEngine/UpgradeableRejectReason.cs b/src/NzbDrone.Core/DecisionEngine/UpgradeableRejectReason.cs index 7ed6d6a0f..2b1b1cfe9 100644 --- a/src/NzbDrone.Core/DecisionEngine/UpgradeableRejectReason.cs +++ b/src/NzbDrone.Core/DecisionEngine/UpgradeableRejectReason.cs @@ -7,6 +7,7 @@ namespace NzbDrone.Core.DecisionEngine BetterRevision, QualityCutoff, CustomFormatScore, - CustomFormatCutoff + CustomFormatCutoff, + MinCustomFormatScore } } diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupQualityProfileFormatItems.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupQualityProfileFormatItems.cs index 3c8cef581..a08f52aa7 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupQualityProfileFormatItems.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupQualityProfileFormatItems.cs @@ -65,6 +65,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { profile.MinFormatScore = 0; profile.CutoffFormatScore = 0; + profile.MinUpgradeFormatScore = 1; } updatedProfiles.Add(profile); @@ -73,7 +74,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers if (updatedProfiles.Any()) { - _repository.SetFields(updatedProfiles, p => p.FormatItems, p => p.MinFormatScore, p => p.CutoffFormatScore); + _repository.SetFields(updatedProfiles, p => p.FormatItems, p => p.MinFormatScore, p => p.CutoffFormatScore, p => p.MinUpgradeFormatScore); } } } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index c7054ab8e..df13e3e01 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -910,6 +910,8 @@ "MinimumAvailability": "Minimum Availability", "MinimumCustomFormatScore": "Minimum Custom Format Score", "MinimumCustomFormatScoreHelpText": "Minimum custom format score allowed to download", + "MinimumCustomFormatScoreIncrement": "Minimum Custom Format Score Increment", + "MinimumCustomFormatScoreIncrementHelpText": "Minimum required improvement of the custom format score between existing and new releases before {appName} considers it an upgrade", "MinimumFreeSpace": "Minimum Free Space", "MinimumFreeSpaceHelpText": "Prevent import if it would leave less than this amount of disk space available", "MinimumLimits": "Minimum Limits", diff --git a/src/NzbDrone.Core/Profiles/Qualities/QualityProfile.cs b/src/NzbDrone.Core/Profiles/Qualities/QualityProfile.cs index 98d1629ad..c4447e004 100644 --- a/src/NzbDrone.Core/Profiles/Qualities/QualityProfile.cs +++ b/src/NzbDrone.Core/Profiles/Qualities/QualityProfile.cs @@ -19,6 +19,7 @@ namespace NzbDrone.Core.Profiles.Qualities public List Items { get; set; } public int MinFormatScore { get; set; } public int CutoffFormatScore { get; set; } + public int MinUpgradeFormatScore { get; set; } public List FormatItems { get; set; } public Language Language { get; set; } public bool UpgradeAllowed { get; set; } diff --git a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs index 6eee18938..bb65471b2 100644 --- a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs +++ b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs @@ -112,6 +112,7 @@ namespace NzbDrone.Core.Profiles.Qualities { profile.MinFormatScore = 0; profile.CutoffFormatScore = 0; + profile.MinUpgradeFormatScore = 1; } Update(profile); @@ -262,6 +263,7 @@ namespace NzbDrone.Core.Profiles.Qualities Language = Language.English, MinFormatScore = 0, CutoffFormatScore = 0, + MinUpgradeFormatScore = 1, FormatItems = formatItems }; diff --git a/src/Radarr.Api.V3/Profiles/Quality/QualityProfileController.cs b/src/Radarr.Api.V3/Profiles/Quality/QualityProfileController.cs index 5b924849c..630691245 100644 --- a/src/Radarr.Api.V3/Profiles/Quality/QualityProfileController.cs +++ b/src/Radarr.Api.V3/Profiles/Quality/QualityProfileController.cs @@ -15,22 +15,22 @@ namespace Radarr.Api.V3.Profiles.Quality public class QualityProfileController : RestController { private readonly IQualityProfileService _qualityProfileService; - private readonly ICustomFormatService _formatService; public QualityProfileController(IQualityProfileService qualityProfileService, ICustomFormatService formatService) { _qualityProfileService = qualityProfileService; - _formatService = formatService; + SharedValidator.RuleFor(c => c.Name).NotEmpty(); // TODO: Need to validate the cutoff is allowed and the ID/quality ID exists // TODO: Need to validate the Items to ensure groups have names and at no item has no name, no items and no quality + SharedValidator.RuleFor(c => c.MinUpgradeFormatScore).GreaterThanOrEqualTo(1); SharedValidator.RuleFor(c => c.Cutoff).ValidCutoff(); SharedValidator.RuleFor(c => c.Items).ValidItems(); SharedValidator.RuleFor(c => c.FormatItems).Must(items => { - var all = _formatService.All().Select(f => f.Id).ToList(); + var all = formatService.All().Select(f => f.Id).ToList(); var ids = items.Select(i => i.Format); return all.Except(ids).Empty(); diff --git a/src/Radarr.Api.V3/Profiles/Quality/QualityProfileResource.cs b/src/Radarr.Api.V3/Profiles/Quality/QualityProfileResource.cs index 5bcfda0b9..75c13b1be 100644 --- a/src/Radarr.Api.V3/Profiles/Quality/QualityProfileResource.cs +++ b/src/Radarr.Api.V3/Profiles/Quality/QualityProfileResource.cs @@ -16,6 +16,7 @@ namespace Radarr.Api.V3.Profiles.Quality public List Items { get; set; } public int MinFormatScore { get; set; } public int CutoffFormatScore { get; set; } + public int MinUpgradeFormatScore { get; set; } public List FormatItems { get; set; } public Language Language { get; set; } } @@ -58,6 +59,7 @@ namespace Radarr.Api.V3.Profiles.Quality Items = model.Items.ConvertAll(ToResource), MinFormatScore = model.MinFormatScore, CutoffFormatScore = model.CutoffFormatScore, + MinUpgradeFormatScore = model.MinUpgradeFormatScore, FormatItems = model.FormatItems.ConvertAll(ToResource), Language = model.Language }; @@ -106,6 +108,7 @@ namespace Radarr.Api.V3.Profiles.Quality Items = resource.Items.ConvertAll(ToModel), MinFormatScore = resource.MinFormatScore, CutoffFormatScore = resource.CutoffFormatScore, + MinUpgradeFormatScore = resource.MinUpgradeFormatScore, FormatItems = resource.FormatItems.ConvertAll(ToModel), Language = resource.Language };