diff --git a/src/NzbDrone.Api/Config/IndexerConfigModule.cs b/src/NzbDrone.Api/Config/IndexerConfigModule.cs index aa9c8d5bf..073a1d505 100644 --- a/src/NzbDrone.Api/Config/IndexerConfigModule.cs +++ b/src/NzbDrone.Api/Config/IndexerConfigModule.cs @@ -9,6 +9,12 @@ namespace NzbDrone.Api.Config public IndexerConfigModule(IConfigService configService) : base(configService) { + SharedValidator.RuleFor(c => c.MinimumAge) + .GreaterThan(0); + + SharedValidator.RuleFor(c => c.Retention) + .GreaterThanOrEqualTo(0); + SharedValidator.RuleFor(c => c.RssSyncInterval) .InclusiveBetween(10, 120) .When(c => c.RssSyncInterval > 0); diff --git a/src/NzbDrone.Api/Config/IndexerConfigResource.cs b/src/NzbDrone.Api/Config/IndexerConfigResource.cs index e902b538f..c5581f112 100644 --- a/src/NzbDrone.Api/Config/IndexerConfigResource.cs +++ b/src/NzbDrone.Api/Config/IndexerConfigResource.cs @@ -5,6 +5,7 @@ namespace NzbDrone.Api.Config { public class IndexerConfigResource : RestResource { + public Int32 MinimumAge { get; set; } public Int32 Retention { get; set; } public Int32 RssSyncInterval { get; set; } } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/MinimumAgeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/MinimumAgeSpecificationFixture.cs new file mode 100644 index 000000000..745eb68d5 --- /dev/null +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/MinimumAgeSpecificationFixture.cs @@ -0,0 +1,64 @@ +using System; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.DecisionEngineTests +{ + [TestFixture] + + public class MinimumAgeSpecificationFixture : CoreTest + { + private RemoteEpisode _remoteEpisode; + + [SetUp] + public void Setup() + { + _remoteEpisode = new RemoteEpisode + { + Release = new ReleaseInfo() { DownloadProtocol = DownloadProtocol.Usenet } + }; + } + + private void WithMinimumAge(int minutes) + { + Mocker.GetMock().SetupGet(c => c.MinimumAge).Returns(minutes); + } + + private void WithAge(int minutes) + { + _remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddMinutes(-minutes); + } + + [Test] + public void should_return_true_when_minimum_age_is_set_to_zero() + { + WithMinimumAge(0); + WithAge(100); + + Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_true_when_age_is_greater_than_minimum_age() + { + WithMinimumAge(30); + WithAge(100); + + Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue(); + } + + [Test] + public void should_return_false_when_age_is_less_than_minimum_age() + { + WithMinimumAge(30); + WithAge(10); + + Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse(); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RetentionSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RetentionSpecificationFixture.cs index e5249c4aa..a9c8ace61 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RetentionSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RetentionSpecificationFixture.cs @@ -13,7 +13,6 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public class RetentionSpecificationFixture : CoreTest { - private RemoteEpisode _remoteEpisode; [SetUp] @@ -32,7 +31,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private void WithAge(int days) { - _remoteEpisode.Release.PublishDate = DateTime.Now.AddDays(-days); + _remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddDays(-days); } [Test] diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index ad9006b4c..5e711b73c 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -136,6 +136,7 @@ + diff --git a/src/NzbDrone.Core/Configuration/ConfigService.cs b/src/NzbDrone.Core/Configuration/ConfigService.cs index 28baf7f80..9f6a0ab66 100644 --- a/src/NzbDrone.Core/Configuration/ConfigService.cs +++ b/src/NzbDrone.Core/Configuration/ConfigService.cs @@ -108,6 +108,13 @@ namespace NzbDrone.Core.Configuration set { SetValue("RssSyncInterval", value); } } + public int MinimumAge + { + get { return GetValueInt("MinimumAge", 0); } + + set { SetValue("MinimumAge", value); } + } + public Boolean AutoDownloadPropers { get { return GetValueBoolean("AutoDownloadPropers", true); } diff --git a/src/NzbDrone.Core/Configuration/IConfigService.cs b/src/NzbDrone.Core/Configuration/IConfigService.cs index 2b6717600..423c7fd96 100644 --- a/src/NzbDrone.Core/Configuration/IConfigService.cs +++ b/src/NzbDrone.Core/Configuration/IConfigService.cs @@ -49,6 +49,7 @@ namespace NzbDrone.Core.Configuration //Indexers Int32 Retention { get; set; } Int32 RssSyncInterval { get; set; } + Int32 MinimumAge { get; set; } //UI Int32 FirstDayOfWeek { get; set; } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs new file mode 100644 index 000000000..36da39acb --- /dev/null +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/MinimumAgeSpecification.cs @@ -0,0 +1,43 @@ +using NLog; +using NzbDrone.Core.Configuration; +using NzbDrone.Core.IndexerSearch.Definitions; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.DecisionEngine.Specifications +{ + public class MinimumAgeSpecification : IDecisionEngineSpecification + { + private readonly IConfigService _configService; + private readonly Logger _logger; + + public MinimumAgeSpecification(IConfigService configService, Logger logger) + { + _configService = configService; + _logger = logger; + } + + public RejectionType Type { get { return RejectionType.Temporary; } } + + public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) + { + if (subject.Release.DownloadProtocol != Indexers.DownloadProtocol.Usenet) + { + _logger.Debug("Not checking minimum age requirement for non-usenet report"); + return Decision.Accept(); + } + + var age = subject.Release.AgeMinutes; + var minimumAge = _configService.MinimumAge; + + _logger.Debug("Checking if report meets minimum age requirements. {0}", age); + + if (minimumAge > 0 && age < minimumAge) + { + _logger.Debug("Only {0} minutes old, minimum age is {1} minutes", age, minimumAge); + return Decision.Reject("Only {0} minutes old, minimum age is {1} minutes", age, minimumAge); + } + + return Decision.Accept(); + } + } +} diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs index 4963002ca..4fe19d9d0 100644 --- a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs +++ b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common.Extensions; +using NzbDrone.Core.Configuration; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Indexers; using NzbDrone.Core.Messaging.Events; @@ -36,6 +37,7 @@ namespace NzbDrone.Core.Download.Pending private readonly ISeriesService _seriesService; private readonly IParsingService _parsingService; private readonly IDelayProfileService _delayProfileService; + private readonly IConfigService _configService; private readonly IEventAggregator _eventAggregator; private readonly Logger _logger; @@ -43,6 +45,7 @@ namespace NzbDrone.Core.Download.Pending ISeriesService seriesService, IParsingService parsingService, IDelayProfileService delayProfileService, + IConfigService configService, IEventAggregator eventAggregator, Logger logger) { @@ -50,6 +53,7 @@ namespace NzbDrone.Core.Download.Pending _seriesService = seriesService; _parsingService = parsingService; _delayProfileService = delayProfileService; + _configService = configService; _eventAggregator = eventAggregator; _logger = logger; } @@ -202,8 +206,10 @@ namespace NzbDrone.Core.Download.Pending private int GetDelay(RemoteEpisode remoteEpisode) { var delayProfile = _delayProfileService.AllForTags(remoteEpisode.Series.Tags).OrderBy(d => d.Order).First(); + var delay = delayProfile.GetProtocolDelay(remoteEpisode.Release.DownloadProtocol); + var minimumAge = _configService.MinimumAge; - return delayProfile.GetProtocolDelay(remoteEpisode.Release.DownloadProtocol); + return new [] { delay, minimumAge }.Max(); } private void RemoveGrabbed(RemoteEpisode remoteEpisode) diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 14ec9ed73..4c63cd740 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -268,6 +268,7 @@ + diff --git a/src/UI/Activity/Queue/TimeleftCell.js b/src/UI/Activity/Queue/TimeleftCell.js index cd592615b..c0c8eed11 100644 --- a/src/UI/Activity/Queue/TimeleftCell.js +++ b/src/UI/Activity/Queue/TimeleftCell.js @@ -18,7 +18,7 @@ define( //If the release is pending we want to use the timeleft as the time it will be processed at if (this.cellValue.get('status').toLowerCase() === 'pending') { this.$el.html('-'); - this.$el.attr('title', 'Will be processed {0}'.format(moment(this.cellValue.get('estimatedCompletionTime')).calendar())); + this.$el.attr('title', 'Will be processed during the first RSS Sync after {0}'.format(moment(this.cellValue.get('estimatedCompletionTime')).calendar())); this.$el.attr('data-container', 'body'); return this; diff --git a/src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.hbs b/src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.hbs index 0c222b999..fa3d45be2 100644 --- a/src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.hbs +++ b/src/UI/Settings/Indexers/Options/IndexerOptionsViewTemplate.hbs @@ -1,10 +1,26 @@ 
Options +
+ + +
+ +
+ +
+ +
+
+
-
+
+ +
+ +