diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj
index 62f2c41ff..af1b5fe55 100644
--- a/src/NzbDrone.Api/NzbDrone.Api.csproj
+++ b/src/NzbDrone.Api/NzbDrone.Api.csproj
@@ -98,6 +98,9 @@
+
+
+
@@ -200,6 +203,7 @@
+
@@ -221,6 +225,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..f18bdf6ef
--- /dev/null
+++ b/src/NzbDrone.Api/Profiles/Delay/DelayProfileModule.cs
@@ -0,0 +1,65 @@
+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);
+ SharedValidator.RuleFor(d => d.UsenetDelay).GreaterThanOrEqualTo(0);
+ SharedValidator.RuleFor(d => d.TorrentDelay).GreaterThanOrEqualTo(0);
+ SharedValidator.RuleFor(d => d.Id).SetValidator(new DelayProfileValidator());
+ }
+
+ 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..bbc2fc67f
--- /dev/null
+++ b/src/NzbDrone.Api/Profiles/Delay/DelayProfileResource.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+using NzbDrone.Api.REST;
+using NzbDrone.Core.Indexers;
+
+namespace NzbDrone.Api.Profiles.Delay
+{
+ public class DelayProfileResource : RestResource
+ {
+ public bool EnableUsenet { get; set; }
+ public bool EnableTorrent { get; set; }
+ 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/Delay/DelayProfileValidator.cs b/src/NzbDrone.Api/Profiles/Delay/DelayProfileValidator.cs
new file mode 100644
index 000000000..b854d87a2
--- /dev/null
+++ b/src/NzbDrone.Api/Profiles/Delay/DelayProfileValidator.cs
@@ -0,0 +1,27 @@
+using FluentValidation.Validators;
+using NzbDrone.Core.Profiles.Delay;
+using Omu.ValueInjecter;
+
+namespace NzbDrone.Api.Profiles.Delay
+{
+ public class DelayProfileValidator : PropertyValidator
+ {
+ public DelayProfileValidator()
+ : base("Usenet or Torrent must be enabled")
+ {
+ }
+
+ protected override bool IsValid(PropertyValidatorContext context)
+ {
+ var delayProfile = new DelayProfile();
+ delayProfile.InjectFrom(context.ParentContext.InstanceToValidate);
+
+ if (!delayProfile.EnableUsenet && !delayProfile.EnableTorrent)
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
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/Datastore/Migration/070_delay_profileFixture.cs b/src/NzbDrone.Core.Test/Datastore/Migration/070_delay_profileFixture.cs
new file mode 100644
index 000000000..7f0cf4d1b
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Datastore/Migration/070_delay_profileFixture.cs
@@ -0,0 +1,107 @@
+using System.Collections.Generic;
+using System.Linq;
+using FluentAssertions;
+using NUnit.Framework;
+using NzbDrone.Core.Datastore.Migration;
+using NzbDrone.Core.Indexers;
+using NzbDrone.Core.Profiles.Delay;
+using NzbDrone.Core.Tags;
+using NzbDrone.Core.Test.Framework;
+using NzbDrone.Core.Tv;
+
+namespace NzbDrone.Core.Test.Datastore.Migration
+{
+ [TestFixture]
+ public class delay_profileFixture : MigrationTest
+ {
+ [Test]
+ public void should_migrate_old_delays()
+ {
+ WithTestDb(c =>
+ {
+ c.Insert.IntoTable("Profiles").Row(new
+ {
+ GrabDelay = 1,
+ Name = "OneHour",
+ Cutoff = "{}",
+ Items = "{}"
+ });
+
+ c.Insert.IntoTable("Profiles").Row(new
+ {
+ GrabDelay = 2,
+ Name = "TwoHours",
+ Cutoff = "{}",
+ Items = "[]"
+ });
+ });
+
+ var allProfiles = Mocker.Resolve().All().ToList();
+
+ allProfiles.Should().HaveCount(3);
+ allProfiles.Should().OnlyContain(c => c.PreferredProtocol == DownloadProtocol.Usenet);
+ allProfiles.Should().OnlyContain(c => c.TorrentDelay == 0);
+ allProfiles.Should().Contain(c => c.UsenetDelay == 60);
+ allProfiles.Should().Contain(c => c.UsenetDelay == 120);
+ }
+
+ [Test]
+ public void should_create_tag_for_delay_profile()
+ {
+ WithTestDb(c =>
+ c.Insert.IntoTable("Profiles").Row(new
+ {
+ GrabDelay = 1,
+ Name = "OneHour",
+ Cutoff = "{}",
+ Items = "{}"
+ })
+ );
+
+ var tags = Mocker.Resolve().All().ToList();
+
+ tags.Should().HaveCount(1);
+ tags.First().Label.Should().Be("delay-60");
+ }
+
+ [Test]
+ public void should_add_tag_to_series_that_had_a_profile_with_delay_attached()
+ {
+ WithTestDb(c =>
+ {
+ c.Insert.IntoTable("Profiles").Row(new
+ {
+ GrabDelay = 1,
+ Name = "OneHour",
+ Cutoff = "{}",
+ Items = "{}"
+ });
+
+ c.Insert.IntoTable("Series").Row(new
+ {
+ TvdbId = 0,
+ TvRageId = 0,
+ Title = "Series",
+ TitleSlug = "series",
+ CleanTitle = "series",
+ Status = 0,
+ Images = "[]",
+ Path = @"C:\Test\Series",
+ Monitored = 1,
+ SeasonFolder = 1,
+ RunTime = 0,
+ SeriesType = 0,
+ UseSceneNumbering = 0,
+ Tags = "[1]"
+ });
+ });
+
+ var tag = Mocker.Resolve().All().ToList().First();
+ var series = Mocker.Resolve().All().ToList();
+
+ series.Should().HaveCount(1);
+ series.First().Tags.Should().HaveCount(1);
+ series.First().Tags.First().Should().Be(tag.Id);
+ }
+ }
+}
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/ProtocolSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/ProtocolSpecificationFixture.cs
new file mode 100644
index 000000000..4bfaf34dc
--- /dev/null
+++ b/src/NzbDrone.Core.Test/DecisionEngineTests/ProtocolSpecificationFixture.cs
@@ -0,0 +1,75 @@
+using System.Collections.Generic;
+using FluentAssertions;
+using Moq;
+using NUnit.Framework;
+using NzbDrone.Core.DecisionEngine.Specifications;
+using NzbDrone.Core.Indexers;
+using NzbDrone.Core.Parser.Model;
+using NzbDrone.Core.Profiles.Delay;
+using NzbDrone.Core.Test.Framework;
+using NzbDrone.Core.Tv;
+
+namespace NzbDrone.Core.Test.DecisionEngineTests
+{
+ [TestFixture]
+ public class ProtocolSpecificationFixture : CoreTest
+ {
+ private RemoteEpisode _remoteEpisode;
+ private DelayProfile _delayProfile;
+
+ [SetUp]
+ public void Setup()
+ {
+ _remoteEpisode = new RemoteEpisode();
+ _remoteEpisode.Release = new ReleaseInfo();
+ _remoteEpisode.Series = new Series();
+
+ _delayProfile = new DelayProfile();
+
+ Mocker.GetMock()
+ .Setup(s => s.BestForTags(It.IsAny>()))
+ .Returns(_delayProfile);
+ }
+
+ private void GivenProtocol(DownloadProtocol downloadProtocol)
+ {
+ _remoteEpisode.Release.DownloadProtocol = downloadProtocol;
+ }
+
+ [Test]
+ public void should_be_true_if_usenet_and_usenet_is_enabled()
+ {
+ GivenProtocol(DownloadProtocol.Usenet);
+ _delayProfile.EnableUsenet = true;
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().Be(true);
+ }
+
+ [Test]
+ public void should_be_true_if_torrent_and_torrent_is_enabled()
+ {
+ GivenProtocol(DownloadProtocol.Torrent);
+ _delayProfile.EnableTorrent = true;
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().Be(true);
+ }
+
+ [Test]
+ public void should_be_false_if_usenet_and_usenet_is_disabled()
+ {
+ GivenProtocol(DownloadProtocol.Usenet);
+ _delayProfile.EnableUsenet = false;
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().Be(false);
+ }
+
+ [Test]
+ public void should_be_false_if_torrent_and_torrent_is_disabled()
+ {
+ GivenProtocol(DownloadProtocol.Torrent);
+ _delayProfile.EnableTorrent = false;
+
+ Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().Be(false);
+ }
+ }
+}
\ No newline at end of file
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.Test/Framework/DbTest.cs b/src/NzbDrone.Core.Test/Framework/DbTest.cs
index be9ffed86..3e81b68ad 100644
--- a/src/NzbDrone.Core.Test/Framework/DbTest.cs
+++ b/src/NzbDrone.Core.Test/Framework/DbTest.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using FluentMigrator;
using FluentMigrator.Runner;
using Marr.Data;
using Moq;
@@ -13,7 +14,6 @@ using NzbDrone.Core.Messaging.Events;
namespace NzbDrone.Core.Test.Framework
{
-
public abstract class DbTest : DbTest
where TSubject : class
where TModel : ModelBase, new()
@@ -85,27 +85,34 @@ namespace NzbDrone.Core.Test.Framework
}
}
- private void WithTestDb()
+ protected virtual TestDatabase WithTestDb(Action beforeMigration)
{
- WithTempAsAppPath();
+ var factory = Mocker.Resolve();
+ var database = factory.Create(MigrationType);
+ Mocker.SetConstant(database);
+
+ var testDb = new TestDatabase(database);
+
+ return testDb;
+ }
+ protected void SetupContainer()
+ {
+ WithTempAsAppPath();
+
Mocker.SetConstant(Mocker.Resolve());
Mocker.SetConstant(Mocker.Resolve());
Mocker.SetConstant(Mocker.Resolve());
MapRepository.Instance.EnableTraceLogging = true;
-
- var factory = Mocker.Resolve();
- var _database = factory.Create(MigrationType);
- _db = new TestDatabase(_database);
- Mocker.SetConstant(_database);
}
[SetUp]
- public void SetupReadDb()
+ public virtual void SetupDb()
{
- WithTestDb();
+ SetupContainer();
+ _db = WithTestDb(null);
}
[TearDown]
diff --git a/src/NzbDrone.Core.Test/Framework/MigrationTest.cs b/src/NzbDrone.Core.Test/Framework/MigrationTest.cs
new file mode 100644
index 000000000..137f41983
--- /dev/null
+++ b/src/NzbDrone.Core.Test/Framework/MigrationTest.cs
@@ -0,0 +1,36 @@
+using System;
+using FluentMigrator;
+using NUnit.Framework;
+using NzbDrone.Core.Datastore;
+
+namespace NzbDrone.Core.Test.Framework
+{
+ [Category("DbMigrationTest")]
+ [Category("DbTest")]
+ public abstract class MigrationTest : DbTest where TMigration : MigrationBase
+ {
+ protected override TestDatabase WithTestDb(Action beforeMigration)
+ {
+ var factory = Mocker.Resolve();
+
+ var database = factory.Create(MigrationType, m =>
+ {
+ if (m.GetType() == typeof(TMigration))
+ {
+ beforeMigration(m);
+ }
+ });
+
+ var testDb = new TestDatabase(database);
+ Mocker.SetConstant(database);
+
+ return testDb;
+ }
+
+ [SetUp]
+ public override void SetupDb()
+ {
+ SetupContainer();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj
index 6f4bb2718..02cbab637 100644
--- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj
+++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj
@@ -116,6 +116,7 @@
+
@@ -123,6 +124,7 @@
+
@@ -157,6 +159,7 @@
+
diff --git a/src/NzbDrone.Core/Datastore/DbFactory.cs b/src/NzbDrone.Core/Datastore/DbFactory.cs
index df7889d83..e4fffed2f 100644
--- a/src/NzbDrone.Core/Datastore/DbFactory.cs
+++ b/src/NzbDrone.Core/Datastore/DbFactory.cs
@@ -12,7 +12,7 @@ namespace NzbDrone.Core.Datastore
{
public interface IDbFactory
{
- IDatabase Create(MigrationType migrationType = MigrationType.Main);
+ IDatabase Create(MigrationType migrationType = MigrationType.Main, Action beforeMigration = null);
}
public class DbFactory : IDbFactory
@@ -43,7 +43,7 @@ namespace NzbDrone.Core.Datastore
_connectionStringFactory = connectionStringFactory;
}
- public IDatabase Create(MigrationType migrationType = MigrationType.Main)
+ public IDatabase Create(MigrationType migrationType = MigrationType.Main, Action beforeMigration = null)
{
string connectionString;
@@ -66,7 +66,7 @@ namespace NzbDrone.Core.Datastore
}
}
- _migrationController.MigrateToLatest(connectionString, migrationType);
+ _migrationController.MigrateToLatest(connectionString, migrationType, beforeMigration);
var db = new Database(() =>
{
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..8ef186064
--- /dev/null
+++ b/src/NzbDrone.Core/Datastore/Migration/070_delay_profile.cs
@@ -0,0 +1,165 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using FluentMigrator;
+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("EnableUsenet").AsBoolean().NotNullable()
+ .WithColumn("EnableTorrent").AsBoolean().NotNullable()
+ .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
+ {
+ EnableUsenet = 1,
+ EnableTorrent = 1,
+ 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 (EnableUsenet, EnableTorrent, PreferredProtocol, TorrentDelay, UsenetDelay, [Order], Tags) VALUES (1, 1, 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/Migration/Framework/MigrationContext.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationContext.cs
index e1bb47bcf..0d49450af 100644
--- a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationContext.cs
+++ b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationContext.cs
@@ -1,7 +1,18 @@
-namespace NzbDrone.Core.Datastore.Migration.Framework
+using System;
+
+namespace NzbDrone.Core.Datastore.Migration.Framework
{
public class MigrationContext
{
- public MigrationType MigrationType { get; set; }
+ public MigrationType MigrationType { get; private set; }
+
+ public Action BeforeMigration { get; private set; }
+
+ public MigrationContext(MigrationType migrationType, Action beforeAction)
+ {
+ MigrationType = migrationType;
+
+ BeforeMigration = beforeAction;
+ }
}
}
\ No newline at end of file
diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs
index 4c55762b8..a505078fe 100644
--- a/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs
+++ b/src/NzbDrone.Core/Datastore/Migration/Framework/MigrationController.cs
@@ -1,14 +1,14 @@
-using System.Diagnostics;
+using System;
+using System.Diagnostics;
using System.Reflection;
using FluentMigrator.Runner;
using FluentMigrator.Runner.Initialization;
-using FluentMigrator.Runner.Processors.SQLite;
namespace NzbDrone.Core.Datastore.Migration.Framework
{
public interface IMigrationController
{
- void MigrateToLatest(string connectionString, MigrationType migrationType);
+ void MigrateToLatest(string connectionString, MigrationType migrationType, Action beforeMigration);
}
public class MigrationController : IMigrationController
@@ -20,7 +20,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
_announcer = announcer;
}
- public void MigrateToLatest(string connectionString, MigrationType migrationType)
+ public void MigrateToLatest(string connectionString, MigrationType migrationType, Action beforeMigration)
{
var sw = Stopwatch.StartNew();
@@ -31,10 +31,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
var migrationContext = new RunnerContext(_announcer)
{
Namespace = "NzbDrone.Core.Datastore.Migration",
- ApplicationContext = new MigrationContext
- {
- MigrationType = migrationType
- }
+ ApplicationContext = new MigrationContext(migrationType, beforeMigration)
};
var options = new MigrationOptions { PreviewOnly = false, Timeout = 60 };
@@ -45,7 +42,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
sw.Stop();
- _announcer.ElapsedTime(sw.Elapsed);
+ _announcer.ElapsedTime(sw.Elapsed);
}
}
}
diff --git a/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs b/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs
index c2fca697a..a2144f0ec 100644
--- a/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs
+++ b/src/NzbDrone.Core/Datastore/Migration/Framework/NzbDroneMigrationBase.cs
@@ -1,4 +1,5 @@
using System;
+using FluentMigrator;
using NLog;
using NzbDrone.Common.Instrumentation;
@@ -7,6 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
public abstract class NzbDroneMigrationBase : FluentMigrator.Migration
{
protected readonly Logger _logger;
+ private MigrationContext _migrationContext;
protected NzbDroneMigrationBase()
{
@@ -21,11 +23,36 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
{
}
+ public int Version
+ {
+ get
+ {
+ var migrationAttribute = (MigrationAttribute)Attribute.GetCustomAttribute(GetType(), typeof(MigrationAttribute));
+ return (int)migrationAttribute.Version;
+ }
+ }
+
+ public MigrationContext Context
+ {
+ get
+ {
+ if (_migrationContext == null)
+ {
+ _migrationContext = (MigrationContext)ApplicationContext;
+ }
+ return _migrationContext;
+ }
+ }
+
public override void Up()
{
- var context = (MigrationContext)ApplicationContext;
- switch (context.MigrationType)
+ if (Context.BeforeMigration != null)
+ {
+ Context.BeforeMigration(this);
+ }
+
+ switch (Context.MigrationType)
{
case MigrationType.Main:
MainDbUpgrade();
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/ProtocolSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/ProtocolSpecification.cs
new file mode 100644
index 000000000..00f9e0083
--- /dev/null
+++ b/src/NzbDrone.Core/DecisionEngine/Specifications/ProtocolSpecification.cs
@@ -0,0 +1,45 @@
+using NLog;
+using NzbDrone.Core.Download.Pending;
+using NzbDrone.Core.Indexers;
+using NzbDrone.Core.IndexerSearch.Definitions;
+using NzbDrone.Core.Parser.Model;
+using NzbDrone.Core.Profiles.Delay;
+
+namespace NzbDrone.Core.DecisionEngine.Specifications
+{
+ public class ProtocolSpecification : IDecisionEngineSpecification
+ {
+ private readonly IPendingReleaseService _pendingReleaseService;
+ private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification;
+ private readonly IDelayProfileService _delayProfileService;
+ private readonly Logger _logger;
+
+ public ProtocolSpecification(IDelayProfileService delayProfileService,
+ Logger logger)
+ {
+ _delayProfileService = delayProfileService;
+ _logger = logger;
+ }
+
+ public RejectionType Type { get { return RejectionType.Temporary; } }
+
+ public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
+ {
+ var delayProfile = _delayProfileService.BestForTags(subject.Series.Tags);
+
+ if (subject.Release.DownloadProtocol == DownloadProtocol.Usenet && !delayProfile.EnableUsenet)
+ {
+ _logger.Debug("[{0}] Usenet is not enabled for this series", subject.Release.Title);
+ return Decision.Reject("Usenet is not enabled for this series");
+ }
+
+ if (subject.Release.DownloadProtocol == DownloadProtocol.Torrent && !delayProfile.EnableTorrent)
+ {
+ _logger.Debug("[{0}] Torrent is not enabled for this series", subject.Release.Title);
+ return Decision.Reject("Torrent is not enabled for this series");
+ }
+
+ return Decision.Accept();
+ }
+ }
+}
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..b0a532c18 100644
--- a/src/NzbDrone.Core/NzbDrone.Core.csproj
+++ b/src/NzbDrone.Core/NzbDrone.Core.csproj
@@ -230,6 +230,7 @@
+
@@ -260,6 +261,7 @@
+
@@ -623,6 +625,10 @@
+
+
+
+
@@ -738,11 +744,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..ef20bb6a5
--- /dev/null
+++ b/src/NzbDrone.Core/Profiles/Delay/DelayProfile.cs
@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+using NzbDrone.Core.Datastore;
+using NzbDrone.Core.Indexers;
+
+namespace NzbDrone.Core.Profiles.Delay
+{
+ public class DelayProfile : ModelBase
+ {
+ public bool EnableUsenet { get; set; }
+ public bool EnableTorrent { get; set; }
+ 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..3a5d2a5c7
--- /dev/null
+++ b/src/NzbDrone.Core/Profiles/Delay/DelayProfileTagInUseValidator.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using System.Linq;
+using FluentValidation.Validators;
+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/Mixins/TagInput.js b/src/UI/Mixins/TagInput.js
index 9c6dd81dd..a03730532 100644
--- a/src/UI/Mixins/TagInput.js
+++ b/src/UI/Mixins/TagInput.js
@@ -26,22 +26,27 @@ define(
if (existing) {
originalAdd.call(this, existing, dontPushVal);
- return;
}
- var newTag = new TagModel();
- newTag.set({ label: item.toLowerCase() });
- TagCollection.add(newTag);
+ else {
+ var newTag = new TagModel();
+ newTag.set({ label: item.toLowerCase() });
+ TagCollection.add(newTag);
- newTag.save().done(function () {
- item = newTag.toJSON();
- originalAdd.call(self, item, dontPushVal);
- });
+ newTag.save().done(function () {
+ item = newTag.toJSON();
+ originalAdd.call(self, item, dontPushVal);
+ });
+ }
}
else {
originalAdd.call(this, item, dontPushVal);
}
+
+ if (this.options.tag) {
+ self.$input.typeahead('val', '');
+ }
};
$.fn.tagsinput.Constructor.prototype.remove = function (item, dontPushVal) {
@@ -69,6 +74,11 @@ define(
}
});
+ self.$input.on('focusout', function () {
+ self.add(self.$input.val());
+ self.$input.val('');
+ });
+
originalBuild.call(this, options);
};
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 @@
-
+