mirror of
https://github.com/Sonarr/Sonarr.git
synced 2024-12-16 11:37:58 +02:00
commit
81794c3200
@ -98,6 +98,9 @@
|
||||
<Compile Include="Commands\CommandResource.cs" />
|
||||
<Compile Include="Extensions\AccessControlHeaders.cs" />
|
||||
<Compile Include="Extensions\Pipelines\CorsPipeline.cs" />
|
||||
<Compile Include="Profiles\Delay\DelayProfileModule.cs" />
|
||||
<Compile Include="Profiles\Delay\DelayProfileResource.cs" />
|
||||
<Compile Include="Profiles\Delay\DelayProfileValidator.cs" />
|
||||
<Compile Include="RemotePathMappings\RemotePathMappingModule.cs" />
|
||||
<Compile Include="RemotePathMappings\RemotePathMappingResource.cs" />
|
||||
<Compile Include="Config\UiConfigModule.cs" />
|
||||
@ -200,6 +203,7 @@
|
||||
<Compile Include="Restrictions\RestrictionModule.cs" />
|
||||
<Compile Include="Restrictions\RestrictionResource.cs" />
|
||||
<Compile Include="REST\BadRequestException.cs" />
|
||||
<Compile Include="REST\MethodNotAllowedException.cs" />
|
||||
<Compile Include="REST\ResourceValidator.cs" />
|
||||
<Compile Include="REST\RestModule.cs" />
|
||||
<Compile Include="REST\RestResource.cs" />
|
||||
@ -221,6 +225,7 @@
|
||||
<Compile Include="TinyIoCNancyBootstrapper.cs" />
|
||||
<Compile Include="Update\UpdateModule.cs" />
|
||||
<Compile Include="Update\UpdateResource.cs" />
|
||||
<Compile Include="Validation\EmptyCollectionValidator.cs" />
|
||||
<Compile Include="Validation\RuleBuilderExtensions.cs" />
|
||||
<Compile Include="Wanted\CutoffModule.cs" />
|
||||
<Compile Include="Wanted\LegacyMissingModule.cs" />
|
||||
|
65
src/NzbDrone.Api/Profiles/Delay/DelayProfileModule.cs
Normal file
65
src/NzbDrone.Api/Profiles/Delay/DelayProfileModule.cs
Normal file
@ -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<DelayProfileResource>
|
||||
{
|
||||
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<DelayProfile>();
|
||||
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<DelayProfile>(_delayProfileService.Update, resource);
|
||||
}
|
||||
|
||||
private DelayProfileResource GetById(int id)
|
||||
{
|
||||
return _delayProfileService.Get(id).InjectTo<DelayProfileResource>();
|
||||
}
|
||||
|
||||
private List<DelayProfileResource> GetAll()
|
||||
{
|
||||
return _delayProfileService.All().InjectTo<List<DelayProfileResource>>();
|
||||
}
|
||||
}
|
||||
}
|
17
src/NzbDrone.Api/Profiles/Delay/DelayProfileResource.cs
Normal file
17
src/NzbDrone.Api/Profiles/Delay/DelayProfileResource.cs
Normal file
@ -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<int> Tags { get; set; }
|
||||
}
|
||||
}
|
27
src/NzbDrone.Api/Profiles/Delay/DelayProfileValidator.cs
Normal file
27
src/NzbDrone.Api/Profiles/Delay/DelayProfileValidator.cs
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -46,8 +46,6 @@ private void Update(ProfileResource resource)
|
||||
model.Cutoff = (Quality)resource.Cutoff.Id;
|
||||
model.Items = resource.Items.InjectTo<List<ProfileQualityItem>>();
|
||||
model.Language = resource.Language;
|
||||
model.GrabDelay = resource.GrabDelay;
|
||||
model.GrabDelayMode = resource.GrabDelayMode;
|
||||
|
||||
_profileService.Update(model);
|
||||
}
|
||||
|
@ -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 @@ public class ProfileResource : RestResource
|
||||
public Quality Cutoff { get; set; }
|
||||
public List<ProfileQualityItemResource> Items { get; set; }
|
||||
public Language Language { get; set; }
|
||||
public Int32 GrabDelay { get; set; }
|
||||
public GrabDelayMode GrabDelayMode { get; set; }
|
||||
}
|
||||
|
||||
public class ProfileQualityItemResource : RestResource
|
||||
|
13
src/NzbDrone.Api/REST/MethodNotAllowedException.cs
Normal file
13
src/NzbDrone.Api/REST/MethodNotAllowedException.cs
Normal file
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
23
src/NzbDrone.Api/Validation/EmptyCollectionValidator.cs
Normal file
23
src/NzbDrone.Api/Validation/EmptyCollectionValidator.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentValidation.Validators;
|
||||
using NzbDrone.Common.Extensions;
|
||||
|
||||
namespace NzbDrone.Api.Validation
|
||||
{
|
||||
public class EmptyCollectionValidator<T> : 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<T>;
|
||||
|
||||
return collection != null && collection.Empty();
|
||||
}
|
||||
}
|
||||
}
|
@ -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 @@ public static IRuleBuilderOptions<T, string> NotBlank<T>(this IRuleBuilder<T, st
|
||||
{
|
||||
return ruleBuilder.SetValidator(new NotNullValidator()).SetValidator(new NotEmptyValidator(""));
|
||||
}
|
||||
|
||||
public static IRuleBuilderOptions<T, IEnumerable<TProp>> EmptyCollection<T, TProp>(this IRuleBuilder<T, IEnumerable<TProp>> ruleBuilder)
|
||||
{
|
||||
return ruleBuilder.SetValidator(new EmptyCollectionValidator<TProp>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<delay_profile>
|
||||
{
|
||||
[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<DelayProfileRepository>().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<TagRepository>().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<TagRepository>().All().ToList().First();
|
||||
var series = Mocker.Resolve<SeriesRepository>().All().ToList();
|
||||
|
||||
series.Should().HaveCount(1);
|
||||
series.First().Tags.Should().HaveCount(1);
|
||||
series.First().Tags.First().Should().Be(tag.Id);
|
||||
}
|
||||
}
|
||||
}
|
@ -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<DownloadDecisionPriorizationService>
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
GivenPreferredDownloadProtocol(DownloadProtocol.Usenet);
|
||||
}
|
||||
|
||||
private Episode GivenEpisode(int id)
|
||||
{
|
||||
return Builder<Episode>.CreateNew()
|
||||
@ -25,7 +33,7 @@ private Episode GivenEpisode(int id)
|
||||
.Build();
|
||||
}
|
||||
|
||||
private RemoteEpisode GivenRemoteEpisode(List<Episode> episodes, QualityModel quality, int age = 0, long size = 0)
|
||||
private RemoteEpisode GivenRemoteEpisode(List<Episode> 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 @@ private RemoteEpisode GivenRemoteEpisode(List<Episode> episodes, QualityModel qu
|
||||
remoteEpisode.Release = new ReleaseInfo();
|
||||
remoteEpisode.Release.PublishDate = DateTime.Now.AddDays(-age);
|
||||
remoteEpisode.Release.Size = size;
|
||||
remoteEpisode.Release.DownloadProtocol = downloadProtocol;
|
||||
|
||||
remoteEpisode.Series = Builder<Series>.CreateNew()
|
||||
.With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() })
|
||||
@ -45,6 +54,16 @@ private RemoteEpisode GivenRemoteEpisode(List<Episode> episodes, QualityModel qu
|
||||
return remoteEpisode;
|
||||
}
|
||||
|
||||
private void GivenPreferredDownloadProtocol(DownloadProtocol downloadProtocol)
|
||||
{
|
||||
Mocker.GetMock<IDelayProfileService>()
|
||||
.Setup(s => s.BestForTags(It.IsAny<HashSet<int>>()))
|
||||
.Returns(new DelayProfile
|
||||
{
|
||||
PreferredProtocol = downloadProtocol
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_put_propers_before_non_propers()
|
||||
{
|
||||
@ -148,5 +167,37 @@ public void should_not_throw_if_no_episodes_are_found()
|
||||
|
||||
Subject.PrioritizeDecisions(decisions);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_put_usenet_above_torrent_when_usenet_is_preferred()
|
||||
{
|
||||
GivenPreferredDownloadProtocol(DownloadProtocol.Usenet);
|
||||
|
||||
var remoteEpisode1 = GivenRemoteEpisode(new List<Episode> { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), downloadProtocol: DownloadProtocol.Torrent);
|
||||
var remoteEpisode2 = GivenRemoteEpisode(new List<Episode> { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), downloadProtocol: DownloadProtocol.Usenet);
|
||||
|
||||
var decisions = new List<DownloadDecision>();
|
||||
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<Episode> { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), downloadProtocol: DownloadProtocol.Torrent);
|
||||
var remoteEpisode2 = GivenRemoteEpisode(new List<Episode> { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), downloadProtocol: DownloadProtocol.Usenet);
|
||||
|
||||
var decisions = new List<DownloadDecision>();
|
||||
decisions.Add(new DownloadDecision(remoteEpisode1));
|
||||
decisions.Add(new DownloadDecision(remoteEpisode2));
|
||||
|
||||
var qualifiedReports = Subject.PrioritizeDecisions(decisions);
|
||||
qualifiedReports.First().RemoteEpisode.Release.DownloadProtocol.Should().Be(DownloadProtocol.Torrent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<ProtocolSpecification>
|
||||
{
|
||||
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<IDelayProfileService>()
|
||||
.Setup(s => s.BestForTags(It.IsAny<HashSet<int>>()))
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
@ -15,12 +15,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
[TestFixture]
|
||||
public class ReleaseRestrictionsSpecificationFixture : CoreTest<ReleaseRestrictionsSpecification>
|
||||
{
|
||||
private RemoteEpisode _parseResult;
|
||||
private RemoteEpisode _remoteEpisode;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_parseResult = new RemoteEpisode
|
||||
_remoteEpisode = new RemoteEpisode
|
||||
{
|
||||
Series = new Series
|
||||
{
|
||||
@ -54,7 +54,7 @@ public void should_be_true_when_restrictions_are_empty()
|
||||
.Setup(s => s.AllForTags(It.IsAny<HashSet<Int32>>()))
|
||||
.Returns(new List<Restriction>());
|
||||
|
||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeTrue();
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -62,7 +62,7 @@ public void should_be_true_when_title_contains_one_required_term()
|
||||
{
|
||||
GivenRestictions("WEBRip", null);
|
||||
|
||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeTrue();
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -70,7 +70,7 @@ public void should_be_false_when_title_does_not_contain_any_required_terms()
|
||||
{
|
||||
GivenRestictions("doesnt,exist", null);
|
||||
|
||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeFalse();
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -78,7 +78,7 @@ public void should_be_true_when_title_does_not_contain_any_ignored_terms()
|
||||
{
|
||||
GivenRestictions(null, "ignored");
|
||||
|
||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeTrue();
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -86,7 +86,7 @@ public void should_be_false_when_title_contains_one_anded_ignored_terms()
|
||||
{
|
||||
GivenRestictions(null, "edited");
|
||||
|
||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeFalse();
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[TestCase("EdiTED")]
|
||||
@ -97,7 +97,7 @@ public void should_ignore_case_when_matching_required(String required)
|
||||
{
|
||||
GivenRestictions(required, null);
|
||||
|
||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeTrue();
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("EdiTED")]
|
||||
@ -108,7 +108,22 @@ public void should_ignore_case_when_matching_ignored(String ignored)
|
||||
{
|
||||
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<IRestrictionService>()
|
||||
.Setup(s => s.AllForTags(It.IsAny<HashSet<Int32>>()))
|
||||
.Returns(new List<Restriction>
|
||||
{
|
||||
new Restriction { Required = "x264", Ignored = "www.Speed.cd" }
|
||||
});
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,14 +9,15 @@
|
||||
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<DelaySpecification>
|
||||
{
|
||||
private Profile _profile;
|
||||
private DelayProfile _delayProfile;
|
||||
private RemoteEpisode _remoteEpisode;
|
||||
|
||||
[SetUp]
|
||||
@ -32,6 +34,10 @@ public void Setup()
|
||||
_profile = Builder<Profile>.CreateNew()
|
||||
.Build();
|
||||
|
||||
_delayProfile = Builder<DelayProfile>.CreateNew()
|
||||
.With(d => d.PreferredProtocol = DownloadProtocol.Usenet)
|
||||
.Build();
|
||||
|
||||
var series = Builder<Series>.CreateNew()
|
||||
.With(s => s.Profile = _profile)
|
||||
.Build();
|
||||
@ -46,13 +52,21 @@ public void Setup()
|
||||
_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<Episode>.CreateListOfSize(1).Build().ToList();
|
||||
_remoteEpisode.Episodes.First().EpisodeFileId = 0;
|
||||
|
||||
Mocker.GetMock<IDelayProfileService>()
|
||||
.Setup(s => s.BestForTags(It.IsAny<HashSet<int>>()))
|
||||
.Returns(_delayProfile);
|
||||
|
||||
Mocker.GetMock<IPendingReleaseService>()
|
||||
.Setup(s => s.GetPendingRemoteEpisodes(It.IsAny<int>()))
|
||||
.Returns(new List<RemoteEpisode>());
|
||||
}
|
||||
|
||||
private void GivenExistingFile(QualityModel quality)
|
||||
@ -81,7 +95,7 @@ public void should_be_true_when_search()
|
||||
[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 @@ public void should_be_true_when_release_is_older_than_delay()
|
||||
{
|
||||
_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 @@ public void should_be_false_when_release_is_younger_than_delay()
|
||||
_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 @@ public void should_be_true_when_release_is_a_proper_for_existing_episode()
|
||||
.Setup(s => s.IsRevisionUpgrade(It.IsAny<QualityModel>(), It.IsAny<QualityModel>()))
|
||||
.Returns(true);
|
||||
|
||||
_profile.GrabDelay = 12;
|
||||
_delayProfile.UsenetDelay = 12;
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
@ -147,47 +161,11 @@ public void should_be_true_when_release_is_a_real_for_existing_episode()
|
||||
.Setup(s => s.IsRevisionUpgrade(It.IsAny<QualityModel>(), It.IsAny<QualityModel>()))
|
||||
.Returns(true);
|
||||
|
||||
_profile.GrabDelay = 12;
|
||||
_delayProfile.UsenetDelay = 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;
|
||||
|
||||
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 @@ public void should_be_false_when_release_is_proper_for_existing_episode_of_diffe
|
||||
|
||||
GivenExistingFile(new QualityModel(Quality.SDTV));
|
||||
|
||||
_profile.GrabDelay = 12;
|
||||
_delayProfile.UsenetDelay = 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<IPendingReleaseService>()
|
||||
.Setup(s => s.GetPendingRemoteEpisodes(It.IsAny<Int32>()))
|
||||
.Returns(new List<RemoteEpisode>());
|
||||
|
||||
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<IPendingReleaseService>()
|
||||
.Setup(s => s.GetPendingRemoteEpisodes(It.IsAny<Int32>()))
|
||||
.Returns(new List<RemoteEpisode> { _remoteEpisode.JsonClone() });
|
||||
|
||||
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<IPendingReleaseService>()
|
||||
.Setup(s => s.GetPendingRemoteEpisodes(It.IsAny<Int32>()))
|
||||
.Returns(new List<RemoteEpisode> { 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<IPendingReleaseService>()
|
||||
.Setup(s => s.GetPendingRemoteEpisodes(It.IsAny<Int32>()))
|
||||
.Returns(new List<RemoteEpisode> { pendingRemoteEpisode1, pendingRemoteEpisode2 });
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,6 @@ public void Setup()
|
||||
{
|
||||
Name = "Test",
|
||||
Cutoff = Quality.HDTV720p,
|
||||
GrabDelay = 1,
|
||||
Items = new List<ProfileQualityItem>
|
||||
{
|
||||
new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p },
|
||||
|
@ -40,7 +40,6 @@ public void Setup()
|
||||
{
|
||||
Name = "Test",
|
||||
Cutoff = Quality.HDTV720p,
|
||||
GrabDelay = 1,
|
||||
Items = new List<ProfileQualityItem>
|
||||
{
|
||||
new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p },
|
||||
|
@ -40,7 +40,6 @@ public void Setup()
|
||||
{
|
||||
Name = "Test",
|
||||
Cutoff = Quality.HDTV720p,
|
||||
GrabDelay = 1,
|
||||
Items = new List<ProfileQualityItem>
|
||||
{
|
||||
new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p },
|
||||
|
@ -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 @@
|
||||
|
||||
namespace NzbDrone.Core.Test.Framework
|
||||
{
|
||||
|
||||
public abstract class DbTest<TSubject, TModel> : DbTest
|
||||
where TSubject : class
|
||||
where TModel : ModelBase, new()
|
||||
@ -85,27 +85,34 @@ protected ITestDatabase Db
|
||||
}
|
||||
}
|
||||
|
||||
private void WithTestDb()
|
||||
protected virtual TestDatabase WithTestDb(Action<MigrationBase> beforeMigration)
|
||||
{
|
||||
var factory = Mocker.Resolve<DbFactory>();
|
||||
var database = factory.Create(MigrationType);
|
||||
Mocker.SetConstant(database);
|
||||
|
||||
var testDb = new TestDatabase(database);
|
||||
|
||||
return testDb;
|
||||
}
|
||||
|
||||
|
||||
protected void SetupContainer()
|
||||
{
|
||||
WithTempAsAppPath();
|
||||
|
||||
|
||||
Mocker.SetConstant<IAnnouncer>(Mocker.Resolve<MigrationLogger>());
|
||||
Mocker.SetConstant<IConnectionStringFactory>(Mocker.Resolve<ConnectionStringFactory>());
|
||||
Mocker.SetConstant<IMigrationController>(Mocker.Resolve<MigrationController>());
|
||||
|
||||
MapRepository.Instance.EnableTraceLogging = true;
|
||||
|
||||
var factory = Mocker.Resolve<DbFactory>();
|
||||
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]
|
||||
|
36
src/NzbDrone.Core.Test/Framework/MigrationTest.cs
Normal file
36
src/NzbDrone.Core.Test/Framework/MigrationTest.cs
Normal file
@ -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<TMigration> : DbTest where TMigration : MigrationBase
|
||||
{
|
||||
protected override TestDatabase WithTestDb(Action<MigrationBase> beforeMigration)
|
||||
{
|
||||
var factory = Mocker.Resolve<DbFactory>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -116,6 +116,7 @@
|
||||
<Compile Include="Datastore\DatabaseRelationshipFixture.cs" />
|
||||
<Compile Include="Datastore\MappingExtentionFixture.cs" />
|
||||
<Compile Include="Datastore\MarrDataLazyLoadingFixture.cs" />
|
||||
<Compile Include="Datastore\Migration\070_delay_profileFixture.cs" />
|
||||
<Compile Include="Datastore\ObjectDatabaseFixture.cs" />
|
||||
<Compile Include="Datastore\PagingSpecExtensionsTests\PagingOffsetFixture.cs" />
|
||||
<Compile Include="Datastore\PagingSpecExtensionsTests\ToSortDirectionFixture.cs" />
|
||||
@ -123,6 +124,7 @@
|
||||
<Compile Include="Datastore\SqliteSchemaDumperTests\SqliteSchemaDumperFixture.cs" />
|
||||
<Compile Include="DecisionEngineTests\AcceptableSizeSpecificationFixture.cs" />
|
||||
<Compile Include="DecisionEngineTests\AnimeVersionUpgradeSpecificationFixture.cs" />
|
||||
<Compile Include="DecisionEngineTests\ProtocolSpecificationFixture.cs" />
|
||||
<Compile Include="DecisionEngineTests\CutoffSpecificationFixture.cs" />
|
||||
<Compile Include="DecisionEngineTests\DownloadDecisionMakerFixture.cs" />
|
||||
<Compile Include="DecisionEngineTests\HistorySpecificationFixture.cs" />
|
||||
@ -157,6 +159,7 @@
|
||||
<Compile Include="FluentTest.cs" />
|
||||
<Compile Include="Framework\CoreTest.cs" />
|
||||
<Compile Include="Framework\DbTest.cs" />
|
||||
<Compile Include="Framework\MigrationTest.cs" />
|
||||
<Compile Include="Framework\NBuilderExtensions.cs" />
|
||||
<Compile Include="Framework\TestDbHelper.cs" />
|
||||
<Compile Include="HealthCheck\Checks\AppDataLocationFixture.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<NzbDroneMigrationBase> beforeMigration = null);
|
||||
}
|
||||
|
||||
public class DbFactory : IDbFactory
|
||||
@ -43,7 +43,7 @@ public DbFactory(IMigrationController migrationController, IConnectionStringFact
|
||||
_connectionStringFactory = connectionStringFactory;
|
||||
}
|
||||
|
||||
public IDatabase Create(MigrationType migrationType = MigrationType.Main)
|
||||
public IDatabase Create(MigrationType migrationType = MigrationType.Main, Action<NzbDroneMigrationBase> beforeMigration = null)
|
||||
{
|
||||
string connectionString;
|
||||
|
||||
@ -66,7 +66,7 @@ public IDatabase Create(MigrationType migrationType = MigrationType.Main)
|
||||
}
|
||||
}
|
||||
|
||||
_migrationController.MigrateToLatest(connectionString, migrationType);
|
||||
_migrationController.MigrateToLatest(connectionString, migrationType, beforeMigration);
|
||||
|
||||
var db = new Database(() =>
|
||||
{
|
||||
|
165
src/NzbDrone.Core/Datastore/Migration/070_delay_profile.cs
Normal file
165
src/NzbDrone.Core/Datastore/Migration/070_delay_profile.cs
Normal file
@ -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<Profile70> GetProfiles(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
var profiles = new List<Profile70>();
|
||||
|
||||
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<int> 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<List<int>>(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<int> Tags { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
@ -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<NzbDroneMigrationBase> BeforeMigration { get; private set; }
|
||||
|
||||
public MigrationContext(MigrationType migrationType, Action<NzbDroneMigrationBase> beforeAction)
|
||||
{
|
||||
MigrationType = migrationType;
|
||||
|
||||
BeforeMigration = beforeAction;
|
||||
}
|
||||
}
|
||||
}
|
@ -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<NzbDroneMigrationBase> beforeMigration);
|
||||
}
|
||||
|
||||
public class MigrationController : IMigrationController
|
||||
@ -20,7 +20,7 @@ public MigrationController(IAnnouncer announcer)
|
||||
_announcer = announcer;
|
||||
}
|
||||
|
||||
public void MigrateToLatest(string connectionString, MigrationType migrationType)
|
||||
public void MigrateToLatest(string connectionString, MigrationType migrationType, Action<NzbDroneMigrationBase> beforeMigration)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
@ -31,10 +31,7 @@ public void MigrateToLatest(string connectionString, MigrationType migrationType
|
||||
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 @@ public void MigrateToLatest(string connectionString, MigrationType migrationType
|
||||
|
||||
sw.Stop();
|
||||
|
||||
_announcer.ElapsedTime(sw.Elapsed);
|
||||
_announcer.ElapsedTime(sw.Elapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 @@ protected virtual void LogDbUpgrade()
|
||||
{
|
||||
}
|
||||
|
||||
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();
|
||||
|
@ -16,6 +16,7 @@
|
||||
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 @@ public static void Map()
|
||||
Mapper.Entity<RemotePathMapping>().RegisterModel("RemotePathMappings");
|
||||
Mapper.Entity<Tag>().RegisterModel("Tags");
|
||||
Mapper.Entity<Restriction>().RegisterModel("Restrictions");
|
||||
|
||||
Mapper.Entity<DelayProfile>().RegisterModel("DelayProfiles");
|
||||
}
|
||||
|
||||
private static void RegisterMappers()
|
||||
|
@ -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 @@ public interface IPrioritizeDownloadDecision
|
||||
|
||||
public class DownloadDecisionPriorizationService : IPrioritizeDownloadDecision
|
||||
{
|
||||
private readonly IDelayProfileService _delayProfileService;
|
||||
|
||||
public DownloadDecisionPriorizationService(IDelayProfileService delayProfileService)
|
||||
{
|
||||
_delayProfileService = delayProfileService;
|
||||
}
|
||||
|
||||
public List<DownloadDecision> PrioritizeDecisions(List<DownloadDecision> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
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 @@ public class DelaySpecification : IDecisionEngineSpecification
|
||||
{
|
||||
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 @@ public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase
|
||||
}
|
||||
|
||||
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 (oldest != null && oldest.Release.AgeHours > delay)
|
||||
{
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
if (profile.GrabDelayMode == GrabDelayMode.First)
|
||||
if (subject.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();
|
||||
}
|
||||
}
|
||||
|
||||
if (subject.Release.AgeHours < profile.GrabDelay)
|
||||
{
|
||||
_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");
|
||||
}
|
||||
|
||||
|
@ -5,9 +5,11 @@
|
||||
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 @@ public interface IPendingReleaseService
|
||||
void RemoveGrabbed(List<DownloadDecision> grabbed);
|
||||
void RemoveRejected(List<DownloadDecision> rejected);
|
||||
List<ReleaseInfo> GetPending();
|
||||
List<RemoteEpisode> GetPendingRemoteEpisodes(Int32 seriesId);
|
||||
List<RemoteEpisode> GetPendingRemoteEpisodes(int seriesId);
|
||||
List<Queue.Queue> GetPendingQueue();
|
||||
RemoteEpisode OldestPendingRelease(int seriesId, IEnumerable<int> episodeIds);
|
||||
}
|
||||
|
||||
public class PendingReleaseService : IPendingReleaseService, IHandle<SeriesDeletedEvent>
|
||||
@ -29,18 +32,21 @@ public class PendingReleaseService : IPendingReleaseService, IHandle<SeriesDelet
|
||||
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 @@ public List<RemoteEpisode> GetPendingRemoteEpisodes(int seriesId)
|
||||
{
|
||||
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 @@ public List<RemoteEpisode> GetPendingRemoteEpisodes(int seriesId)
|
||||
return queued;
|
||||
}
|
||||
|
||||
public RemoteEpisode OldestPendingRelease(int seriesId, IEnumerable<int> episodeIds)
|
||||
{
|
||||
return GetPendingRemoteEpisodes(seriesId)
|
||||
.Where(r => r.Episodes.Select(e => e.Id).Intersect(episodeIds).Any())
|
||||
.OrderByDescending(p => p.Release.AgeHours)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
|
||||
private List<PendingRelease> GetPendingReleases()
|
||||
{
|
||||
var result = new List<PendingRelease>();
|
||||
@ -225,6 +238,13 @@ private Func<PendingRelease, bool> MatchingReleasePredicate(DownloadDecision dec
|
||||
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);
|
||||
|
@ -230,6 +230,7 @@
|
||||
<Compile Include="Datastore\Migration\066_add_tags.cs" />
|
||||
<Compile Include="Datastore\Migration\067_add_added_to_series.cs" />
|
||||
<Compile Include="Datastore\Migration\069_quality_proper.cs" />
|
||||
<Compile Include="Datastore\Migration\070_delay_profile.cs" />
|
||||
<Compile Include="Datastore\Migration\Framework\MigrationContext.cs" />
|
||||
<Compile Include="Datastore\Migration\Framework\MigrationController.cs" />
|
||||
<Compile Include="Datastore\Migration\Framework\MigrationDbFactory.cs" />
|
||||
@ -260,6 +261,7 @@
|
||||
<Compile Include="DecisionEngine\Specifications\BlacklistSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\AnimeVersionUpgradeSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\CutoffSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\ProtocolSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\LanguageSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\NotInQueueSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\ReleaseRestrictionsSpecification.cs" />
|
||||
@ -623,6 +625,10 @@
|
||||
<Compile Include="MetadataSource\Trakt\TraktException.cs" />
|
||||
<Compile Include="MetadataSource\TraktProxy.cs" />
|
||||
<Compile Include="MetadataSource\Tvdb\TvdbProxy.cs" />
|
||||
<Compile Include="Profiles\Delay\DelayProfile.cs" />
|
||||
<Compile Include="Profiles\Delay\DelayProfileService.cs" />
|
||||
<Compile Include="Profiles\Delay\DelayProfileTagInUseValidator.cs" />
|
||||
<Compile Include="Profiles\ProfileRepository.cs" />
|
||||
<Compile Include="Qualities\Revision.cs" />
|
||||
<Compile Include="RemotePathMappings\RemotePathMapping.cs" />
|
||||
<Compile Include="RemotePathMappings\RemotePathMappingRepository.cs" />
|
||||
@ -738,11 +744,10 @@
|
||||
<Compile Include="Parser\ParsingService.cs" />
|
||||
<Compile Include="Parser\SceneChecker.cs" />
|
||||
<Compile Include="Parser\QualityParser.cs" />
|
||||
<Compile Include="Profiles\GrabDelayMode.cs" />
|
||||
<Compile Include="Profiles\Profile.cs" />
|
||||
<Compile Include="Profiles\ProfileInUseException.cs" />
|
||||
<Compile Include="Profiles\ProfileQualityItem.cs" />
|
||||
<Compile Include="Profiles\ProfileRepository.cs" />
|
||||
<Compile Include="Profiles\Delay\DelayProfileRepository.cs" />
|
||||
<Compile Include="Profiles\ProfileService.cs" />
|
||||
<Compile Include="ProgressMessaging\CommandUpdatedEvent.cs" />
|
||||
<Compile Include="ProgressMessaging\ProgressMessageTarget.cs" />
|
||||
|
27
src/NzbDrone.Core/Profiles/Delay/DelayProfile.cs
Normal file
27
src/NzbDrone.Core/Profiles/Delay/DelayProfile.cs
Normal file
@ -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<int> Tags { get; set; }
|
||||
|
||||
public DelayProfile()
|
||||
{
|
||||
Tags = new HashSet<int>();
|
||||
}
|
||||
|
||||
public int GetProtocolDelay(DownloadProtocol protocol)
|
||||
{
|
||||
return protocol == DownloadProtocol.Torrent ? TorrentDelay : UsenetDelay;
|
||||
}
|
||||
}
|
||||
}
|
18
src/NzbDrone.Core/Profiles/Delay/DelayProfileRepository.cs
Normal file
18
src/NzbDrone.Core/Profiles/Delay/DelayProfileRepository.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Profiles.Delay
|
||||
{
|
||||
public interface IDelayProfileRepository : IBasicRepository<DelayProfile>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public class DelayProfileRepository : BasicRepository<DelayProfile>, IDelayProfileRepository
|
||||
{
|
||||
public DelayProfileRepository(IDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
76
src/NzbDrone.Core/Profiles/Delay/DelayProfileService.cs
Normal file
76
src/NzbDrone.Core/Profiles/Delay/DelayProfileService.cs
Normal file
@ -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<DelayProfile> All();
|
||||
DelayProfile Get(int id);
|
||||
List<DelayProfile> AllForTags(HashSet<int> tagIds);
|
||||
DelayProfile BestForTags(HashSet<int> 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<DelayProfile> All()
|
||||
{
|
||||
return _repo.All().ToList();
|
||||
}
|
||||
|
||||
public DelayProfile Get(int id)
|
||||
{
|
||||
return _repo.Get(id);
|
||||
}
|
||||
|
||||
public List<DelayProfile> AllForTags(HashSet<int> tagIds)
|
||||
{
|
||||
return _repo.All().Where(r => r.Tags.Intersect(tagIds).Any() || r.Tags.Empty()).ToList();
|
||||
}
|
||||
|
||||
public DelayProfile BestForTags(HashSet<int> tagIds)
|
||||
{
|
||||
return _repo.All().Where(r => r.Tags.Intersect(tagIds).Any() || r.Tags.Empty())
|
||||
.OrderBy(d => d.Order).First();
|
||||
}
|
||||
}
|
||||
}
|
@ -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<int>;
|
||||
|
||||
if (collection == null || collection.Empty()) return true;
|
||||
|
||||
return _delayProfileService.All().None(d => d.Id != delayProfile.Id && d.Tags.Intersect(collection).Any());
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
namespace NzbDrone.Core.Profiles
|
||||
{
|
||||
public enum GrabDelayMode
|
||||
{
|
||||
First = 0,
|
||||
Cutoff = 1,
|
||||
Always = 2
|
||||
}
|
||||
}
|
@ -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 @@ public class Profile : ModelBase
|
||||
public Quality Cutoff { get; set; }
|
||||
public List<ProfileQualityItem> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,8 @@
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=NUnit_002ENonPublicMethodWithTestAttribute/@EntryIndexedValue">ERROR</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ReturnTypeCanBeEnumerable_002EGlobal/@EntryIndexedValue">HINT</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=StringLiteralTypo/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=TestClassNameDoesNotMatchFileNameWarning/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=TestClassNameSuffixWarning/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UnusedParameter_002ELocal/@EntryIndexedValue">WARNING</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseObjectOrCollectionInitializer/@EntryIndexedValue">HINT</s:String>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/TestFileAnalysis/SeachForOrphanedProjectFiles/@EntryValue">True</s:Boolean>
|
||||
|
@ -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);
|
||||
|
32
src/UI/Mixins/AsSortedCollectionView.js
Normal file
32
src/UI/Mixins/AsSortedCollectionView.js
Normal file
@ -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;
|
||||
};
|
||||
}
|
||||
);
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -1,12 +1,12 @@
|
||||
<span class="col-sm-2">
|
||||
<div>{{host}}</div>
|
||||
</span>
|
||||
<span class="col-sm-5">
|
||||
<div>{{remotePath}}</div>
|
||||
</span>
|
||||
<span class="col-sm-4">
|
||||
<div>{{localPath}}</div>
|
||||
</span>
|
||||
<span class="col-sm-1">
|
||||
<div class="col-sm-2">
|
||||
{{host}}
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
{{remotePath}}
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
{{localPath}}
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<div class="pull-right"><i class="icon-nd-edit x-edit" title="" data-original-title="Edit Mapping"></i></div>
|
||||
</span>
|
||||
</div>
|
@ -1,12 +1,12 @@
|
||||
<span class="col-sm-4">
|
||||
<div class="col-sm-4">
|
||||
{{genericTagDisplay required 'label label-success'}}
|
||||
</span>
|
||||
<span class="col-sm-4">
|
||||
</div>
|
||||
<div class="col-sm-4">
|
||||
{{genericTagDisplay ignored 'label label-danger'}}
|
||||
</span>
|
||||
<span class="col-sm-3">
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
{{tagDisplay tags}}
|
||||
</span>
|
||||
<span class="col-sm-1">
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<div class="pull-right"><i class="icon-nd-edit x-edit" title="" data-original-title="Edit"></i></div>
|
||||
</span>
|
||||
</div>
|
12
src/UI/Settings/Profile/Delay/DelayProfileCollection.js
Normal file
12
src/UI/Settings/Profile/Delay/DelayProfileCollection.js
Normal file
@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
define(
|
||||
[
|
||||
'backbone',
|
||||
'Settings/Profile/Delay/DelayProfileModel'
|
||||
], function (Backbone, DelayProfileModel) {
|
||||
|
||||
return Backbone.Collection.extend({
|
||||
model: DelayProfileModel,
|
||||
url : window.NzbDrone.ApiRoot + '/delayprofile'
|
||||
});
|
||||
});
|
17
src/UI/Settings/Profile/Delay/DelayProfileCollectionView.js
Normal file
17
src/UI/Settings/Profile/Delay/DelayProfileCollectionView.js
Normal file
@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
define([
|
||||
'backbone.collectionview',
|
||||
'Settings/Profile/Delay/DelayProfileItemView'
|
||||
], function (BackboneSortableCollectionView, DelayProfileItemView) {
|
||||
|
||||
return BackboneSortableCollectionView.extend({
|
||||
className : 'delay-profiles',
|
||||
modelView : DelayProfileItemView,
|
||||
|
||||
events: {
|
||||
'click li, td' : '_listItem_onMousedown',
|
||||
'dblclick li, td' : '_listItem_onDoubleClick',
|
||||
'keydown' : '_onKeydown'
|
||||
}
|
||||
});
|
||||
});
|
27
src/UI/Settings/Profile/Delay/DelayProfileItemView.js
Normal file
27
src/UI/Settings/Profile/Delay/DelayProfileItemView.js
Normal file
@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
define([
|
||||
'jquery',
|
||||
'AppLayout',
|
||||
'marionette',
|
||||
'Settings/Profile/Delay/Edit/DelayProfileEditView'
|
||||
], function ($, AppLayout, Marionette, EditView) {
|
||||
|
||||
return Marionette.ItemView.extend({
|
||||
template : 'Settings/Profile/Delay/DelayProfileItemViewTemplate',
|
||||
className : 'row',
|
||||
|
||||
events: {
|
||||
'click .x-edit' : '_edit'
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.listenTo(this.model, 'sync', this.render);
|
||||
},
|
||||
|
||||
_edit: function() {
|
||||
var view = new EditView({ model: this.model, targetCollection: this.model.collection});
|
||||
AppLayout.modalRegion.show(view);
|
||||
}
|
||||
});
|
||||
});
|
@ -0,0 +1,57 @@
|
||||
<div class="col-sm-2">
|
||||
{{#if enableUsenet}}
|
||||
{{#if enableTorrent}}
|
||||
{{#if_eq preferredProtocol compare="usenet"}}
|
||||
Prefer Usenet
|
||||
{{else}}
|
||||
Prefer Torrent
|
||||
{{/if_eq}}
|
||||
{{else}}
|
||||
Only Usenet
|
||||
{{/if}}
|
||||
{{else}}
|
||||
Only Torrent
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
{{#if enableUsenet}}
|
||||
{{#if_eq usenetDelay compare="0"}}
|
||||
No delay
|
||||
{{else}}
|
||||
{{#if_eq usenetDelay compare="1"}}
|
||||
1 minute
|
||||
{{else}}
|
||||
{{usenetDelay}} minutes
|
||||
{{/if_eq}}
|
||||
{{/if_eq}}
|
||||
{{else}}
|
||||
-
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
{{#if enableTorrent}}
|
||||
{{#if_eq torrentDelay compare="0"}}
|
||||
No delay
|
||||
{{else}}
|
||||
{{#if_eq torrentDelay compare="1"}}
|
||||
1 minute
|
||||
{{else}}
|
||||
{{torrentDelay}} minutes
|
||||
{{/if_eq}}
|
||||
{{/if_eq}}
|
||||
{{else}}
|
||||
-
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="col-sm-5">
|
||||
{{tagDisplay tags}}
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<div class="pull-right">
|
||||
{{#unless_eq id compare="1"}}
|
||||
<i class="drag-handle icon-reorder x-drag-handle" title="Reorder"/>
|
||||
{{/unless_eq}}
|
||||
|
||||
<i class="icon-nd-edit x-edit" title="Edit"></i>
|
||||
</div>
|
||||
</div>
|
109
src/UI/Settings/Profile/Delay/DelayProfileLayout.js
Normal file
109
src/UI/Settings/Profile/Delay/DelayProfileLayout.js
Normal file
@ -0,0 +1,109 @@
|
||||
'use strict';
|
||||
define(
|
||||
[
|
||||
'jquery',
|
||||
'underscore',
|
||||
'vent',
|
||||
'AppLayout',
|
||||
'marionette',
|
||||
'backbone',
|
||||
'Settings/Profile/Delay/DelayProfileCollectionView',
|
||||
'Settings/Profile/Delay/Edit/DelayProfileEditView',
|
||||
'Settings/Profile/Delay/DelayProfileModel'
|
||||
], function ($,
|
||||
_,
|
||||
vent,
|
||||
AppLayout,
|
||||
Marionette,
|
||||
Backbone,
|
||||
DelayProfileCollectionView,
|
||||
EditView,
|
||||
Model) {
|
||||
|
||||
return Marionette.Layout.extend({
|
||||
template: 'Settings/Profile/Delay/DelayProfileLayoutTemplate',
|
||||
|
||||
regions: {
|
||||
delayProfiles : '.x-rows'
|
||||
},
|
||||
|
||||
events: {
|
||||
'click .x-add' : '_add'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
this.collection = options.collection;
|
||||
|
||||
this._updateOrderedCollection();
|
||||
|
||||
this.listenTo(this.collection, 'sync', this._updateOrderedCollection);
|
||||
this.listenTo(this.collection, 'add', this._updateOrderedCollection);
|
||||
this.listenTo(this.collection, 'remove', function () {
|
||||
this.collection.fetch();
|
||||
});
|
||||
},
|
||||
|
||||
onRender: function () {
|
||||
|
||||
this.sortableListView = new DelayProfileCollectionView({
|
||||
sortable : true,
|
||||
collection : this.orderedCollection,
|
||||
|
||||
sortableOptions : {
|
||||
handle: '.x-drag-handle'
|
||||
},
|
||||
|
||||
sortableModelsFilter : function( model ) {
|
||||
return model.get('id') !== 1;
|
||||
}
|
||||
});
|
||||
|
||||
this.delayProfiles.show(this.sortableListView);
|
||||
|
||||
this.listenTo(this.sortableListView, 'sortStop', this._updateOrder);
|
||||
},
|
||||
|
||||
_updateOrder: function() {
|
||||
var self = this;
|
||||
|
||||
this.collection.forEach(function (model) {
|
||||
if (model.get('id') === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var orderedModel = self.orderedCollection.get(model);
|
||||
var order = self.orderedCollection.indexOf(orderedModel) + 1;
|
||||
|
||||
if (model.get('order') !== order) {
|
||||
model.set('order', order);
|
||||
model.save();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_add: function() {
|
||||
var model = new Model({
|
||||
preferredProtocol : 1,
|
||||
usenetDelay : 0,
|
||||
torrentDelay : 0,
|
||||
order : this.collection.length,
|
||||
tags : []
|
||||
});
|
||||
|
||||
model.collection = this.collection;
|
||||
|
||||
var view = new EditView({ model: model, targetCollection: this.collection});
|
||||
AppLayout.modalRegion.show(view);
|
||||
},
|
||||
|
||||
_updateOrderedCollection: function () {
|
||||
if (!this.orderedCollection) {
|
||||
this.orderedCollection = new Backbone.Collection();
|
||||
}
|
||||
|
||||
this.orderedCollection.reset(_.sortBy(this.collection.models, function (model) {
|
||||
return model.get('order');
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
24
src/UI/Settings/Profile/Delay/DelayProfileLayoutTemplate.hbs
Normal file
24
src/UI/Settings/Profile/Delay/DelayProfileLayoutTemplate.hbs
Normal file
@ -0,0 +1,24 @@
|
||||
<fieldset class="advanced-setting">
|
||||
<legend>Delay Profiles</legend>
|
||||
|
||||
<div class="col-md-12">
|
||||
<div class="rule-setting-list">
|
||||
<div class="rule-setting-header x-header hidden-xs">
|
||||
<div class="row">
|
||||
<span class="col-sm-2">Protocol</span>
|
||||
<span class="col-sm-2">Usenet Delay</span>
|
||||
<span class="col-sm-2">Torrent Delay</span>
|
||||
<span class="col-sm-5">Tags</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rows x-rows"></div>
|
||||
<div class="rule-setting-footer">
|
||||
<div class="pull-right">
|
||||
<span class="add-rule-setting-mapping">
|
||||
<i class="icon-nd-add x-add" title="Add new delay profile" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
8
src/UI/Settings/Profile/Delay/DelayProfileModel.js
Normal file
8
src/UI/Settings/Profile/Delay/DelayProfileModel.js
Normal file
@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
define(
|
||||
[
|
||||
'backbone'
|
||||
], function (Backbone) {
|
||||
return Backbone.Model.extend({
|
||||
});
|
||||
});
|
@ -0,0 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
define([
|
||||
'vent',
|
||||
'marionette'
|
||||
], function (vent, Marionette) {
|
||||
return Marionette.ItemView.extend({
|
||||
template: 'Settings/Profile/Delay/Delete/DelayProfileDeleteViewTemplate',
|
||||
|
||||
events: {
|
||||
'click .x-confirm-delete': '_delete'
|
||||
},
|
||||
|
||||
_delete: function () {
|
||||
var collection = this.model.collection;
|
||||
|
||||
this.model.destroy({
|
||||
wait : true,
|
||||
success: function () {
|
||||
vent.trigger(vent.Commands.CloseModalCommand);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
@ -0,0 +1,13 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h3>Delete Delay Profile</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete this delay profile?</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" data-dismiss="modal">cancel</button>
|
||||
<button class="btn btn-danger x-confirm-delete">delete</button>
|
||||
</div>
|
||||
</div>
|
127
src/UI/Settings/Profile/Delay/Edit/DelayProfileEditView.js
Normal file
127
src/UI/Settings/Profile/Delay/Edit/DelayProfileEditView.js
Normal file
@ -0,0 +1,127 @@
|
||||
'use strict';
|
||||
|
||||
define([
|
||||
'vent',
|
||||
'AppLayout',
|
||||
'marionette',
|
||||
'Settings/Profile/Delay/Delete/DelayProfileDeleteView',
|
||||
'Mixins/AsModelBoundView',
|
||||
'Mixins/AsValidatedView',
|
||||
'Mixins/AsEditModalView',
|
||||
'Mixins/TagInput',
|
||||
'bootstrap'
|
||||
], function (vent, AppLayout, Marionette, DeleteView, AsModelBoundView, AsValidatedView, AsEditModalView) {
|
||||
|
||||
var view = Marionette.ItemView.extend({
|
||||
template: 'Settings/Profile/Delay/Edit/DelayProfileEditViewTemplate',
|
||||
|
||||
_deleteView: DeleteView,
|
||||
|
||||
ui: {
|
||||
tags : '.x-tags',
|
||||
usenetDelay : '.x-usenet-delay',
|
||||
torrentDelay : '.x-torrent-delay',
|
||||
protocol : '.x-protocol'
|
||||
},
|
||||
|
||||
events: {
|
||||
'change .x-protocol' : '_updateModel'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
this.targetCollection = options.targetCollection;
|
||||
},
|
||||
|
||||
onRender: function () {
|
||||
if (this.model.id !== 1) {
|
||||
this.ui.tags.tagInput({
|
||||
model : this.model,
|
||||
property : 'tags'
|
||||
});
|
||||
}
|
||||
|
||||
this._toggleControls();
|
||||
},
|
||||
|
||||
_onAfterSave: function () {
|
||||
this.targetCollection.add(this.model, { merge: true });
|
||||
vent.trigger(vent.Commands.CloseModalCommand);
|
||||
},
|
||||
|
||||
_updateModel: function () {
|
||||
var protocol = this.ui.protocol.val();
|
||||
|
||||
if (protocol === 'preferUsenet') {
|
||||
this.model.set({
|
||||
enableUsenet : true,
|
||||
enableTorrent : true,
|
||||
preferredProtocol : 'usenet'
|
||||
});
|
||||
}
|
||||
|
||||
if (protocol === 'preferTorrent') {
|
||||
this.model.set({
|
||||
enableUsenet : true,
|
||||
enableTorrent : true,
|
||||
preferredProtocol : 'torrent'
|
||||
});
|
||||
}
|
||||
|
||||
if (protocol === 'onlyUsenet') {
|
||||
this.model.set({
|
||||
enableUsenet : true,
|
||||
enableTorrent : false,
|
||||
preferredProtocol : 'usenet'
|
||||
});
|
||||
}
|
||||
|
||||
if (protocol === 'onlyTorrent') {
|
||||
this.model.set({
|
||||
enableUsenet : false,
|
||||
enableTorrent : true,
|
||||
preferredProtocol : 'torrent'
|
||||
});
|
||||
}
|
||||
|
||||
this._toggleControls();
|
||||
},
|
||||
|
||||
_toggleControls: function () {
|
||||
var enableUsenet = this.model.get('enableUsenet');
|
||||
var enableTorrent = this.model.get('enableTorrent');
|
||||
var preferred = this.model.get('preferredProtocol');
|
||||
|
||||
if (preferred === 'usenet') {
|
||||
this.ui.protocol.val('preferUsenet');
|
||||
}
|
||||
|
||||
else {
|
||||
this.ui.protocol.val('preferTorrent');
|
||||
}
|
||||
|
||||
if (enableUsenet) {
|
||||
this.ui.usenetDelay.show();
|
||||
}
|
||||
|
||||
else {
|
||||
this.ui.protocol.val('onlyTorrent');
|
||||
this.ui.usenetDelay.hide();
|
||||
}
|
||||
|
||||
if (enableTorrent) {
|
||||
this.ui.torrentDelay.show();
|
||||
}
|
||||
|
||||
else {
|
||||
this.ui.protocol.val('onlyUsenet');
|
||||
this.ui.torrentDelay.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AsModelBoundView.call(view);
|
||||
AsValidatedView.call(view);
|
||||
AsEditModalView.call(view);
|
||||
|
||||
return view;
|
||||
});
|
@ -0,0 +1,80 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" aria-hidden="true" data-dismiss="modal">×</button>
|
||||
{{#if id}}
|
||||
<h3>Edit - Delay Profile</h3>
|
||||
{{else}}
|
||||
<h3>Add - Delay Profile</h3>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="modal-body indexer-modal">
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">Protocol</label>
|
||||
|
||||
<div class="col-sm-1 col-sm-push-5 help-inline">
|
||||
<i class="icon-nd-form-info" title="Choose which protocol(s) to use and which one is preferred when choosing between otherwise equal releases" />
|
||||
</div>
|
||||
|
||||
<div class="col-sm-5 col-sm-pull-1">
|
||||
<select class="form-control x-protocol">
|
||||
<option value="preferUsenet">Prefer Usenet</option>
|
||||
<option value="preferTorrent">Prefer Torrent</option>
|
||||
<option value="onlyUsenet">Only Usenet</option>
|
||||
<option value="onlyTorrent">Only Torrent</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group x-usenet-delay">
|
||||
<label class="col-sm-3 control-label">Usenet Delay</label>
|
||||
|
||||
<div class="col-sm-1 col-sm-push-5 help-inline">
|
||||
<i class="icon-nd-form-info" title="Delay in minutes to wait before grabbing a release from Usenet" />
|
||||
</div>
|
||||
|
||||
<div class="col-sm-5 col-sm-pull-1">
|
||||
<input type="number" class="form-control" name="usenetDelay"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group x-torrent-delay">
|
||||
<label class="col-sm-3 control-label">Torrent Delay</label>
|
||||
|
||||
<div class="col-sm-1 col-sm-push-5 help-inline">
|
||||
<i class="icon-nd-form-info" title="Delay in minutes to wait before grabbing a torrent" />
|
||||
</div>
|
||||
|
||||
<div class="col-sm-5 col-sm-pull-1">
|
||||
<input type="number" class="form-control" name="torrentDelay"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if_eq id compare="1"}}
|
||||
<div class="alert alert-info" role="alert">This is the default profile. It applies to all series that don't have an explicit profile.</div>
|
||||
{{else}}
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">Tags</label>
|
||||
|
||||
<div class="col-sm-1 col-sm-push-5 help-inline">
|
||||
<i class="icon-nd-form-info" title="One or more tags to apply these rules to matching series" />
|
||||
</div>
|
||||
|
||||
<div class="col-sm-5 col-sm-pull-1">
|
||||
<input type="text" class="form-control x-tags">
|
||||
</div>
|
||||
</div>
|
||||
{{/if_eq}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
{{#if id}}
|
||||
{{#if_gt id compare="1"}}
|
||||
<button class="btn btn-danger pull-left x-delete">delete</button>
|
||||
{{/if_gt}}
|
||||
{{/if}}
|
||||
<span class="indicator x-indicator"><i class="icon-spinner icon-spin"></i></span>
|
||||
<button class="btn" data-dismiss="modal">cancel</button>
|
||||
<button class="btn btn-primary x-save">save</button>
|
||||
</div>
|
||||
</div>
|
@ -13,14 +13,7 @@ define(
|
||||
template: 'Settings/Profile/Edit/EditProfileViewTemplate',
|
||||
|
||||
ui: {
|
||||
cutoff : '.x-cutoff',
|
||||
delay : '.x-delay',
|
||||
delayMode : '.x-delay-mode'
|
||||
},
|
||||
|
||||
events: {
|
||||
'change .x-delay': 'toggleDelayMode',
|
||||
'keyup .x-delay': 'toggleDelayMode'
|
||||
cutoff : '.x-cutoff'
|
||||
},
|
||||
|
||||
templateHelpers: function () {
|
||||
@ -29,30 +22,10 @@ define(
|
||||
};
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
this.toggleDelayMode();
|
||||
},
|
||||
|
||||
getCutoff: function () {
|
||||
var self = this;
|
||||
|
||||
return _.findWhere(_.pluck(this.model.get('items'), 'quality'), { id: parseInt(self.ui.cutoff.val(), 10)});
|
||||
},
|
||||
|
||||
toggleDelayMode: function () {
|
||||
var delay = parseInt(this.ui.delay.val(), 10);
|
||||
|
||||
if (isNaN(delay)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (delay > 0 && Config.getValueBoolean(Config.Keys.AdvancedSettings)) {
|
||||
this.ui.delayMode.show();
|
||||
}
|
||||
|
||||
else {
|
||||
this.ui.delayMode.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -24,34 +24,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group advanced-setting">
|
||||
<label class="col-sm-3 control-label">Delay</label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<input type="number" min="0" max="72" name="grabDelay" class="form-control x-delay">
|
||||
</div>
|
||||
|
||||
<div class="col-sm-1 help-inline">
|
||||
<i class="icon-nd-form-info" title="Wait time in hours before grabbing a release automatically, set to 0 to disable. The highest allowed quality in the profile will be grabbed immediately when available."/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group advanced-setting x-delay-mode">
|
||||
<label class="col-sm-3 control-label">Delay Mode</label>
|
||||
|
||||
<div class="col-sm-5">
|
||||
<select class="form-control" name="grabDelayMode">
|
||||
<option value="first">First</option>
|
||||
<option value="cutoff">Cutoff</option>
|
||||
<option value="always">Always</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-1 help-inline">
|
||||
<i class="icon-nd-form-info" data-html="true" title="First: Delay until first wanted release passes delay, grabbing best quality release at that time. Cutoff: Delay for all qualities below the cutoff. Always: Delay before grabbing all qualities"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">Cutoff</label>
|
||||
|
||||
|
@ -5,22 +5,29 @@ define(
|
||||
'marionette',
|
||||
'Profile/ProfileCollection',
|
||||
'Settings/Profile/ProfileCollectionView',
|
||||
'Settings/Profile/Delay/DelayProfileLayout',
|
||||
'Settings/Profile/Delay/DelayProfileCollection',
|
||||
'Settings/Profile/Language/LanguageCollection'
|
||||
], function (Marionette, ProfileCollection, ProfileCollectionView, LanguageCollection) {
|
||||
], function (Marionette, ProfileCollection, ProfileCollectionView, DelayProfileLayout, DelayProfileCollection, LanguageCollection) {
|
||||
return Marionette.Layout.extend({
|
||||
template: 'Settings/Profile/ProfileLayoutTemplate',
|
||||
|
||||
regions: {
|
||||
profile : '#profile'
|
||||
profile : '#profile',
|
||||
delayProfile : '#delay-profile'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
this.settings = options.settings;
|
||||
ProfileCollection.fetch();
|
||||
|
||||
this.delayProfileCollection = new DelayProfileCollection();
|
||||
this.delayProfileCollection.fetch();
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
this.profile.show(new ProfileCollectionView({collection: ProfileCollection}));
|
||||
this.delayProfile.show(new DelayProfileLayout({collection: this.delayProfileCollection}));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,3 +1,5 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12" id="profile"/>
|
||||
|
||||
<div class="col-md-12 delay-profile-region" id="delay-profile"/>
|
||||
</div>
|
||||
|
@ -5,11 +5,8 @@
|
||||
|
||||
<div class="language">
|
||||
{{languageLabel}}
|
||||
|
||||
{{#if_gt grabDelay compare="0"}}
|
||||
<i class="icon-time" title="{{grabDelay}} hour, Mode: {{TitleCase grabDelayMode}}"></i>
|
||||
{{/if_gt}}
|
||||
</div>
|
||||
|
||||
<ul class="allowed-qualities">
|
||||
{{allowedLabeler}}
|
||||
</ul>
|
||||
|
@ -29,3 +29,15 @@
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.delay-profile-region {
|
||||
margin-top : 30px;
|
||||
}
|
||||
|
||||
.delay-profiles {
|
||||
padding-left : 0px;
|
||||
|
||||
li {
|
||||
list-style-type : none;
|
||||
}
|
||||
}
|
@ -154,7 +154,8 @@ li.save-and-add:hover {
|
||||
padding : 5px;
|
||||
|
||||
i {
|
||||
cursor : pointer;
|
||||
cursor : pointer;
|
||||
margin-left : 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user