diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj
index 62f2c41ff..92ff378f7 100644
--- a/src/NzbDrone.Api/NzbDrone.Api.csproj
+++ b/src/NzbDrone.Api/NzbDrone.Api.csproj
@@ -98,6 +98,8 @@
+
+
@@ -200,6 +202,7 @@
+
@@ -221,6 +224,7 @@
+
diff --git a/src/NzbDrone.Api/Profiles/Delay/DelayProfileModule.cs b/src/NzbDrone.Api/Profiles/Delay/DelayProfileModule.cs
new file mode 100644
index 000000000..6cea9b4f7
--- /dev/null
+++ b/src/NzbDrone.Api/Profiles/Delay/DelayProfileModule.cs
@@ -0,0 +1,62 @@
+using System.Collections.Generic;
+using FluentValidation;
+using NzbDrone.Api.Mapping;
+using NzbDrone.Api.REST;
+using NzbDrone.Api.Validation;
+using NzbDrone.Core.Profiles.Delay;
+
+namespace NzbDrone.Api.Profiles.Delay
+{
+ public class DelayProfileModule : NzbDroneRestModule
+ {
+ private readonly IDelayProfileService _delayProfileService;
+
+ public DelayProfileModule(IDelayProfileService delayProfileService, DelayProfileTagInUseValidator tagInUseValidator)
+ {
+ _delayProfileService = delayProfileService;
+
+ GetResourceAll = GetAll;
+ GetResourceById = GetById;
+ UpdateResource = Update;
+ CreateResource = Create;
+ DeleteResource = DeleteProfile;
+
+ SharedValidator.RuleFor(d => d.Tags).NotEmpty().When(d => d.Id != 1);
+ SharedValidator.RuleFor(d => d.Tags).EmptyCollection().When(d => d.Id == 1);
+ SharedValidator.RuleFor(d => d.Tags).SetValidator(tagInUseValidator);
+ }
+
+ private int Create(DelayProfileResource resource)
+ {
+ var model = resource.InjectTo();
+ model = _delayProfileService.Add(model);
+
+ return model.Id;
+ }
+
+ private void DeleteProfile(int id)
+ {
+ if (id == 1)
+ {
+ throw new MethodNotAllowedException("Cannot delete global delay profile");
+ }
+
+ _delayProfileService.Delete(id);
+ }
+
+ private void Update(DelayProfileResource resource)
+ {
+ GetNewId(_delayProfileService.Update, resource);
+ }
+
+ private DelayProfileResource GetById(int id)
+ {
+ return _delayProfileService.Get(id).InjectTo();
+ }
+
+ private List GetAll()
+ {
+ return _delayProfileService.All().InjectTo>();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/NzbDrone.Api/Profiles/Delay/DelayProfileResource.cs b/src/NzbDrone.Api/Profiles/Delay/DelayProfileResource.cs
new file mode 100644
index 000000000..00e5c8e44
--- /dev/null
+++ b/src/NzbDrone.Api/Profiles/Delay/DelayProfileResource.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+using NzbDrone.Api.REST;
+using NzbDrone.Core.Indexers;
+
+namespace NzbDrone.Api.Profiles.Delay
+{
+ public class DelayProfileResource : RestResource
+ {
+ public DownloadProtocol PreferredProtocol { get; set; }
+ public int UsenetDelay { get; set; }
+ public int TorrentDelay { get; set; }
+ public int Order { get; set; }
+ public HashSet Tags { get; set; }
+ }
+}
diff --git a/src/NzbDrone.Api/Profiles/ProfileModule.cs b/src/NzbDrone.Api/Profiles/ProfileModule.cs
index 413d99281..ca1276dad 100644
--- a/src/NzbDrone.Api/Profiles/ProfileModule.cs
+++ b/src/NzbDrone.Api/Profiles/ProfileModule.cs
@@ -46,8 +46,6 @@ namespace NzbDrone.Api.Profiles
model.Cutoff = (Quality)resource.Cutoff.Id;
model.Items = resource.Items.InjectTo>();
model.Language = resource.Language;
- model.GrabDelay = resource.GrabDelay;
- model.GrabDelayMode = resource.GrabDelayMode;
_profileService.Update(model);
}
diff --git a/src/NzbDrone.Api/Profiles/ProfileResource.cs b/src/NzbDrone.Api/Profiles/ProfileResource.cs
index 432569460..9adb4ca70 100644
--- a/src/NzbDrone.Api/Profiles/ProfileResource.cs
+++ b/src/NzbDrone.Api/Profiles/ProfileResource.cs
@@ -2,7 +2,6 @@
using System.Collections.Generic;
using NzbDrone.Api.REST;
using NzbDrone.Core.Parser;
-using NzbDrone.Core.Profiles;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Api.Profiles
@@ -13,8 +12,6 @@ namespace NzbDrone.Api.Profiles
public Quality Cutoff { get; set; }
public List Items { get; set; }
public Language Language { get; set; }
- public Int32 GrabDelay { get; set; }
- public GrabDelayMode GrabDelayMode { get; set; }
}
public class ProfileQualityItemResource : RestResource
diff --git a/src/NzbDrone.Api/REST/MethodNotAllowedException.cs b/src/NzbDrone.Api/REST/MethodNotAllowedException.cs
new file mode 100644
index 000000000..44d2065c6
--- /dev/null
+++ b/src/NzbDrone.Api/REST/MethodNotAllowedException.cs
@@ -0,0 +1,13 @@
+using Nancy;
+using NzbDrone.Api.ErrorManagement;
+
+namespace NzbDrone.Api.REST
+{
+ public class MethodNotAllowedException : ApiException
+ {
+ public MethodNotAllowedException(object content = null)
+ : base(HttpStatusCode.MethodNotAllowed, content)
+ {
+ }
+ }
+}
diff --git a/src/NzbDrone.Api/Validation/EmptyCollectionValidator.cs b/src/NzbDrone.Api/Validation/EmptyCollectionValidator.cs
new file mode 100644
index 000000000..432eb1ed9
--- /dev/null
+++ b/src/NzbDrone.Api/Validation/EmptyCollectionValidator.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+using FluentValidation.Validators;
+using NzbDrone.Common.Extensions;
+
+namespace NzbDrone.Api.Validation
+{
+ public class EmptyCollectionValidator : PropertyValidator
+ {
+ public EmptyCollectionValidator()
+ : base("Collection Must Be Empty")
+ {
+ }
+
+ protected override bool IsValid(PropertyValidatorContext context)
+ {
+ if (context.PropertyValue == null) return true;
+
+ var collection = context.PropertyValue as IEnumerable;
+
+ return collection != null && collection.Empty();
+ }
+ }
+}
diff --git a/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs b/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs
index e88607b65..45cd0e1c6 100644
--- a/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs
+++ b/src/NzbDrone.Api/Validation/RuleBuilderExtensions.cs
@@ -1,4 +1,5 @@
-using System.Text.RegularExpressions;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
using FluentValidation;
using FluentValidation.Validators;
@@ -25,5 +26,10 @@ namespace NzbDrone.Api.Validation
{
return ruleBuilder.SetValidator(new NotNullValidator()).SetValidator(new NotEmptyValidator(""));
}
+
+ public static IRuleBuilderOptions> EmptyCollection(this IRuleBuilder> ruleBuilder)
+ {
+ return ruleBuilder.SetValidator(new EmptyCollectionValidator());
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs
index 08c66f2ef..47e3ed127 100644
--- a/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/PrioritizeDownloadDecisionFixture.cs
@@ -1,12 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using Moq;
+using NzbDrone.Core.Indexers;
+using NzbDrone.Core.Profiles.Delay;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Profiles;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.DecisionEngine;
-using NzbDrone.Core.DecisionEngine.Specifications;
using NUnit.Framework;
using FluentAssertions;
using FizzWare.NBuilder;
@@ -17,6 +19,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[TestFixture]
public class PrioritizeDownloadDecisionFixture : CoreTest
{
+ [SetUp]
+ public void Setup()
+ {
+ GivenPreferredDownloadProtocol(DownloadProtocol.Usenet);
+ }
+
private Episode GivenEpisode(int id)
{
return Builder.CreateNew()
@@ -25,7 +33,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
.Build();
}
- private RemoteEpisode GivenRemoteEpisode(List episodes, QualityModel quality, int age = 0, long size = 0)
+ private RemoteEpisode GivenRemoteEpisode(List episodes, QualityModel quality, int age = 0, long size = 0, DownloadProtocol downloadProtocol = DownloadProtocol.Usenet)
{
var remoteEpisode = new RemoteEpisode();
remoteEpisode.ParsedEpisodeInfo = new ParsedEpisodeInfo();
@@ -37,6 +45,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
remoteEpisode.Release = new ReleaseInfo();
remoteEpisode.Release.PublishDate = DateTime.Now.AddDays(-age);
remoteEpisode.Release.Size = size;
+ remoteEpisode.Release.DownloadProtocol = downloadProtocol;
remoteEpisode.Series = Builder.CreateNew()
.With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() })
@@ -45,6 +54,16 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
return remoteEpisode;
}
+ private void GivenPreferredDownloadProtocol(DownloadProtocol downloadProtocol)
+ {
+ Mocker.GetMock()
+ .Setup(s => s.BestForTags(It.IsAny>()))
+ .Returns(new DelayProfile
+ {
+ PreferredProtocol = downloadProtocol
+ });
+ }
+
[Test]
public void should_put_propers_before_non_propers()
{
@@ -148,5 +167,37 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
Subject.PrioritizeDecisions(decisions);
}
+
+ [Test]
+ public void should_put_usenet_above_torrent_when_usenet_is_preferred()
+ {
+ GivenPreferredDownloadProtocol(DownloadProtocol.Usenet);
+
+ var remoteEpisode1 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), downloadProtocol: DownloadProtocol.Torrent);
+ var remoteEpisode2 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), downloadProtocol: DownloadProtocol.Usenet);
+
+ var decisions = new List();
+ decisions.Add(new DownloadDecision(remoteEpisode1));
+ decisions.Add(new DownloadDecision(remoteEpisode2));
+
+ var qualifiedReports = Subject.PrioritizeDecisions(decisions);
+ qualifiedReports.First().RemoteEpisode.Release.DownloadProtocol.Should().Be(DownloadProtocol.Usenet);
+ }
+
+ [Test]
+ public void should_put_torrent_above_usenet_when_torrent_is_preferred()
+ {
+ GivenPreferredDownloadProtocol(DownloadProtocol.Torrent);
+
+ var remoteEpisode1 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), downloadProtocol: DownloadProtocol.Torrent);
+ var remoteEpisode2 = GivenRemoteEpisode(new List { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), downloadProtocol: DownloadProtocol.Usenet);
+
+ var decisions = new List();
+ decisions.Add(new DownloadDecision(remoteEpisode1));
+ decisions.Add(new DownloadDecision(remoteEpisode2));
+
+ var qualifiedReports = Subject.PrioritizeDecisions(decisions);
+ qualifiedReports.First().RemoteEpisode.Release.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
+ }
}
}
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs
index 0beb24e40..2c2083036 100644
--- a/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs
@@ -15,12 +15,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
[TestFixture]
public class ReleaseRestrictionsSpecificationFixture : CoreTest
{
- private RemoteEpisode _parseResult;
+ private RemoteEpisode _remoteEpisode;
[SetUp]
public void Setup()
{
- _parseResult = new RemoteEpisode
+ _remoteEpisode = new RemoteEpisode
{
Series = new Series
{
@@ -54,7 +54,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
.Setup(s => s.AllForTags(It.IsAny>()))
.Returns(new List());
- Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeTrue();
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
@@ -62,7 +62,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
GivenRestictions("WEBRip", null);
- Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeTrue();
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
@@ -70,7 +70,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
GivenRestictions("doesnt,exist", null);
- Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeFalse();
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
}
[Test]
@@ -78,7 +78,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
GivenRestictions(null, "ignored");
- Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeTrue();
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[Test]
@@ -86,7 +86,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
GivenRestictions(null, "edited");
- Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeFalse();
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
}
[TestCase("EdiTED")]
@@ -97,7 +97,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
GivenRestictions(required, null);
- Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeTrue();
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
[TestCase("EdiTED")]
@@ -108,7 +108,22 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
{
GivenRestictions(null, ignored);
- Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeFalse();
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
+ }
+
+ [Test]
+ public void should_be_false_when_release_contains_one_restricted_word_and_one_required_word()
+ {
+ _remoteEpisode.Release.Title = "[ www.Speed.cd ] -Whose.Line.is.it.Anyway.US.S10E24.720p.HDTV.x264-BAJSKORV";
+
+ Mocker.GetMock()
+ .Setup(s => s.AllForTags(It.IsAny>()))
+ .Returns(new List
+ {
+ new Restriction { Required = "x264", Ignored = "www.Speed.cd" }
+ });
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
}
}
}
diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs
index 536c29660..df8c9939f 100644
--- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs
@@ -9,14 +9,15 @@ using NUnit.Framework;
using NzbDrone.Core.DecisionEngine;
using NzbDrone.Core.DecisionEngine.Specifications.RssSync;
using NzbDrone.Core.Download.Pending;
+using NzbDrone.Core.Indexers;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Profiles;
+using NzbDrone.Core.Profiles.Delay;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Test.Framework;
using NzbDrone.Core.Tv;
-using NzbDrone.Test.Common;
namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
{
@@ -24,6 +25,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
public class DelaySpecificationFixture : CoreTest
{
private Profile _profile;
+ private DelayProfile _delayProfile;
private RemoteEpisode _remoteEpisode;
[SetUp]
@@ -32,6 +34,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
_profile = Builder.CreateNew()
.Build();
+ _delayProfile = Builder.CreateNew()
+ .With(d => d.PreferredProtocol = DownloadProtocol.Usenet)
+ .Build();
+
var series = Builder.CreateNew()
.With(s => s.Profile = _profile)
.Build();
@@ -46,13 +52,21 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
_profile.Items.Add(new ProfileQualityItem { Allowed = true, Quality = Quality.Bluray720p });
_profile.Cutoff = Quality.WEBDL720p;
- _profile.GrabDelayMode = GrabDelayMode.Always;
_remoteEpisode.ParsedEpisodeInfo = new ParsedEpisodeInfo();
_remoteEpisode.Release = new ReleaseInfo();
+ _remoteEpisode.Release.DownloadProtocol = DownloadProtocol.Usenet;
_remoteEpisode.Episodes = Builder.CreateListOfSize(1).Build().ToList();
_remoteEpisode.Episodes.First().EpisodeFileId = 0;
+
+ Mocker.GetMock()
+ .Setup(s => s.BestForTags(It.IsAny>()))
+ .Returns(_delayProfile);
+
+ Mocker.GetMock()
+ .Setup(s => s.GetPendingRemoteEpisodes(It.IsAny()))
+ .Returns(new List());
}
private void GivenExistingFile(QualityModel quality)
@@ -81,7 +95,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
[Test]
public void should_be_true_when_profile_does_not_have_a_delay()
{
- _profile.GrabDelay = 0;
+ _delayProfile.UsenetDelay = 0;
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
@@ -99,8 +113,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
{
_remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.HDTV720p);
_remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddHours(-10);
-
- _profile.GrabDelay = 1;
+
+ _delayProfile.UsenetDelay = 1;
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
@@ -111,7 +125,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
_remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.SDTV);
_remoteEpisode.Release.PublishDate = DateTime.UtcNow;
- _profile.GrabDelay = 12;
+ _delayProfile.UsenetDelay = 12;
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
}
@@ -129,7 +143,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
.Setup(s => s.IsRevisionUpgrade(It.IsAny(), It.IsAny()))
.Returns(true);
- _profile.GrabDelay = 12;
+ _delayProfile.UsenetDelay = 12;
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
@@ -147,47 +161,11 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
.Setup(s => s.IsRevisionUpgrade(It.IsAny(), It.IsAny()))
.Returns(true);
- _profile.GrabDelay = 12;
-
- Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
- }
-
- [Test]
- public void should_be_true_when_release_meets_cutoff_and_mode_is_cutoff()
- {
- _profile.GrabDelayMode = GrabDelayMode.Cutoff;
- _remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.WEBDL720p);
- _remoteEpisode.Release.PublishDate = DateTime.UtcNow;
-
- _profile.GrabDelay = 12;
+ _delayProfile.UsenetDelay = 12;
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
}
- [Test]
- public void should_be_true_when_release_exceeds_cutoff_and_mode_is_cutoff()
- {
- _profile.GrabDelayMode = GrabDelayMode.Cutoff;
- _remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.Bluray720p);
- _remoteEpisode.Release.PublishDate = DateTime.UtcNow;
-
- _profile.GrabDelay = 12;
-
- Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
- }
-
- [Test]
- public void should_be_false_when_release_is_below_cutoff_and_mode_is_cutoff()
- {
- _profile.GrabDelayMode = GrabDelayMode.Cutoff;
- _remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.HDTV720p);
- _remoteEpisode.Release.PublishDate = DateTime.UtcNow;
-
- _profile.GrabDelay = 12;
-
- Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
- }
-
[Test]
public void should_be_false_when_release_is_proper_for_existing_episode_of_different_quality()
{
@@ -196,82 +174,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
GivenExistingFile(new QualityModel(Quality.SDTV));
- _profile.GrabDelay = 12;
-
- Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
- }
-
- [Test]
- public void should_be_false_when_release_is_first_detected_and_mode_is_first()
- {
- _profile.GrabDelayMode = GrabDelayMode.First;
- _remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.HDTV720p);
- _remoteEpisode.Release.PublishDate = DateTime.UtcNow;
-
- _profile.GrabDelay = 12;
-
- Mocker.GetMock()
- .Setup(s => s.GetPendingRemoteEpisodes(It.IsAny()))
- .Returns(new List());
-
- Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
- }
-
- [Test]
- public void should_be_false_when_release_is_not_first_but_oldest_has_not_expired_and_type_is_first()
- {
- _profile.GrabDelayMode = GrabDelayMode.First;
- _remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.HDTV720p);
- _remoteEpisode.Release.PublishDate = DateTime.UtcNow;
-
- _profile.GrabDelay = 12;
-
- Mocker.GetMock()
- .Setup(s => s.GetPendingRemoteEpisodes(It.IsAny()))
- .Returns(new List { _remoteEpisode.JsonClone() });
+ _delayProfile.UsenetDelay = 12;
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
}
-
- [Test]
- public void should_be_true_when_existing_pending_release_expired_and_mode_is_first()
- {
- _profile.GrabDelayMode = GrabDelayMode.First;
-
- _remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.WEBDL720p);
- _remoteEpisode.Release.PublishDate = DateTime.UtcNow;
- _profile.GrabDelay = 12;
-
- var pendingRemoteEpisode = _remoteEpisode.JsonClone();
- pendingRemoteEpisode.Release.PublishDate = DateTime.UtcNow.AddHours(-15);
-
- Mocker.GetMock()
- .Setup(s => s.GetPendingRemoteEpisodes(It.IsAny()))
- .Returns(new List { pendingRemoteEpisode });
-
- Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
- }
-
- [Test]
- public void should_be_true_when_one_existing_pending_release_is_expired_and_mode_is_first()
- {
- _profile.GrabDelayMode = GrabDelayMode.First;
-
- _remoteEpisode.ParsedEpisodeInfo.Quality = new QualityModel(Quality.WEBDL720p);
- _remoteEpisode.Release.PublishDate = DateTime.UtcNow;
- _profile.GrabDelay = 12;
-
- var pendingRemoteEpisode1 = _remoteEpisode.JsonClone();
- pendingRemoteEpisode1.Release.PublishDate = DateTime.UtcNow.AddHours(-15);
-
- var pendingRemoteEpisode2 = _remoteEpisode.JsonClone();
- pendingRemoteEpisode2.Release.PublishDate = DateTime.UtcNow.AddHours(5);
-
- Mocker.GetMock()
- .Setup(s => s.GetPendingRemoteEpisodes(It.IsAny()))
- .Returns(new List { pendingRemoteEpisode1, pendingRemoteEpisode2 });
-
- Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
- }
}
}
diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs
index e7b95035d..6dac469ab 100644
--- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/AddFixture.cs
@@ -40,7 +40,6 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
{
Name = "Test",
Cutoff = Quality.HDTV720p,
- GrabDelay = 1,
Items = new List
{
new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p },
diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs
index 424f61241..800f9a720 100644
--- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveGrabbedFixture.cs
@@ -40,7 +40,6 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
{
Name = "Test",
Cutoff = Quality.HDTV720p,
- GrabDelay = 1,
Items = new List
{
new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p },
diff --git a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs
index fa7560422..c80780c4a 100644
--- a/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs
+++ b/src/NzbDrone.Core.Test/Download/Pending/PendingReleaseServiceTests/RemoveRejectedFixture.cs
@@ -40,7 +40,6 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
{
Name = "Test",
Cutoff = Quality.HDTV720p,
- GrabDelay = 1,
Items = new List
{
new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p },
diff --git a/src/NzbDrone.Core/Datastore/Migration/070_delay_profile.cs b/src/NzbDrone.Core/Datastore/Migration/070_delay_profile.cs
new file mode 100644
index 000000000..a8add7616
--- /dev/null
+++ b/src/NzbDrone.Core/Datastore/Migration/070_delay_profile.cs
@@ -0,0 +1,163 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using FluentMigrator;
+using NzbDrone.Common;
+using NzbDrone.Common.Extensions;
+using NzbDrone.Common.Serializer;
+using NzbDrone.Core.Datastore.Migration.Framework;
+
+namespace NzbDrone.Core.Datastore.Migration
+{
+ [Migration(70)]
+ public class delay_profile : NzbDroneMigrationBase
+ {
+ protected override void MainDbUpgrade()
+ {
+ Create.TableForModel("DelayProfiles")
+ .WithColumn("PreferredProtocol").AsInt32().NotNullable()
+ .WithColumn("UsenetDelay").AsInt32().NotNullable()
+ .WithColumn("TorrentDelay").AsInt32().NotNullable()
+ .WithColumn("Order").AsInt32().NotNullable()
+ .WithColumn("Tags").AsString().NotNullable();
+
+
+ Insert.IntoTable("DelayProfiles").Row(new
+ {
+ PreferredProtocol = 1,
+ UsenetDelay = 0,
+ TorrentDelay = 0,
+ Order = Int32.MaxValue,
+ Tags = "[]"
+ });
+
+ Execute.WithConnection(ConvertProfile);
+
+ Delete.Column("GrabDelay").FromTable("Profiles");
+ Delete.Column("GrabDelayMode").FromTable("Profiles");
+ }
+
+ private void ConvertProfile(IDbConnection conn, IDbTransaction tran)
+ {
+ var profiles = GetProfiles(conn, tran);
+ var order = 1;
+
+ foreach (var profileClosure in profiles.DistinctBy(p => p.GrabDelay))
+ {
+ var profile = profileClosure;
+ if (profile.GrabDelay == 0) continue;
+
+ var tag = String.Format("delay-{0}", profile.GrabDelay);
+ var tagId = InsertTag(conn, tran, tag);
+ var tags = String.Format("[{0}]", tagId);
+
+ using (IDbCommand insertDelayProfileCmd = conn.CreateCommand())
+ {
+ insertDelayProfileCmd.Transaction = tran;
+ insertDelayProfileCmd.CommandText = "INSERT INTO DelayProfiles (PreferredProtocol, TorrentDelay, UsenetDelay, [Order], Tags) VALUES (1, 0, ?, ?, ?)";
+ insertDelayProfileCmd.AddParameter(profile.GrabDelay);
+ insertDelayProfileCmd.AddParameter(order);
+ insertDelayProfileCmd.AddParameter(tags);
+
+ insertDelayProfileCmd.ExecuteNonQuery();
+ }
+
+ var matchingProfileIds = profiles.Where(p => p.GrabDelay == profile.GrabDelay)
+ .Select(p => p.Id);
+
+ UpdateSeries(conn, tran, matchingProfileIds, tagId);
+
+ order++;
+ }
+ }
+
+ private List GetProfiles(IDbConnection conn, IDbTransaction tran)
+ {
+ var profiles = new List();
+
+ using (IDbCommand getProfilesCmd = conn.CreateCommand())
+ {
+ getProfilesCmd.Transaction = tran;
+ getProfilesCmd.CommandText = @"SELECT Id, GrabDelay FROM Profiles";
+
+ using (IDataReader profileReader = getProfilesCmd.ExecuteReader())
+ {
+ while (profileReader.Read())
+ {
+ var id = profileReader.GetInt32(0);
+ var delay = profileReader.GetInt32(1);
+
+ profiles.Add(new Profile70
+ {
+ Id = id,
+ GrabDelay = delay * 60
+ });
+ }
+ }
+ }
+
+ return profiles;
+ }
+
+ private Int32 InsertTag(IDbConnection conn, IDbTransaction tran, string tagLabel)
+ {
+ using (IDbCommand insertCmd = conn.CreateCommand())
+ {
+ insertCmd.Transaction = tran;
+ insertCmd.CommandText = @"INSERT INTO Tags (Label) VALUES (?); SELECT last_insert_rowid()";
+ insertCmd.AddParameter(tagLabel);
+
+ var id = insertCmd.ExecuteScalar();
+
+ return Convert.ToInt32(id);
+ }
+ }
+
+ private void UpdateSeries(IDbConnection conn, IDbTransaction tran, IEnumerable profileIds, int tagId)
+ {
+ using (IDbCommand getSeriesCmd = conn.CreateCommand())
+ {
+ getSeriesCmd.Transaction = tran;
+ getSeriesCmd.CommandText = "SELECT Id, Tags FROM Series WHERE ProfileId IN (?)";
+ getSeriesCmd.AddParameter(String.Join(",", profileIds));
+
+ using (IDataReader seriesReader = getSeriesCmd.ExecuteReader())
+ {
+ while (seriesReader.Read())
+ {
+ var id = seriesReader.GetInt32(0);
+ var tagString = seriesReader.GetString(1);
+
+ var tags = Json.Deserialize>(tagString);
+ tags.Add(tagId);
+
+ using (IDbCommand updateSeriesCmd = conn.CreateCommand())
+ {
+ updateSeriesCmd.Transaction = tran;
+ updateSeriesCmd.CommandText = "UPDATE Series SET Tags = ? WHERE Id = ?";
+ updateSeriesCmd.AddParameter(tags.ToJson());
+ updateSeriesCmd.AddParameter(id);
+
+ updateSeriesCmd.ExecuteNonQuery();
+ }
+ }
+ }
+
+ getSeriesCmd.ExecuteNonQuery();
+ }
+ }
+
+ private class Profile70
+ {
+ public int Id { get; set; }
+ public int GrabDelay { get; set; }
+ }
+
+ private class Series70
+ {
+ public int Id { get; set; }
+ public HashSet Tags { get; set; }
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs
index f6f90609f..4fa9a8bad 100644
--- a/src/NzbDrone.Core/Datastore/TableMapping.cs
+++ b/src/NzbDrone.Core/Datastore/TableMapping.cs
@@ -16,6 +16,7 @@ using NzbDrone.Core.Jobs;
using NzbDrone.Core.MediaFiles;
using NzbDrone.Core.Metadata;
using NzbDrone.Core.Metadata.Files;
+using NzbDrone.Core.Profiles.Delay;
using NzbDrone.Core.RemotePathMappings;
using NzbDrone.Core.Notifications;
using NzbDrone.Core.Organizer;
@@ -95,6 +96,8 @@ namespace NzbDrone.Core.Datastore
Mapper.Entity().RegisterModel("RemotePathMappings");
Mapper.Entity().RegisterModel("Tags");
Mapper.Entity().RegisterModel("Restrictions");
+
+ Mapper.Entity().RegisterModel("DelayProfiles");
}
private static void RegisterMappers()
diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs
index 191e60d3e..3bf52ae29 100644
--- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs
+++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionPriorizationService.cs
@@ -1,8 +1,11 @@
using System;
using System.Linq;
using System.Collections.Generic;
+using NzbDrone.Core.Indexers;
using NzbDrone.Core.Parser.Model;
+using NzbDrone.Core.Profiles.Delay;
using NzbDrone.Core.Qualities;
+using NzbDrone.Core.Tv;
namespace NzbDrone.Core.DecisionEngine
{
@@ -13,20 +16,44 @@ namespace NzbDrone.Core.DecisionEngine
public class DownloadDecisionPriorizationService : IPrioritizeDownloadDecision
{
+ private readonly IDelayProfileService _delayProfileService;
+
+ public DownloadDecisionPriorizationService(IDelayProfileService delayProfileService)
+ {
+ _delayProfileService = delayProfileService;
+ }
+
public List PrioritizeDecisions(List decisions)
{
- return decisions
- .Where(c => c.RemoteEpisode.Series != null)
- .GroupBy(c => c.RemoteEpisode.Series.Id, (i, s) => s
- .OrderByDescending(c => c.RemoteEpisode.ParsedEpisodeInfo.Quality, new QualityModelComparer(s.First().RemoteEpisode.Series.Profile))
- .ThenBy(c => c.RemoteEpisode.Episodes.Select(e => e.EpisodeNumber).MinOrDefault())
- .ThenBy(c => c.RemoteEpisode.Release.DownloadProtocol)
- .ThenBy(c => c.RemoteEpisode.Release.Size.Round(200.Megabytes()) / Math.Max(1, c.RemoteEpisode.Episodes.Count))
- .ThenByDescending(c => TorrentInfo.GetSeeders(c.RemoteEpisode.Release))
- .ThenBy(c => c.RemoteEpisode.Release.Age))
- .SelectMany(c => c)
- .Union(decisions.Where(c => c.RemoteEpisode.Series == null))
- .ToList();
+ return decisions.Where(c => c.RemoteEpisode.Series != null)
+ .GroupBy(c => c.RemoteEpisode.Series.Id, (seriesId, d) =>
+ {
+ var downloadDecisions = d.ToList();
+ var series = downloadDecisions.First().RemoteEpisode.Series;
+
+ return downloadDecisions
+ .OrderByDescending(c => c.RemoteEpisode.ParsedEpisodeInfo.Quality, new QualityModelComparer(series.Profile))
+ .ThenBy(c => c.RemoteEpisode.Episodes.Select(e => e.EpisodeNumber).MinOrDefault())
+ .ThenBy(c => PrioritizeDownloadProtocol(series, c.RemoteEpisode.Release.DownloadProtocol))
+ .ThenBy(c => c.RemoteEpisode.Release.Size.Round(200.Megabytes()) / Math.Max(1, c.RemoteEpisode.Episodes.Count))
+ .ThenByDescending(c => TorrentInfo.GetSeeders(c.RemoteEpisode.Release))
+ .ThenBy(c => c.RemoteEpisode.Release.Age);
+ })
+ .SelectMany(c => c)
+ .Union(decisions.Where(c => c.RemoteEpisode.Series == null))
+ .ToList();
+ }
+
+ private int PrioritizeDownloadProtocol(Series series, DownloadProtocol downloadProtocol)
+ {
+ var delayProfile = _delayProfileService.BestForTags(series.Tags);
+
+ if (downloadProtocol == delayProfile.PreferredProtocol)
+ {
+ return 0;
+ }
+
+ return 1;
}
}
}
diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs
index 3c18d0d4d..5b4e23608 100644
--- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs
+++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs
@@ -3,7 +3,7 @@ using NLog;
using NzbDrone.Core.Download.Pending;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
-using NzbDrone.Core.Profiles;
+using NzbDrone.Core.Profiles.Delay;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
@@ -12,12 +12,17 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
{
private readonly IPendingReleaseService _pendingReleaseService;
private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification;
+ private readonly IDelayProfileService _delayProfileService;
private readonly Logger _logger;
- public DelaySpecification(IPendingReleaseService pendingReleaseService, IQualityUpgradableSpecification qualityUpgradableSpecification, Logger logger)
+ public DelaySpecification(IPendingReleaseService pendingReleaseService,
+ IQualityUpgradableSpecification qualityUpgradableSpecification,
+ IDelayProfileService delayProfileService,
+ Logger logger)
{
_pendingReleaseService = pendingReleaseService;
_qualityUpgradableSpecification = qualityUpgradableSpecification;
+ _delayProfileService = delayProfileService;
_logger = logger;
}
@@ -35,71 +40,59 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
}
var profile = subject.Series.Profile.Value;
+ var delayProfile = _delayProfileService.BestForTags(subject.Series.Tags);
+ var delay = delayProfile.GetProtocolDelay(subject.Release.DownloadProtocol);
+ var isPreferredProtocol = subject.Release.DownloadProtocol == delayProfile.PreferredProtocol;
- if (profile.GrabDelay == 0)
+ if (delay == 0)
{
- _logger.Debug("Profile does not delay before download");
+ _logger.Debug("Profile does not require a waiting period before download for {0}.", subject.Release.DownloadProtocol);
return Decision.Accept();
}
var comparer = new QualityModelComparer(profile);
- foreach (var file in subject.Episodes.Where(c => c.EpisodeFileId != 0).Select(c => c.EpisodeFile.Value))
+ if (isPreferredProtocol)
{
- var upgradable = _qualityUpgradableSpecification.IsUpgradable(profile, file.Quality, subject.ParsedEpisodeInfo.Quality);
-
- if (upgradable)
+ foreach (var file in subject.Episodes.Where(c => c.EpisodeFileId != 0).Select(c => c.EpisodeFile.Value))
{
- var revisionUpgrade = _qualityUpgradableSpecification.IsRevisionUpgrade(file.Quality, subject.ParsedEpisodeInfo.Quality);
+ var upgradable = _qualityUpgradableSpecification.IsUpgradable(profile, file.Quality, subject.ParsedEpisodeInfo.Quality);
- if (revisionUpgrade)
+ if (upgradable)
{
- _logger.Debug("New quality is a better revision for existing quality, skipping delay");
- return Decision.Accept();
+ var revisionUpgrade = _qualityUpgradableSpecification.IsRevisionUpgrade(file.Quality, subject.ParsedEpisodeInfo.Quality);
+
+ if (revisionUpgrade)
+ {
+ _logger.Debug("New quality is a better revision for existing quality, skipping delay");
+ return Decision.Accept();
+ }
}
}
}
//If quality meets or exceeds the best allowed quality in the profile accept it immediately
- var bestQualityInProfile = new QualityModel(profile.Items.Last(q => q.Allowed).Quality);
- var bestCompare = comparer.Compare(subject.ParsedEpisodeInfo.Quality, bestQualityInProfile);
+ var bestQualityInProfile = new QualityModel(profile.LastAllowedQuality());
+ var isBestInProfile = comparer.Compare(subject.ParsedEpisodeInfo.Quality, bestQualityInProfile) >= 0;
- if (bestCompare >= 0)
+ if (isBestInProfile && isPreferredProtocol)
{
- _logger.Debug("Quality is highest in profile, will not delay");
+ _logger.Debug("Quality is highest in profile for preferred protocol, will not delay");
return Decision.Accept();
}
- if (profile.GrabDelayMode == GrabDelayMode.Cutoff)
- {
- var cutoff = new QualityModel(profile.Cutoff);
- var cutoffCompare = comparer.Compare(subject.ParsedEpisodeInfo.Quality, cutoff);
+ var episodeIds = subject.Episodes.Select(e => e.Id);
- if (cutoffCompare >= 0)
- {
- _logger.Debug("Quality meets or exceeds the cutoff, will not delay");
- return Decision.Accept();
- }
- }
+ var oldest = _pendingReleaseService.OldestPendingRelease(subject.Series.Id, episodeIds);
- if (profile.GrabDelayMode == GrabDelayMode.First)
+ if (oldest != null && oldest.Release.AgeHours > delay)
{
- var episodeIds = subject.Episodes.Select(e => e.Id);
-
- var oldest = _pendingReleaseService.GetPendingRemoteEpisodes(subject.Series.Id)
- .Where(r => r.Episodes.Select(e => e.Id).Intersect(episodeIds).Any())
- .OrderByDescending(p => p.Release.AgeHours)
- .FirstOrDefault();
-
- if (oldest != null && oldest.Release.AgeHours > profile.GrabDelay)
- {
- return Decision.Accept();
- }
+ return Decision.Accept();
}
- if (subject.Release.AgeHours < profile.GrabDelay)
+ if (subject.Release.AgeHours < delay)
{
- _logger.Debug("Age ({0}) is less than delay {1}, delaying", subject.Release.AgeHours, profile.GrabDelay);
+ _logger.Debug("Waiting for better quality release, There is a {0} hour delay on {1}", delay, subject.Release.DownloadProtocol);
return Decision.Reject("Waiting for better quality release");
}
diff --git a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs
index 0d9fd5a78..d58c506fb 100644
--- a/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs
+++ b/src/NzbDrone.Core/Download/Pending/PendingReleaseService.cs
@@ -5,9 +5,11 @@ using NLog;
using NzbDrone.Common;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.DecisionEngine;
+using NzbDrone.Core.Indexers;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Parser.Model;
+using NzbDrone.Core.Profiles.Delay;
using NzbDrone.Core.Qualities;
using NzbDrone.Core.Tv;
using NzbDrone.Core.Tv.Events;
@@ -20,8 +22,9 @@ namespace NzbDrone.Core.Download.Pending
void RemoveGrabbed(List grabbed);
void RemoveRejected(List rejected);
List GetPending();
- List GetPendingRemoteEpisodes(Int32 seriesId);
+ List GetPendingRemoteEpisodes(int seriesId);
List GetPendingQueue();
+ RemoteEpisode OldestPendingRelease(int seriesId, IEnumerable episodeIds);
}
public class PendingReleaseService : IPendingReleaseService, IHandle
@@ -29,18 +32,21 @@ namespace NzbDrone.Core.Download.Pending
private readonly IPendingReleaseRepository _repository;
private readonly ISeriesService _seriesService;
private readonly IParsingService _parsingService;
+ private readonly IDelayProfileService _delayProfileService;
private readonly IEventAggregator _eventAggregator;
private readonly Logger _logger;
public PendingReleaseService(IPendingReleaseRepository repository,
ISeriesService seriesService,
IParsingService parsingService,
+ IDelayProfileService delayProfileService,
IEventAggregator eventAggregator,
Logger logger)
{
_repository = repository;
_seriesService = seriesService;
_parsingService = parsingService;
+ _delayProfileService = delayProfileService;
_eventAggregator = eventAggregator;
_logger = logger;
}
@@ -138,8 +144,7 @@ namespace NzbDrone.Core.Download.Pending
{
foreach (var episode in pendingRelease.RemoteEpisode.Episodes)
{
- var ect = pendingRelease.Release.PublishDate.AddHours(
- pendingRelease.RemoteEpisode.Series.Profile.Value.GrabDelay);
+ var ect = pendingRelease.Release.PublishDate.AddMinutes(GetDelay(pendingRelease.RemoteEpisode));
var queue = new Queue.Queue
{
@@ -162,6 +167,14 @@ namespace NzbDrone.Core.Download.Pending
return queued;
}
+ public RemoteEpisode OldestPendingRelease(int seriesId, IEnumerable episodeIds)
+ {
+ return GetPendingRemoteEpisodes(seriesId)
+ .Where(r => r.Episodes.Select(e => e.Id).Intersect(episodeIds).Any())
+ .OrderByDescending(p => p.Release.AgeHours)
+ .FirstOrDefault();
+ }
+
private List GetPendingReleases()
{
var result = new List();
@@ -225,6 +238,13 @@ namespace NzbDrone.Core.Download.Pending
p.Release.Indexer == decision.RemoteEpisode.Release.Indexer;
}
+ private int GetDelay(RemoteEpisode remoteEpisode)
+ {
+ var delayProfile = _delayProfileService.AllForTags(remoteEpisode.Series.Tags).OrderBy(d => d.Order).First();
+
+ return delayProfile.GetProtocolDelay(remoteEpisode.Release.DownloadProtocol);
+ }
+
public void Handle(SeriesDeletedEvent message)
{
_repository.DeleteBySeriesId(message.Series.Id);
diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj
index e4e2230c4..876f10c94 100644
--- a/src/NzbDrone.Core/NzbDrone.Core.csproj
+++ b/src/NzbDrone.Core/NzbDrone.Core.csproj
@@ -230,6 +230,7 @@
+
@@ -623,6 +624,10 @@
+
+
+
+
@@ -738,11 +743,10 @@
-
-
+
diff --git a/src/NzbDrone.Core/Profiles/Delay/DelayProfile.cs b/src/NzbDrone.Core/Profiles/Delay/DelayProfile.cs
new file mode 100644
index 000000000..65f403b5b
--- /dev/null
+++ b/src/NzbDrone.Core/Profiles/Delay/DelayProfile.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using NzbDrone.Core.Datastore;
+using NzbDrone.Core.Indexers;
+
+namespace NzbDrone.Core.Profiles.Delay
+{
+ public class DelayProfile : ModelBase
+ {
+ public DownloadProtocol PreferredProtocol { get; set; }
+ public int UsenetDelay { get; set; }
+ public int TorrentDelay { get; set; }
+ public int Order { get; set; }
+ public HashSet Tags { get; set; }
+
+ public DelayProfile()
+ {
+ Tags = new HashSet();
+ }
+
+ public int GetProtocolDelay(DownloadProtocol protocol)
+ {
+ return protocol == DownloadProtocol.Torrent ? TorrentDelay : UsenetDelay;
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Profiles/Delay/DelayProfileRepository.cs b/src/NzbDrone.Core/Profiles/Delay/DelayProfileRepository.cs
new file mode 100644
index 000000000..a6198015d
--- /dev/null
+++ b/src/NzbDrone.Core/Profiles/Delay/DelayProfileRepository.cs
@@ -0,0 +1,18 @@
+using NzbDrone.Core.Datastore;
+using NzbDrone.Core.Messaging.Events;
+
+namespace NzbDrone.Core.Profiles.Delay
+{
+ public interface IDelayProfileRepository : IBasicRepository
+ {
+
+ }
+
+ public class DelayProfileRepository : BasicRepository, IDelayProfileRepository
+ {
+ public DelayProfileRepository(IDatabase database, IEventAggregator eventAggregator)
+ : base(database, eventAggregator)
+ {
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Profiles/Delay/DelayProfileService.cs b/src/NzbDrone.Core/Profiles/Delay/DelayProfileService.cs
new file mode 100644
index 000000000..d6ff9a1d2
--- /dev/null
+++ b/src/NzbDrone.Core/Profiles/Delay/DelayProfileService.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NzbDrone.Common;
+using NzbDrone.Common.Extensions;
+
+namespace NzbDrone.Core.Profiles.Delay
+{
+ public interface IDelayProfileService
+ {
+ DelayProfile Add(DelayProfile profile);
+ DelayProfile Update(DelayProfile profile);
+ void Delete(int id);
+ List All();
+ DelayProfile Get(int id);
+ List AllForTags(HashSet tagIds);
+ DelayProfile BestForTags(HashSet tagIds);
+ }
+
+ public class DelayProfileService : IDelayProfileService
+ {
+ private readonly IDelayProfileRepository _repo;
+
+ public DelayProfileService(IDelayProfileRepository repo)
+ {
+ _repo = repo;
+ }
+
+ public DelayProfile Add(DelayProfile profile)
+ {
+ return _repo.Insert(profile);
+ }
+
+ public DelayProfile Update(DelayProfile profile)
+ {
+ return _repo.Update(profile);
+ }
+
+ public void Delete(int id)
+ {
+ _repo.Delete(id);
+
+ var all = All().OrderBy(d => d.Order).ToList();
+
+ for (int i = 0; i < all.Count; i++)
+ {
+ if (all[i].Id == 1) continue;
+
+ all[i].Order = i + 1;
+ }
+
+ _repo.UpdateMany(all);
+ }
+
+ public List All()
+ {
+ return _repo.All().ToList();
+ }
+
+ public DelayProfile Get(int id)
+ {
+ return _repo.Get(id);
+ }
+
+ public List AllForTags(HashSet tagIds)
+ {
+ return _repo.All().Where(r => r.Tags.Intersect(tagIds).Any() || r.Tags.Empty()).ToList();
+ }
+
+ public DelayProfile BestForTags(HashSet tagIds)
+ {
+ return _repo.All().Where(r => r.Tags.Intersect(tagIds).Any() || r.Tags.Empty())
+ .OrderBy(d => d.Order).First();
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Profiles/Delay/DelayProfileTagInUseValidator.cs b/src/NzbDrone.Core/Profiles/Delay/DelayProfileTagInUseValidator.cs
new file mode 100644
index 000000000..80cb89294
--- /dev/null
+++ b/src/NzbDrone.Core/Profiles/Delay/DelayProfileTagInUseValidator.cs
@@ -0,0 +1,34 @@
+using System.Collections.Generic;
+using System.Linq;
+using FluentValidation.Validators;
+using NzbDrone.Common;
+using NzbDrone.Common.Extensions;
+using Omu.ValueInjecter;
+
+namespace NzbDrone.Core.Profiles.Delay
+{
+ public class DelayProfileTagInUseValidator : PropertyValidator
+ {
+ private readonly IDelayProfileService _delayProfileService;
+
+ public DelayProfileTagInUseValidator(IDelayProfileService delayProfileService)
+ : base("One or more tags is used in another profile")
+ {
+ _delayProfileService = delayProfileService;
+ }
+
+ protected override bool IsValid(PropertyValidatorContext context)
+ {
+ if (context.PropertyValue == null) return true;
+
+ var delayProfile = new DelayProfile();
+ delayProfile.InjectFrom(context.ParentContext.InstanceToValidate);
+
+ var collection = context.PropertyValue as HashSet;
+
+ if (collection == null || collection.Empty()) return true;
+
+ return _delayProfileService.All().None(d => d.Id != delayProfile.Id && d.Tags.Intersect(collection).Any());
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Profiles/GrabDelayMode.cs b/src/NzbDrone.Core/Profiles/GrabDelayMode.cs
deleted file mode 100644
index 146e68894..000000000
--- a/src/NzbDrone.Core/Profiles/GrabDelayMode.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace NzbDrone.Core.Profiles
-{
- public enum GrabDelayMode
- {
- First = 0,
- Cutoff = 1,
- Always = 2
- }
-}
diff --git a/src/NzbDrone.Core/Profiles/Profile.cs b/src/NzbDrone.Core/Profiles/Profile.cs
index 914f49815..55a3a302b 100644
--- a/src/NzbDrone.Core/Profiles/Profile.cs
+++ b/src/NzbDrone.Core/Profiles/Profile.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Parser;
using NzbDrone.Core.Qualities;
@@ -12,7 +13,10 @@ namespace NzbDrone.Core.Profiles
public Quality Cutoff { get; set; }
public List Items { get; set; }
public Language Language { get; set; }
- public Int32 GrabDelay { get; set; }
- public GrabDelayMode GrabDelayMode { get; set; }
+
+ public Quality LastAllowedQuality()
+ {
+ return Items.Last(q => q.Allowed).Quality;
+ }
}
-}
\ No newline at end of file
+}
diff --git a/src/NzbDrone.sln.DotSettings b/src/NzbDrone.sln.DotSettings
index 088eea7f9..cbb36a9fe 100644
--- a/src/NzbDrone.sln.DotSettings
+++ b/src/NzbDrone.sln.DotSettings
@@ -9,6 +9,8 @@
ERRORHINTWARNING
+ WARNING
+ WARNINGWARNINGHINTTrue
diff --git a/src/UI/Mixins/AsEditModalView.js b/src/UI/Mixins/AsEditModalView.js
index bda5d10f1..c33df00eb 100644
--- a/src/UI/Mixins/AsEditModalView.js
+++ b/src/UI/Mixins/AsEditModalView.js
@@ -26,7 +26,7 @@ define(
});
promise.done(function () {
- self.originalModelData = self.model.toJSON();
+ self.originalModelData = JSON.stringify(self.model.toJSON());
});
return promise;
@@ -38,7 +38,7 @@ define(
throw 'View has no model';
}
- this.originalModelData = this.model.toJSON();
+ this.originalModelData = JSON.stringify(this.model.toJSON());
this.events = this.events || {};
this.events['click .x-save'] = '_save';
@@ -63,8 +63,6 @@ define(
if (self._onAfterSave) {
self._onAfterSave.call(self);
}
-
- self.originalModelData = self.model.toJSON();
});
};
@@ -96,7 +94,7 @@ define(
};
this.prototype.onBeforeClose = function () {
- this.model.set(this.originalModelData);
+ this.model.set(JSON.parse(this.originalModelData));
if (originalOnBeforeClose) {
originalOnBeforeClose.call(this);
diff --git a/src/UI/Mixins/AsSortedCollectionView.js b/src/UI/Mixins/AsSortedCollectionView.js
new file mode 100644
index 000000000..c96d9538d
--- /dev/null
+++ b/src/UI/Mixins/AsSortedCollectionView.js
@@ -0,0 +1,32 @@
+'use strict';
+
+define(
+ function () {
+
+ return function () {
+
+ this.prototype.appendHtml = function(collectionView, itemView, index) {
+ var childrenContainer = collectionView.itemViewContainer ? collectionView.$(collectionView.itemViewContainer) : collectionView.$el;
+ var collection = collectionView.collection;
+
+ // If the index of the model is at the end of the collection append, else insert at proper index
+ if (index >= collection.size() - 1) {
+ childrenContainer.append(itemView.el);
+ } else {
+ var previousModel = collection.at(index + 1);
+ var previousView = this.children.findByModel(previousModel);
+
+ if (previousView) {
+ previousView.$el.before(itemView.$el);
+ }
+
+ else {
+ childrenContainer.append(itemView.el);
+ }
+ }
+ };
+
+ return this;
+ };
+ }
+);
diff --git a/src/UI/Series/Details/SeasonCollectionView.js b/src/UI/Series/Details/SeasonCollectionView.js
index ab165d6de..70707912c 100644
--- a/src/UI/Series/Details/SeasonCollectionView.js
+++ b/src/UI/Series/Details/SeasonCollectionView.js
@@ -1,11 +1,12 @@
'use strict';
define(
[
+ 'underscore',
'marionette',
'Series/Details/SeasonLayout',
- 'underscore'
- ], function (Marionette, SeasonLayout, _) {
- return Marionette.CollectionView.extend({
+ 'Mixins/AsSortedCollectionView'
+ ], function (_, Marionette, SeasonLayout, AsSortedCollectionView) {
+ var view = Marionette.CollectionView.extend({
itemView: SeasonLayout,
@@ -19,27 +20,6 @@ define(
this.series = options.series;
},
- appendHtml: function(collectionView, itemView, index) {
- var childrenContainer = collectionView.itemViewContainer ? collectionView.$(collectionView.itemViewContainer) : collectionView.$el;
- var collection = collectionView.collection;
-
- // If the index of the model is at the end of the collection append, else insert at proper index
- if (index >= collection.size() - 1) {
- childrenContainer.append(itemView.el);
- } else {
- var previousModel = collection.at(index + 1);
- var previousView = this.children.findByModel(previousModel);
-
- if (previousView) {
- previousView.$el.before(itemView.$el);
- }
-
- else {
- childrenContainer.append(itemView.el);
- }
- }
- },
-
itemViewOptions: function () {
return {
episodeCollection: this.episodeCollection,
@@ -62,4 +42,8 @@ define(
this.render();
}
});
+
+ AsSortedCollectionView.call(view);
+
+ return view;
});
diff --git a/src/UI/Series/Details/SeasonLayout.js b/src/UI/Series/Details/SeasonLayout.js
index 6aea66c09..8d0b229fd 100644
--- a/src/UI/Series/Details/SeasonLayout.js
+++ b/src/UI/Series/Details/SeasonLayout.js
@@ -99,6 +99,27 @@ define(
}
],
+ templateHelpers: function () {
+
+ var episodeCount = this.episodeCollection.filter(function (episode) {
+ return episode.get('hasFile') || (episode.get('monitored') && moment(episode.get('airDateUtc')).isBefore(moment()));
+ }).length;
+
+ var episodeFileCount = this.episodeCollection.where({ hasFile: true }).length;
+ var percentOfEpisodes = 100;
+
+ if (episodeCount > 0) {
+ percentOfEpisodes = episodeFileCount / episodeCount * 100;
+ }
+
+ return {
+ showingEpisodes : this.showingEpisodes,
+ episodeCount : episodeCount,
+ episodeFileCount : episodeFileCount,
+ percentOfEpisodes: percentOfEpisodes
+ };
+ },
+
initialize: function (options) {
if (!options.episodeCollection) {
@@ -229,27 +250,6 @@ define(
});
},
- templateHelpers: function () {
-
- var episodeCount = this.episodeCollection.filter(function (episode) {
- return episode.get('hasFile') || (episode.get('monitored') && moment(episode.get('airDateUtc')).isBefore(moment()));
- }).length;
-
- var episodeFileCount = this.episodeCollection.where({ hasFile: true }).length;
- var percentOfEpisodes = 100;
-
- if (episodeCount > 0) {
- percentOfEpisodes = episodeFileCount / episodeCount * 100;
- }
-
- return {
- showingEpisodes : this.showingEpisodes,
- episodeCount : episodeCount,
- episodeFileCount : episodeFileCount,
- percentOfEpisodes: percentOfEpisodes
- };
- },
-
_showHideEpisodes: function () {
if (this.showingEpisodes) {
this.showingEpisodes = false;
diff --git a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemViewTemplate.hbs b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemViewTemplate.hbs
index 2aecc5417..2796de5b1 100644
--- a/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemViewTemplate.hbs
+++ b/src/UI/Settings/DownloadClient/RemotePathMapping/RemotePathMappingItemViewTemplate.hbs
@@ -1,12 +1,12 @@
-
-
{{host}}
-
-
-
{{remotePath}}
-
-
-
{{localPath}}
-
-
+
+ {{host}}
+
+
+ {{remotePath}}
+
+
+ {{localPath}}
+
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/UI/Settings/Indexers/Restriction/RestrictionItemViewTemplate.hbs b/src/UI/Settings/Indexers/Restriction/RestrictionItemViewTemplate.hbs
index 55c414af1..0933f49d1 100644
--- a/src/UI/Settings/Indexers/Restriction/RestrictionItemViewTemplate.hbs
+++ b/src/UI/Settings/Indexers/Restriction/RestrictionItemViewTemplate.hbs
@@ -1,12 +1,12 @@
-
+