diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js index ece0e8728..947b2ff54 100644 --- a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js @@ -123,6 +123,7 @@ class EditQualityProfileModalContent extends Component { upgradeAllowed, cutoff, minFormatScore, + minUpgradeFormatScore, cutoffFormatScore, items, formatItems @@ -244,6 +245,25 @@ class EditQualityProfileModalContent extends Component { } + { + upgradeAllowed.value && formatItems.value.length > 0 ? + + + {translate('MinimumCustomFormatScoreIncrement')} + + + + : + null + } +
{getCustomFormatRender(formatItems, otherProps)}
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 9bf70fa13..e3a7a71ee 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs @@ -5,6 +5,7 @@ using NzbDrone.Core.CustomFormats; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.Profiles; using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; @@ -79,9 +80,9 @@ public void should_return_false_if_proper_and_autoDownloadPropers_is_do_not_pref GivenAutoDownloadPropers(ProperDownloadTypes.DoNotPrefer); var profile = new QualityProfile - { - Items = Qualities.QualityFixture.GetDefaultQualities(), - }; + { + Items = Qualities.QualityFixture.GetDefaultQualities(), + }; Subject.IsUpgradable( profile, @@ -96,9 +97,9 @@ public void should_return_false_if_proper_and_autoDownloadPropers_is_do_not_pref public void should_return_false_if_release_and_existing_file_are_the_same() { var profile = new QualityProfile - { - Items = Qualities.QualityFixture.GetDefaultQualities() - }; + { + Items = Qualities.QualityFixture.GetDefaultQualities() + }; Subject.IsUpgradable( profile, @@ -146,5 +147,95 @@ public void should_return_false_if_release_has_higher_quality_and_cutoff_is_alre 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/212_add_minium_upgrade_format_score_to_quality_profiles.cs b/src/NzbDrone.Core/Datastore/Migration/212_add_minium_upgrade_format_score_to_quality_profiles.cs new file mode 100644 index 000000000..a6cb5492b --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/212_add_minium_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(212)] + public class add_minium_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 c4ecbd19a..d0eb1603a 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs @@ -95,6 +95,17 @@ public UpgradeableRejectReason IsUpgradable(QualityProfile qualityProfile, Quali 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 15168e15f..c316a2fb7 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs @@ -77,6 +77,9 @@ public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase 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); } } 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 @@ public enum UpgradeableRejectReason 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 @@ public void Clean() { profile.MinFormatScore = 0; profile.CutoffFormatScore = 0; + profile.MinUpgradeFormatScore = 1; } updatedProfiles.Add(profile); @@ -73,7 +74,7 @@ public void Clean() 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 aba736a67..8d58e8c9b 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1176,6 +1176,8 @@ "MinimumAgeHelpText": "Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider.", "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 f8a214042..68eecfb49 100644 --- a/src/NzbDrone.Core/Profiles/Qualities/QualityProfile.cs +++ b/src/NzbDrone.Core/Profiles/Qualities/QualityProfile.cs @@ -18,6 +18,7 @@ public QualityProfile() public int Cutoff { get; set; } public int MinFormatScore { get; set; } public int CutoffFormatScore { get; set; } + public int MinUpgradeFormatScore { get; set; } public List FormatItems { get; set; } public List Items { get; set; } diff --git a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs index 67b7dc18f..c53362b47 100644 --- a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs +++ b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs @@ -174,6 +174,7 @@ public void Handle(CustomFormatDeletedEvent message) { profile.MinFormatScore = 0; profile.CutoffFormatScore = 0; + profile.MinUpgradeFormatScore = 1; } Update(profile); @@ -232,6 +233,7 @@ public QualityProfile GetDefaultProfile(string name, Quality cutoff = null, para Items = items, MinFormatScore = 0, CutoffFormatScore = 0, + MinUpgradeFormatScore = 1, FormatItems = formatItems }; diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileController.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileController.cs index 46a1c6730..ac75f7fb0 100644 --- a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileController.cs +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileController.cs @@ -15,20 +15,19 @@ namespace Sonarr.Api.V3.Profiles.Quality public class QualityProfileController : RestController { private readonly IQualityProfileService _profileService; - private readonly ICustomFormatService _formatService; public QualityProfileController(IQualityProfileService profileService, ICustomFormatService formatService) { _profileService = profileService; - _formatService = formatService; SharedValidator.RuleFor(c => c.Name).NotEmpty(); + 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/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs index 8f7fef948..e0707b7f8 100644 --- a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs @@ -15,6 +15,7 @@ public class QualityProfileResource : RestResource 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; } } @@ -56,6 +57,7 @@ public static QualityProfileResource ToResource(this QualityProfile model) Items = model.Items.ConvertAll(ToResource), MinFormatScore = model.MinFormatScore, CutoffFormatScore = model.CutoffFormatScore, + MinUpgradeFormatScore = model.MinUpgradeFormatScore, FormatItems = model.FormatItems.ConvertAll(ToResource) }; } @@ -103,6 +105,7 @@ public static QualityProfile ToModel(this QualityProfileResource resource) Items = resource.Items.ConvertAll(ToModel), MinFormatScore = resource.MinFormatScore, CutoffFormatScore = resource.CutoffFormatScore, + MinUpgradeFormatScore = resource.MinUpgradeFormatScore, FormatItems = resource.FormatItems.ConvertAll(ToModel) }; } diff --git a/src/Sonarr.Api.V3/openapi.json b/src/Sonarr.Api.V3/openapi.json index b4f752920..399e3f6ed 100644 --- a/src/Sonarr.Api.V3/openapi.json +++ b/src/Sonarr.Api.V3/openapi.json @@ -10673,6 +10673,10 @@ "type": "integer", "format": "int32" }, + "minUpgradeFormatScore": { + "type": "integer", + "format": "int32" + }, "formatItems": { "type": "array", "items": {