mirror of
https://github.com/Radarr/Radarr.git
synced 2024-11-04 10:02:40 +01:00
commit
81794c3200
@ -98,6 +98,9 @@
|
|||||||
<Compile Include="Commands\CommandResource.cs" />
|
<Compile Include="Commands\CommandResource.cs" />
|
||||||
<Compile Include="Extensions\AccessControlHeaders.cs" />
|
<Compile Include="Extensions\AccessControlHeaders.cs" />
|
||||||
<Compile Include="Extensions\Pipelines\CorsPipeline.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\RemotePathMappingModule.cs" />
|
||||||
<Compile Include="RemotePathMappings\RemotePathMappingResource.cs" />
|
<Compile Include="RemotePathMappings\RemotePathMappingResource.cs" />
|
||||||
<Compile Include="Config\UiConfigModule.cs" />
|
<Compile Include="Config\UiConfigModule.cs" />
|
||||||
@ -200,6 +203,7 @@
|
|||||||
<Compile Include="Restrictions\RestrictionModule.cs" />
|
<Compile Include="Restrictions\RestrictionModule.cs" />
|
||||||
<Compile Include="Restrictions\RestrictionResource.cs" />
|
<Compile Include="Restrictions\RestrictionResource.cs" />
|
||||||
<Compile Include="REST\BadRequestException.cs" />
|
<Compile Include="REST\BadRequestException.cs" />
|
||||||
|
<Compile Include="REST\MethodNotAllowedException.cs" />
|
||||||
<Compile Include="REST\ResourceValidator.cs" />
|
<Compile Include="REST\ResourceValidator.cs" />
|
||||||
<Compile Include="REST\RestModule.cs" />
|
<Compile Include="REST\RestModule.cs" />
|
||||||
<Compile Include="REST\RestResource.cs" />
|
<Compile Include="REST\RestResource.cs" />
|
||||||
@ -221,6 +225,7 @@
|
|||||||
<Compile Include="TinyIoCNancyBootstrapper.cs" />
|
<Compile Include="TinyIoCNancyBootstrapper.cs" />
|
||||||
<Compile Include="Update\UpdateModule.cs" />
|
<Compile Include="Update\UpdateModule.cs" />
|
||||||
<Compile Include="Update\UpdateResource.cs" />
|
<Compile Include="Update\UpdateResource.cs" />
|
||||||
|
<Compile Include="Validation\EmptyCollectionValidator.cs" />
|
||||||
<Compile Include="Validation\RuleBuilderExtensions.cs" />
|
<Compile Include="Validation\RuleBuilderExtensions.cs" />
|
||||||
<Compile Include="Wanted\CutoffModule.cs" />
|
<Compile Include="Wanted\CutoffModule.cs" />
|
||||||
<Compile Include="Wanted\LegacyMissingModule.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.Cutoff = (Quality)resource.Cutoff.Id;
|
||||||
model.Items = resource.Items.InjectTo<List<ProfileQualityItem>>();
|
model.Items = resource.Items.InjectTo<List<ProfileQualityItem>>();
|
||||||
model.Language = resource.Language;
|
model.Language = resource.Language;
|
||||||
model.GrabDelay = resource.GrabDelay;
|
|
||||||
model.GrabDelayMode = resource.GrabDelayMode;
|
|
||||||
|
|
||||||
_profileService.Update(model);
|
_profileService.Update(model);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using NzbDrone.Api.REST;
|
using NzbDrone.Api.REST;
|
||||||
using NzbDrone.Core.Parser;
|
using NzbDrone.Core.Parser;
|
||||||
using NzbDrone.Core.Profiles;
|
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
|
|
||||||
namespace NzbDrone.Api.Profiles
|
namespace NzbDrone.Api.Profiles
|
||||||
@ -13,8 +12,6 @@ public class ProfileResource : RestResource
|
|||||||
public Quality Cutoff { get; set; }
|
public Quality Cutoff { get; set; }
|
||||||
public List<ProfileQualityItemResource> Items { get; set; }
|
public List<ProfileQualityItemResource> Items { get; set; }
|
||||||
public Language Language { get; set; }
|
public Language Language { get; set; }
|
||||||
public Int32 GrabDelay { get; set; }
|
|
||||||
public GrabDelayMode GrabDelayMode { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ProfileQualityItemResource : RestResource
|
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;
|
||||||
using FluentValidation.Validators;
|
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(""));
|
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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Moq;
|
||||||
|
using NzbDrone.Core.Indexers;
|
||||||
|
using NzbDrone.Core.Profiles.Delay;
|
||||||
using NzbDrone.Core.Tv;
|
using NzbDrone.Core.Tv;
|
||||||
using NzbDrone.Core.Profiles;
|
using NzbDrone.Core.Profiles;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.DecisionEngine;
|
using NzbDrone.Core.DecisionEngine;
|
||||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using FizzWare.NBuilder;
|
using FizzWare.NBuilder;
|
||||||
@ -17,6 +19,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class PrioritizeDownloadDecisionFixture : CoreTest<DownloadDecisionPriorizationService>
|
public class PrioritizeDownloadDecisionFixture : CoreTest<DownloadDecisionPriorizationService>
|
||||||
{
|
{
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
GivenPreferredDownloadProtocol(DownloadProtocol.Usenet);
|
||||||
|
}
|
||||||
|
|
||||||
private Episode GivenEpisode(int id)
|
private Episode GivenEpisode(int id)
|
||||||
{
|
{
|
||||||
return Builder<Episode>.CreateNew()
|
return Builder<Episode>.CreateNew()
|
||||||
@ -25,7 +33,7 @@ private Episode GivenEpisode(int id)
|
|||||||
.Build();
|
.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();
|
var remoteEpisode = new RemoteEpisode();
|
||||||
remoteEpisode.ParsedEpisodeInfo = new ParsedEpisodeInfo();
|
remoteEpisode.ParsedEpisodeInfo = new ParsedEpisodeInfo();
|
||||||
@ -37,6 +45,7 @@ private RemoteEpisode GivenRemoteEpisode(List<Episode> episodes, QualityModel qu
|
|||||||
remoteEpisode.Release = new ReleaseInfo();
|
remoteEpisode.Release = new ReleaseInfo();
|
||||||
remoteEpisode.Release.PublishDate = DateTime.Now.AddDays(-age);
|
remoteEpisode.Release.PublishDate = DateTime.Now.AddDays(-age);
|
||||||
remoteEpisode.Release.Size = size;
|
remoteEpisode.Release.Size = size;
|
||||||
|
remoteEpisode.Release.DownloadProtocol = downloadProtocol;
|
||||||
|
|
||||||
remoteEpisode.Series = Builder<Series>.CreateNew()
|
remoteEpisode.Series = Builder<Series>.CreateNew()
|
||||||
.With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() })
|
.With(e => e.Profile = new Profile { Items = Qualities.QualityFixture.GetDefaultQualities() })
|
||||||
@ -45,6 +54,16 @@ private RemoteEpisode GivenRemoteEpisode(List<Episode> episodes, QualityModel qu
|
|||||||
return remoteEpisode;
|
return remoteEpisode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void GivenPreferredDownloadProtocol(DownloadProtocol downloadProtocol)
|
||||||
|
{
|
||||||
|
Mocker.GetMock<IDelayProfileService>()
|
||||||
|
.Setup(s => s.BestForTags(It.IsAny<HashSet<int>>()))
|
||||||
|
.Returns(new DelayProfile
|
||||||
|
{
|
||||||
|
PreferredProtocol = downloadProtocol
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void should_put_propers_before_non_propers()
|
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);
|
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]
|
[TestFixture]
|
||||||
public class ReleaseRestrictionsSpecificationFixture : CoreTest<ReleaseRestrictionsSpecification>
|
public class ReleaseRestrictionsSpecificationFixture : CoreTest<ReleaseRestrictionsSpecification>
|
||||||
{
|
{
|
||||||
private RemoteEpisode _parseResult;
|
private RemoteEpisode _remoteEpisode;
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
_parseResult = new RemoteEpisode
|
_remoteEpisode = new RemoteEpisode
|
||||||
{
|
{
|
||||||
Series = new Series
|
Series = new Series
|
||||||
{
|
{
|
||||||
@ -54,7 +54,7 @@ public void should_be_true_when_restrictions_are_empty()
|
|||||||
.Setup(s => s.AllForTags(It.IsAny<HashSet<Int32>>()))
|
.Setup(s => s.AllForTags(It.IsAny<HashSet<Int32>>()))
|
||||||
.Returns(new List<Restriction>());
|
.Returns(new List<Restriction>());
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeTrue();
|
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -62,7 +62,7 @@ public void should_be_true_when_title_contains_one_required_term()
|
|||||||
{
|
{
|
||||||
GivenRestictions("WEBRip", null);
|
GivenRestictions("WEBRip", null);
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeTrue();
|
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -70,7 +70,7 @@ public void should_be_false_when_title_does_not_contain_any_required_terms()
|
|||||||
{
|
{
|
||||||
GivenRestictions("doesnt,exist", null);
|
GivenRestictions("doesnt,exist", null);
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeFalse();
|
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -78,7 +78,7 @@ public void should_be_true_when_title_does_not_contain_any_ignored_terms()
|
|||||||
{
|
{
|
||||||
GivenRestictions(null, "ignored");
|
GivenRestictions(null, "ignored");
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeTrue();
|
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -86,7 +86,7 @@ public void should_be_false_when_title_contains_one_anded_ignored_terms()
|
|||||||
{
|
{
|
||||||
GivenRestictions(null, "edited");
|
GivenRestictions(null, "edited");
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeFalse();
|
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("EdiTED")]
|
[TestCase("EdiTED")]
|
||||||
@ -97,7 +97,7 @@ public void should_ignore_case_when_matching_required(String required)
|
|||||||
{
|
{
|
||||||
GivenRestictions(required, null);
|
GivenRestictions(required, null);
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeTrue();
|
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase("EdiTED")]
|
[TestCase("EdiTED")]
|
||||||
@ -108,7 +108,22 @@ public void should_ignore_case_when_matching_ignored(String ignored)
|
|||||||
{
|
{
|
||||||
GivenRestictions(null, 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;
|
||||||
using NzbDrone.Core.DecisionEngine.Specifications.RssSync;
|
using NzbDrone.Core.DecisionEngine.Specifications.RssSync;
|
||||||
using NzbDrone.Core.Download.Pending;
|
using NzbDrone.Core.Download.Pending;
|
||||||
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
using NzbDrone.Core.MediaFiles;
|
using NzbDrone.Core.MediaFiles;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.Profiles;
|
using NzbDrone.Core.Profiles;
|
||||||
|
using NzbDrone.Core.Profiles.Delay;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
using NzbDrone.Core.Tv;
|
using NzbDrone.Core.Tv;
|
||||||
using NzbDrone.Test.Common;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
||||||
{
|
{
|
||||||
@ -24,6 +25,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
|||||||
public class DelaySpecificationFixture : CoreTest<DelaySpecification>
|
public class DelaySpecificationFixture : CoreTest<DelaySpecification>
|
||||||
{
|
{
|
||||||
private Profile _profile;
|
private Profile _profile;
|
||||||
|
private DelayProfile _delayProfile;
|
||||||
private RemoteEpisode _remoteEpisode;
|
private RemoteEpisode _remoteEpisode;
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
@ -32,6 +34,10 @@ public void Setup()
|
|||||||
_profile = Builder<Profile>.CreateNew()
|
_profile = Builder<Profile>.CreateNew()
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
|
_delayProfile = Builder<DelayProfile>.CreateNew()
|
||||||
|
.With(d => d.PreferredProtocol = DownloadProtocol.Usenet)
|
||||||
|
.Build();
|
||||||
|
|
||||||
var series = Builder<Series>.CreateNew()
|
var series = Builder<Series>.CreateNew()
|
||||||
.With(s => s.Profile = _profile)
|
.With(s => s.Profile = _profile)
|
||||||
.Build();
|
.Build();
|
||||||
@ -46,13 +52,21 @@ public void Setup()
|
|||||||
_profile.Items.Add(new ProfileQualityItem { Allowed = true, Quality = Quality.Bluray720p });
|
_profile.Items.Add(new ProfileQualityItem { Allowed = true, Quality = Quality.Bluray720p });
|
||||||
|
|
||||||
_profile.Cutoff = Quality.WEBDL720p;
|
_profile.Cutoff = Quality.WEBDL720p;
|
||||||
_profile.GrabDelayMode = GrabDelayMode.Always;
|
|
||||||
|
|
||||||
_remoteEpisode.ParsedEpisodeInfo = new ParsedEpisodeInfo();
|
_remoteEpisode.ParsedEpisodeInfo = new ParsedEpisodeInfo();
|
||||||
_remoteEpisode.Release = new ReleaseInfo();
|
_remoteEpisode.Release = new ReleaseInfo();
|
||||||
|
_remoteEpisode.Release.DownloadProtocol = DownloadProtocol.Usenet;
|
||||||
|
|
||||||
_remoteEpisode.Episodes = Builder<Episode>.CreateListOfSize(1).Build().ToList();
|
_remoteEpisode.Episodes = Builder<Episode>.CreateListOfSize(1).Build().ToList();
|
||||||
_remoteEpisode.Episodes.First().EpisodeFileId = 0;
|
_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)
|
private void GivenExistingFile(QualityModel quality)
|
||||||
@ -81,7 +95,7 @@ public void should_be_true_when_search()
|
|||||||
[Test]
|
[Test]
|
||||||
public void should_be_true_when_profile_does_not_have_a_delay()
|
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();
|
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.ParsedEpisodeInfo.Quality = new QualityModel(Quality.HDTV720p);
|
||||||
_remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddHours(-10);
|
_remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddHours(-10);
|
||||||
|
|
||||||
_profile.GrabDelay = 1;
|
_delayProfile.UsenetDelay = 1;
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
|
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.ParsedEpisodeInfo.Quality = new QualityModel(Quality.SDTV);
|
||||||
_remoteEpisode.Release.PublishDate = DateTime.UtcNow;
|
_remoteEpisode.Release.PublishDate = DateTime.UtcNow;
|
||||||
|
|
||||||
_profile.GrabDelay = 12;
|
_delayProfile.UsenetDelay = 12;
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
|
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>()))
|
.Setup(s => s.IsRevisionUpgrade(It.IsAny<QualityModel>(), It.IsAny<QualityModel>()))
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
|
|
||||||
_profile.GrabDelay = 12;
|
_delayProfile.UsenetDelay = 12;
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
|
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>()))
|
.Setup(s => s.IsRevisionUpgrade(It.IsAny<QualityModel>(), It.IsAny<QualityModel>()))
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
|
|
||||||
_profile.GrabDelay = 12;
|
_delayProfile.UsenetDelay = 12;
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue();
|
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]
|
[Test]
|
||||||
public void should_be_false_when_release_is_proper_for_existing_episode_of_different_quality()
|
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));
|
GivenExistingFile(new QualityModel(Quality.SDTV));
|
||||||
|
|
||||||
_profile.GrabDelay = 12;
|
_delayProfile.UsenetDelay = 12;
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse();
|
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",
|
Name = "Test",
|
||||||
Cutoff = Quality.HDTV720p,
|
Cutoff = Quality.HDTV720p,
|
||||||
GrabDelay = 1,
|
|
||||||
Items = new List<ProfileQualityItem>
|
Items = new List<ProfileQualityItem>
|
||||||
{
|
{
|
||||||
new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p },
|
new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p },
|
||||||
|
@ -40,7 +40,6 @@ public void Setup()
|
|||||||
{
|
{
|
||||||
Name = "Test",
|
Name = "Test",
|
||||||
Cutoff = Quality.HDTV720p,
|
Cutoff = Quality.HDTV720p,
|
||||||
GrabDelay = 1,
|
|
||||||
Items = new List<ProfileQualityItem>
|
Items = new List<ProfileQualityItem>
|
||||||
{
|
{
|
||||||
new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p },
|
new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p },
|
||||||
|
@ -40,7 +40,6 @@ public void Setup()
|
|||||||
{
|
{
|
||||||
Name = "Test",
|
Name = "Test",
|
||||||
Cutoff = Quality.HDTV720p,
|
Cutoff = Quality.HDTV720p,
|
||||||
GrabDelay = 1,
|
|
||||||
Items = new List<ProfileQualityItem>
|
Items = new List<ProfileQualityItem>
|
||||||
{
|
{
|
||||||
new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p },
|
new ProfileQualityItem { Allowed = true, Quality = Quality.HDTV720p },
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using FluentMigrator;
|
||||||
using FluentMigrator.Runner;
|
using FluentMigrator.Runner;
|
||||||
using Marr.Data;
|
using Marr.Data;
|
||||||
using Moq;
|
using Moq;
|
||||||
@ -13,7 +14,6 @@
|
|||||||
|
|
||||||
namespace NzbDrone.Core.Test.Framework
|
namespace NzbDrone.Core.Test.Framework
|
||||||
{
|
{
|
||||||
|
|
||||||
public abstract class DbTest<TSubject, TModel> : DbTest
|
public abstract class DbTest<TSubject, TModel> : DbTest
|
||||||
where TSubject : class
|
where TSubject : class
|
||||||
where TModel : ModelBase, new()
|
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();
|
WithTempAsAppPath();
|
||||||
|
|
||||||
|
|
||||||
Mocker.SetConstant<IAnnouncer>(Mocker.Resolve<MigrationLogger>());
|
Mocker.SetConstant<IAnnouncer>(Mocker.Resolve<MigrationLogger>());
|
||||||
Mocker.SetConstant<IConnectionStringFactory>(Mocker.Resolve<ConnectionStringFactory>());
|
Mocker.SetConstant<IConnectionStringFactory>(Mocker.Resolve<ConnectionStringFactory>());
|
||||||
Mocker.SetConstant<IMigrationController>(Mocker.Resolve<MigrationController>());
|
Mocker.SetConstant<IMigrationController>(Mocker.Resolve<MigrationController>());
|
||||||
|
|
||||||
MapRepository.Instance.EnableTraceLogging = true;
|
MapRepository.Instance.EnableTraceLogging = true;
|
||||||
|
|
||||||
var factory = Mocker.Resolve<DbFactory>();
|
|
||||||
var _database = factory.Create(MigrationType);
|
|
||||||
_db = new TestDatabase(_database);
|
|
||||||
Mocker.SetConstant(_database);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void SetupReadDb()
|
public virtual void SetupDb()
|
||||||
{
|
{
|
||||||
WithTestDb();
|
SetupContainer();
|
||||||
|
_db = WithTestDb(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TearDown]
|
[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\DatabaseRelationshipFixture.cs" />
|
||||||
<Compile Include="Datastore\MappingExtentionFixture.cs" />
|
<Compile Include="Datastore\MappingExtentionFixture.cs" />
|
||||||
<Compile Include="Datastore\MarrDataLazyLoadingFixture.cs" />
|
<Compile Include="Datastore\MarrDataLazyLoadingFixture.cs" />
|
||||||
|
<Compile Include="Datastore\Migration\070_delay_profileFixture.cs" />
|
||||||
<Compile Include="Datastore\ObjectDatabaseFixture.cs" />
|
<Compile Include="Datastore\ObjectDatabaseFixture.cs" />
|
||||||
<Compile Include="Datastore\PagingSpecExtensionsTests\PagingOffsetFixture.cs" />
|
<Compile Include="Datastore\PagingSpecExtensionsTests\PagingOffsetFixture.cs" />
|
||||||
<Compile Include="Datastore\PagingSpecExtensionsTests\ToSortDirectionFixture.cs" />
|
<Compile Include="Datastore\PagingSpecExtensionsTests\ToSortDirectionFixture.cs" />
|
||||||
@ -123,6 +124,7 @@
|
|||||||
<Compile Include="Datastore\SqliteSchemaDumperTests\SqliteSchemaDumperFixture.cs" />
|
<Compile Include="Datastore\SqliteSchemaDumperTests\SqliteSchemaDumperFixture.cs" />
|
||||||
<Compile Include="DecisionEngineTests\AcceptableSizeSpecificationFixture.cs" />
|
<Compile Include="DecisionEngineTests\AcceptableSizeSpecificationFixture.cs" />
|
||||||
<Compile Include="DecisionEngineTests\AnimeVersionUpgradeSpecificationFixture.cs" />
|
<Compile Include="DecisionEngineTests\AnimeVersionUpgradeSpecificationFixture.cs" />
|
||||||
|
<Compile Include="DecisionEngineTests\ProtocolSpecificationFixture.cs" />
|
||||||
<Compile Include="DecisionEngineTests\CutoffSpecificationFixture.cs" />
|
<Compile Include="DecisionEngineTests\CutoffSpecificationFixture.cs" />
|
||||||
<Compile Include="DecisionEngineTests\DownloadDecisionMakerFixture.cs" />
|
<Compile Include="DecisionEngineTests\DownloadDecisionMakerFixture.cs" />
|
||||||
<Compile Include="DecisionEngineTests\HistorySpecificationFixture.cs" />
|
<Compile Include="DecisionEngineTests\HistorySpecificationFixture.cs" />
|
||||||
@ -157,6 +159,7 @@
|
|||||||
<Compile Include="FluentTest.cs" />
|
<Compile Include="FluentTest.cs" />
|
||||||
<Compile Include="Framework\CoreTest.cs" />
|
<Compile Include="Framework\CoreTest.cs" />
|
||||||
<Compile Include="Framework\DbTest.cs" />
|
<Compile Include="Framework\DbTest.cs" />
|
||||||
|
<Compile Include="Framework\MigrationTest.cs" />
|
||||||
<Compile Include="Framework\NBuilderExtensions.cs" />
|
<Compile Include="Framework\NBuilderExtensions.cs" />
|
||||||
<Compile Include="Framework\TestDbHelper.cs" />
|
<Compile Include="Framework\TestDbHelper.cs" />
|
||||||
<Compile Include="HealthCheck\Checks\AppDataLocationFixture.cs" />
|
<Compile Include="HealthCheck\Checks\AppDataLocationFixture.cs" />
|
||||||
|
@ -12,7 +12,7 @@ namespace NzbDrone.Core.Datastore
|
|||||||
{
|
{
|
||||||
public interface IDbFactory
|
public interface IDbFactory
|
||||||
{
|
{
|
||||||
IDatabase Create(MigrationType migrationType = MigrationType.Main);
|
IDatabase Create(MigrationType migrationType = MigrationType.Main, Action<NzbDroneMigrationBase> beforeMigration = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DbFactory : IDbFactory
|
public class DbFactory : IDbFactory
|
||||||
@ -43,7 +43,7 @@ public DbFactory(IMigrationController migrationController, IConnectionStringFact
|
|||||||
_connectionStringFactory = connectionStringFactory;
|
_connectionStringFactory = connectionStringFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDatabase Create(MigrationType migrationType = MigrationType.Main)
|
public IDatabase Create(MigrationType migrationType = MigrationType.Main, Action<NzbDroneMigrationBase> beforeMigration = null)
|
||||||
{
|
{
|
||||||
string connectionString;
|
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(() =>
|
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 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 System.Reflection;
|
||||||
using FluentMigrator.Runner;
|
using FluentMigrator.Runner;
|
||||||
using FluentMigrator.Runner.Initialization;
|
using FluentMigrator.Runner.Initialization;
|
||||||
using FluentMigrator.Runner.Processors.SQLite;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Datastore.Migration.Framework
|
namespace NzbDrone.Core.Datastore.Migration.Framework
|
||||||
{
|
{
|
||||||
public interface IMigrationController
|
public interface IMigrationController
|
||||||
{
|
{
|
||||||
void MigrateToLatest(string connectionString, MigrationType migrationType);
|
void MigrateToLatest(string connectionString, MigrationType migrationType, Action<NzbDroneMigrationBase> beforeMigration);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MigrationController : IMigrationController
|
public class MigrationController : IMigrationController
|
||||||
@ -20,7 +20,7 @@ public MigrationController(IAnnouncer announcer)
|
|||||||
_announcer = announcer;
|
_announcer = announcer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void MigrateToLatest(string connectionString, MigrationType migrationType)
|
public void MigrateToLatest(string connectionString, MigrationType migrationType, Action<NzbDroneMigrationBase> beforeMigration)
|
||||||
{
|
{
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
|
|
||||||
@ -31,10 +31,7 @@ public void MigrateToLatest(string connectionString, MigrationType migrationType
|
|||||||
var migrationContext = new RunnerContext(_announcer)
|
var migrationContext = new RunnerContext(_announcer)
|
||||||
{
|
{
|
||||||
Namespace = "NzbDrone.Core.Datastore.Migration",
|
Namespace = "NzbDrone.Core.Datastore.Migration",
|
||||||
ApplicationContext = new MigrationContext
|
ApplicationContext = new MigrationContext(migrationType, beforeMigration)
|
||||||
{
|
|
||||||
MigrationType = migrationType
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var options = new MigrationOptions { PreviewOnly = false, Timeout = 60 };
|
var options = new MigrationOptions { PreviewOnly = false, Timeout = 60 };
|
||||||
@ -45,7 +42,7 @@ public void MigrateToLatest(string connectionString, MigrationType migrationType
|
|||||||
|
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
|
|
||||||
_announcer.ElapsedTime(sw.Elapsed);
|
_announcer.ElapsedTime(sw.Elapsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using FluentMigrator;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common.Instrumentation;
|
using NzbDrone.Common.Instrumentation;
|
||||||
|
|
||||||
@ -7,6 +8,7 @@ namespace NzbDrone.Core.Datastore.Migration.Framework
|
|||||||
public abstract class NzbDroneMigrationBase : FluentMigrator.Migration
|
public abstract class NzbDroneMigrationBase : FluentMigrator.Migration
|
||||||
{
|
{
|
||||||
protected readonly Logger _logger;
|
protected readonly Logger _logger;
|
||||||
|
private MigrationContext _migrationContext;
|
||||||
|
|
||||||
protected NzbDroneMigrationBase()
|
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()
|
public override void Up()
|
||||||
{
|
{
|
||||||
var context = (MigrationContext)ApplicationContext;
|
|
||||||
|
|
||||||
switch (context.MigrationType)
|
if (Context.BeforeMigration != null)
|
||||||
|
{
|
||||||
|
Context.BeforeMigration(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (Context.MigrationType)
|
||||||
{
|
{
|
||||||
case MigrationType.Main:
|
case MigrationType.Main:
|
||||||
MainDbUpgrade();
|
MainDbUpgrade();
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
using NzbDrone.Core.MediaFiles;
|
using NzbDrone.Core.MediaFiles;
|
||||||
using NzbDrone.Core.Metadata;
|
using NzbDrone.Core.Metadata;
|
||||||
using NzbDrone.Core.Metadata.Files;
|
using NzbDrone.Core.Metadata.Files;
|
||||||
|
using NzbDrone.Core.Profiles.Delay;
|
||||||
using NzbDrone.Core.RemotePathMappings;
|
using NzbDrone.Core.RemotePathMappings;
|
||||||
using NzbDrone.Core.Notifications;
|
using NzbDrone.Core.Notifications;
|
||||||
using NzbDrone.Core.Organizer;
|
using NzbDrone.Core.Organizer;
|
||||||
@ -95,6 +96,8 @@ public static void Map()
|
|||||||
Mapper.Entity<RemotePathMapping>().RegisterModel("RemotePathMappings");
|
Mapper.Entity<RemotePathMapping>().RegisterModel("RemotePathMappings");
|
||||||
Mapper.Entity<Tag>().RegisterModel("Tags");
|
Mapper.Entity<Tag>().RegisterModel("Tags");
|
||||||
Mapper.Entity<Restriction>().RegisterModel("Restrictions");
|
Mapper.Entity<Restriction>().RegisterModel("Restrictions");
|
||||||
|
|
||||||
|
Mapper.Entity<DelayProfile>().RegisterModel("DelayProfiles");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RegisterMappers()
|
private static void RegisterMappers()
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Core.Profiles.Delay;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
|
||||||
namespace NzbDrone.Core.DecisionEngine
|
namespace NzbDrone.Core.DecisionEngine
|
||||||
{
|
{
|
||||||
@ -13,20 +16,44 @@ public interface IPrioritizeDownloadDecision
|
|||||||
|
|
||||||
public class DownloadDecisionPriorizationService : IPrioritizeDownloadDecision
|
public class DownloadDecisionPriorizationService : IPrioritizeDownloadDecision
|
||||||
{
|
{
|
||||||
|
private readonly IDelayProfileService _delayProfileService;
|
||||||
|
|
||||||
|
public DownloadDecisionPriorizationService(IDelayProfileService delayProfileService)
|
||||||
|
{
|
||||||
|
_delayProfileService = delayProfileService;
|
||||||
|
}
|
||||||
|
|
||||||
public List<DownloadDecision> PrioritizeDecisions(List<DownloadDecision> decisions)
|
public List<DownloadDecision> PrioritizeDecisions(List<DownloadDecision> decisions)
|
||||||
{
|
{
|
||||||
return decisions
|
return decisions.Where(c => c.RemoteEpisode.Series != null)
|
||||||
.Where(c => c.RemoteEpisode.Series != null)
|
.GroupBy(c => c.RemoteEpisode.Series.Id, (seriesId, d) =>
|
||||||
.GroupBy(c => c.RemoteEpisode.Series.Id, (i, s) => s
|
{
|
||||||
.OrderByDescending(c => c.RemoteEpisode.ParsedEpisodeInfo.Quality, new QualityModelComparer(s.First().RemoteEpisode.Series.Profile))
|
var downloadDecisions = d.ToList();
|
||||||
.ThenBy(c => c.RemoteEpisode.Episodes.Select(e => e.EpisodeNumber).MinOrDefault())
|
var series = downloadDecisions.First().RemoteEpisode.Series;
|
||||||
.ThenBy(c => c.RemoteEpisode.Release.DownloadProtocol)
|
|
||||||
.ThenBy(c => c.RemoteEpisode.Release.Size.Round(200.Megabytes()) / Math.Max(1, c.RemoteEpisode.Episodes.Count))
|
return downloadDecisions
|
||||||
.ThenByDescending(c => TorrentInfo.GetSeeders(c.RemoteEpisode.Release))
|
.OrderByDescending(c => c.RemoteEpisode.ParsedEpisodeInfo.Quality, new QualityModelComparer(series.Profile))
|
||||||
.ThenBy(c => c.RemoteEpisode.Release.Age))
|
.ThenBy(c => c.RemoteEpisode.Episodes.Select(e => e.EpisodeNumber).MinOrDefault())
|
||||||
.SelectMany(c => c)
|
.ThenBy(c => PrioritizeDownloadProtocol(series, c.RemoteEpisode.Release.DownloadProtocol))
|
||||||
.Union(decisions.Where(c => c.RemoteEpisode.Series == null))
|
.ThenBy(c => c.RemoteEpisode.Release.Size.Round(200.Megabytes()) / Math.Max(1, c.RemoteEpisode.Episodes.Count))
|
||||||
.ToList();
|
.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.Download.Pending;
|
||||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.Profiles;
|
using NzbDrone.Core.Profiles.Delay;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
|
|
||||||
namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync
|
||||||
@ -12,12 +12,17 @@ public class DelaySpecification : IDecisionEngineSpecification
|
|||||||
{
|
{
|
||||||
private readonly IPendingReleaseService _pendingReleaseService;
|
private readonly IPendingReleaseService _pendingReleaseService;
|
||||||
private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification;
|
private readonly IQualityUpgradableSpecification _qualityUpgradableSpecification;
|
||||||
|
private readonly IDelayProfileService _delayProfileService;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public DelaySpecification(IPendingReleaseService pendingReleaseService, IQualityUpgradableSpecification qualityUpgradableSpecification, Logger logger)
|
public DelaySpecification(IPendingReleaseService pendingReleaseService,
|
||||||
|
IQualityUpgradableSpecification qualityUpgradableSpecification,
|
||||||
|
IDelayProfileService delayProfileService,
|
||||||
|
Logger logger)
|
||||||
{
|
{
|
||||||
_pendingReleaseService = pendingReleaseService;
|
_pendingReleaseService = pendingReleaseService;
|
||||||
_qualityUpgradableSpecification = qualityUpgradableSpecification;
|
_qualityUpgradableSpecification = qualityUpgradableSpecification;
|
||||||
|
_delayProfileService = delayProfileService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,71 +40,59 @@ public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
var profile = subject.Series.Profile.Value;
|
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();
|
return Decision.Accept();
|
||||||
}
|
}
|
||||||
|
|
||||||
var comparer = new QualityModelComparer(profile);
|
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);
|
foreach (var file in subject.Episodes.Where(c => c.EpisodeFileId != 0).Select(c => c.EpisodeFile.Value))
|
||||||
|
|
||||||
if (upgradable)
|
|
||||||
{
|
{
|
||||||
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");
|
var revisionUpgrade = _qualityUpgradableSpecification.IsRevisionUpgrade(file.Quality, subject.ParsedEpisodeInfo.Quality);
|
||||||
return Decision.Accept();
|
|
||||||
|
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
|
//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 bestQualityInProfile = new QualityModel(profile.LastAllowedQuality());
|
||||||
var bestCompare = comparer.Compare(subject.ParsedEpisodeInfo.Quality, bestQualityInProfile);
|
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();
|
return Decision.Accept();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile.GrabDelayMode == GrabDelayMode.Cutoff)
|
var episodeIds = subject.Episodes.Select(e => e.Id);
|
||||||
{
|
|
||||||
var cutoff = new QualityModel(profile.Cutoff);
|
|
||||||
var cutoffCompare = comparer.Compare(subject.ParsedEpisodeInfo.Quality, cutoff);
|
|
||||||
|
|
||||||
if (cutoffCompare >= 0)
|
var oldest = _pendingReleaseService.OldestPendingRelease(subject.Series.Id, episodeIds);
|
||||||
{
|
|
||||||
_logger.Debug("Quality meets or exceeds the cutoff, will not delay");
|
if (oldest != null && oldest.Release.AgeHours > delay)
|
||||||
return Decision.Accept();
|
{
|
||||||
}
|
return Decision.Accept();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile.GrabDelayMode == GrabDelayMode.First)
|
if (subject.Release.AgeHours < delay)
|
||||||
{
|
{
|
||||||
var episodeIds = subject.Episodes.Select(e => e.Id);
|
_logger.Debug("Waiting for better quality release, There is a {0} hour delay on {1}", delay, subject.Release.DownloadProtocol);
|
||||||
|
|
||||||
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);
|
|
||||||
return Decision.Reject("Waiting for better quality release");
|
return Decision.Reject("Waiting for better quality release");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,9 +5,11 @@
|
|||||||
using NzbDrone.Common;
|
using NzbDrone.Common;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.DecisionEngine;
|
using NzbDrone.Core.DecisionEngine;
|
||||||
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Core.Parser;
|
using NzbDrone.Core.Parser;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Core.Profiles.Delay;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
using NzbDrone.Core.Tv;
|
using NzbDrone.Core.Tv;
|
||||||
using NzbDrone.Core.Tv.Events;
|
using NzbDrone.Core.Tv.Events;
|
||||||
@ -20,8 +22,9 @@ public interface IPendingReleaseService
|
|||||||
void RemoveGrabbed(List<DownloadDecision> grabbed);
|
void RemoveGrabbed(List<DownloadDecision> grabbed);
|
||||||
void RemoveRejected(List<DownloadDecision> rejected);
|
void RemoveRejected(List<DownloadDecision> rejected);
|
||||||
List<ReleaseInfo> GetPending();
|
List<ReleaseInfo> GetPending();
|
||||||
List<RemoteEpisode> GetPendingRemoteEpisodes(Int32 seriesId);
|
List<RemoteEpisode> GetPendingRemoteEpisodes(int seriesId);
|
||||||
List<Queue.Queue> GetPendingQueue();
|
List<Queue.Queue> GetPendingQueue();
|
||||||
|
RemoteEpisode OldestPendingRelease(int seriesId, IEnumerable<int> episodeIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PendingReleaseService : IPendingReleaseService, IHandle<SeriesDeletedEvent>
|
public class PendingReleaseService : IPendingReleaseService, IHandle<SeriesDeletedEvent>
|
||||||
@ -29,18 +32,21 @@ public class PendingReleaseService : IPendingReleaseService, IHandle<SeriesDelet
|
|||||||
private readonly IPendingReleaseRepository _repository;
|
private readonly IPendingReleaseRepository _repository;
|
||||||
private readonly ISeriesService _seriesService;
|
private readonly ISeriesService _seriesService;
|
||||||
private readonly IParsingService _parsingService;
|
private readonly IParsingService _parsingService;
|
||||||
|
private readonly IDelayProfileService _delayProfileService;
|
||||||
private readonly IEventAggregator _eventAggregator;
|
private readonly IEventAggregator _eventAggregator;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public PendingReleaseService(IPendingReleaseRepository repository,
|
public PendingReleaseService(IPendingReleaseRepository repository,
|
||||||
ISeriesService seriesService,
|
ISeriesService seriesService,
|
||||||
IParsingService parsingService,
|
IParsingService parsingService,
|
||||||
|
IDelayProfileService delayProfileService,
|
||||||
IEventAggregator eventAggregator,
|
IEventAggregator eventAggregator,
|
||||||
Logger logger)
|
Logger logger)
|
||||||
{
|
{
|
||||||
_repository = repository;
|
_repository = repository;
|
||||||
_seriesService = seriesService;
|
_seriesService = seriesService;
|
||||||
_parsingService = parsingService;
|
_parsingService = parsingService;
|
||||||
|
_delayProfileService = delayProfileService;
|
||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
@ -138,8 +144,7 @@ public List<RemoteEpisode> GetPendingRemoteEpisodes(int seriesId)
|
|||||||
{
|
{
|
||||||
foreach (var episode in pendingRelease.RemoteEpisode.Episodes)
|
foreach (var episode in pendingRelease.RemoteEpisode.Episodes)
|
||||||
{
|
{
|
||||||
var ect = pendingRelease.Release.PublishDate.AddHours(
|
var ect = pendingRelease.Release.PublishDate.AddMinutes(GetDelay(pendingRelease.RemoteEpisode));
|
||||||
pendingRelease.RemoteEpisode.Series.Profile.Value.GrabDelay);
|
|
||||||
|
|
||||||
var queue = new Queue.Queue
|
var queue = new Queue.Queue
|
||||||
{
|
{
|
||||||
@ -162,6 +167,14 @@ public List<RemoteEpisode> GetPendingRemoteEpisodes(int seriesId)
|
|||||||
return queued;
|
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()
|
private List<PendingRelease> GetPendingReleases()
|
||||||
{
|
{
|
||||||
var result = new List<PendingRelease>();
|
var result = new List<PendingRelease>();
|
||||||
@ -225,6 +238,13 @@ private Func<PendingRelease, bool> MatchingReleasePredicate(DownloadDecision dec
|
|||||||
p.Release.Indexer == decision.RemoteEpisode.Release.Indexer;
|
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)
|
public void Handle(SeriesDeletedEvent message)
|
||||||
{
|
{
|
||||||
_repository.DeleteBySeriesId(message.Series.Id);
|
_repository.DeleteBySeriesId(message.Series.Id);
|
||||||
|
@ -230,6 +230,7 @@
|
|||||||
<Compile Include="Datastore\Migration\066_add_tags.cs" />
|
<Compile Include="Datastore\Migration\066_add_tags.cs" />
|
||||||
<Compile Include="Datastore\Migration\067_add_added_to_series.cs" />
|
<Compile Include="Datastore\Migration\067_add_added_to_series.cs" />
|
||||||
<Compile Include="Datastore\Migration\069_quality_proper.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\MigrationContext.cs" />
|
||||||
<Compile Include="Datastore\Migration\Framework\MigrationController.cs" />
|
<Compile Include="Datastore\Migration\Framework\MigrationController.cs" />
|
||||||
<Compile Include="Datastore\Migration\Framework\MigrationDbFactory.cs" />
|
<Compile Include="Datastore\Migration\Framework\MigrationDbFactory.cs" />
|
||||||
@ -260,6 +261,7 @@
|
|||||||
<Compile Include="DecisionEngine\Specifications\BlacklistSpecification.cs" />
|
<Compile Include="DecisionEngine\Specifications\BlacklistSpecification.cs" />
|
||||||
<Compile Include="DecisionEngine\Specifications\AnimeVersionUpgradeSpecification.cs" />
|
<Compile Include="DecisionEngine\Specifications\AnimeVersionUpgradeSpecification.cs" />
|
||||||
<Compile Include="DecisionEngine\Specifications\CutoffSpecification.cs" />
|
<Compile Include="DecisionEngine\Specifications\CutoffSpecification.cs" />
|
||||||
|
<Compile Include="DecisionEngine\Specifications\ProtocolSpecification.cs" />
|
||||||
<Compile Include="DecisionEngine\Specifications\LanguageSpecification.cs" />
|
<Compile Include="DecisionEngine\Specifications\LanguageSpecification.cs" />
|
||||||
<Compile Include="DecisionEngine\Specifications\NotInQueueSpecification.cs" />
|
<Compile Include="DecisionEngine\Specifications\NotInQueueSpecification.cs" />
|
||||||
<Compile Include="DecisionEngine\Specifications\ReleaseRestrictionsSpecification.cs" />
|
<Compile Include="DecisionEngine\Specifications\ReleaseRestrictionsSpecification.cs" />
|
||||||
@ -623,6 +625,10 @@
|
|||||||
<Compile Include="MetadataSource\Trakt\TraktException.cs" />
|
<Compile Include="MetadataSource\Trakt\TraktException.cs" />
|
||||||
<Compile Include="MetadataSource\TraktProxy.cs" />
|
<Compile Include="MetadataSource\TraktProxy.cs" />
|
||||||
<Compile Include="MetadataSource\Tvdb\TvdbProxy.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="Qualities\Revision.cs" />
|
||||||
<Compile Include="RemotePathMappings\RemotePathMapping.cs" />
|
<Compile Include="RemotePathMappings\RemotePathMapping.cs" />
|
||||||
<Compile Include="RemotePathMappings\RemotePathMappingRepository.cs" />
|
<Compile Include="RemotePathMappings\RemotePathMappingRepository.cs" />
|
||||||
@ -738,11 +744,10 @@
|
|||||||
<Compile Include="Parser\ParsingService.cs" />
|
<Compile Include="Parser\ParsingService.cs" />
|
||||||
<Compile Include="Parser\SceneChecker.cs" />
|
<Compile Include="Parser\SceneChecker.cs" />
|
||||||
<Compile Include="Parser\QualityParser.cs" />
|
<Compile Include="Parser\QualityParser.cs" />
|
||||||
<Compile Include="Profiles\GrabDelayMode.cs" />
|
|
||||||
<Compile Include="Profiles\Profile.cs" />
|
<Compile Include="Profiles\Profile.cs" />
|
||||||
<Compile Include="Profiles\ProfileInUseException.cs" />
|
<Compile Include="Profiles\ProfileInUseException.cs" />
|
||||||
<Compile Include="Profiles\ProfileQualityItem.cs" />
|
<Compile Include="Profiles\ProfileQualityItem.cs" />
|
||||||
<Compile Include="Profiles\ProfileRepository.cs" />
|
<Compile Include="Profiles\Delay\DelayProfileRepository.cs" />
|
||||||
<Compile Include="Profiles\ProfileService.cs" />
|
<Compile Include="Profiles\ProfileService.cs" />
|
||||||
<Compile Include="ProgressMessaging\CommandUpdatedEvent.cs" />
|
<Compile Include="ProgressMessaging\CommandUpdatedEvent.cs" />
|
||||||
<Compile Include="ProgressMessaging\ProgressMessageTarget.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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using NzbDrone.Core.Datastore;
|
using NzbDrone.Core.Datastore;
|
||||||
using NzbDrone.Core.Parser;
|
using NzbDrone.Core.Parser;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
@ -12,7 +13,10 @@ public class Profile : ModelBase
|
|||||||
public Quality Cutoff { get; set; }
|
public Quality Cutoff { get; set; }
|
||||||
public List<ProfileQualityItem> Items { get; set; }
|
public List<ProfileQualityItem> Items { get; set; }
|
||||||
public Language Language { 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/=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/=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/=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/=UnusedParameter_002ELocal/@EntryIndexedValue">WARNING</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=UseObjectOrCollectionInitializer/@EntryIndexedValue">HINT</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>
|
<s:Boolean x:Key="/Default/CodeInspection/TestFileAnalysis/SeachForOrphanedProjectFiles/@EntryValue">True</s:Boolean>
|
||||||
|
@ -26,7 +26,7 @@ define(
|
|||||||
});
|
});
|
||||||
|
|
||||||
promise.done(function () {
|
promise.done(function () {
|
||||||
self.originalModelData = self.model.toJSON();
|
self.originalModelData = JSON.stringify(self.model.toJSON());
|
||||||
});
|
});
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
@ -38,7 +38,7 @@ define(
|
|||||||
throw 'View has no model';
|
throw 'View has no model';
|
||||||
}
|
}
|
||||||
|
|
||||||
this.originalModelData = this.model.toJSON();
|
this.originalModelData = JSON.stringify(this.model.toJSON());
|
||||||
|
|
||||||
this.events = this.events || {};
|
this.events = this.events || {};
|
||||||
this.events['click .x-save'] = '_save';
|
this.events['click .x-save'] = '_save';
|
||||||
@ -63,8 +63,6 @@ define(
|
|||||||
if (self._onAfterSave) {
|
if (self._onAfterSave) {
|
||||||
self._onAfterSave.call(self);
|
self._onAfterSave.call(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.originalModelData = self.model.toJSON();
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -96,7 +94,7 @@ define(
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.prototype.onBeforeClose = function () {
|
this.prototype.onBeforeClose = function () {
|
||||||
this.model.set(this.originalModelData);
|
this.model.set(JSON.parse(this.originalModelData));
|
||||||
|
|
||||||
if (originalOnBeforeClose) {
|
if (originalOnBeforeClose) {
|
||||||
originalOnBeforeClose.call(this);
|
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) {
|
if (existing) {
|
||||||
originalAdd.call(this, existing, dontPushVal);
|
originalAdd.call(this, existing, dontPushVal);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var newTag = new TagModel();
|
else {
|
||||||
newTag.set({ label: item.toLowerCase() });
|
var newTag = new TagModel();
|
||||||
TagCollection.add(newTag);
|
newTag.set({ label: item.toLowerCase() });
|
||||||
|
TagCollection.add(newTag);
|
||||||
|
|
||||||
newTag.save().done(function () {
|
newTag.save().done(function () {
|
||||||
item = newTag.toJSON();
|
item = newTag.toJSON();
|
||||||
originalAdd.call(self, item, dontPushVal);
|
originalAdd.call(self, item, dontPushVal);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
else {
|
||||||
originalAdd.call(this, item, dontPushVal);
|
originalAdd.call(this, item, dontPushVal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.options.tag) {
|
||||||
|
self.$input.typeahead('val', '');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$.fn.tagsinput.Constructor.prototype.remove = function (item, dontPushVal) {
|
$.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);
|
originalBuild.call(this, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
define(
|
define(
|
||||||
[
|
[
|
||||||
|
'underscore',
|
||||||
'marionette',
|
'marionette',
|
||||||
'Series/Details/SeasonLayout',
|
'Series/Details/SeasonLayout',
|
||||||
'underscore'
|
'Mixins/AsSortedCollectionView'
|
||||||
], function (Marionette, SeasonLayout, _) {
|
], function (_, Marionette, SeasonLayout, AsSortedCollectionView) {
|
||||||
return Marionette.CollectionView.extend({
|
var view = Marionette.CollectionView.extend({
|
||||||
|
|
||||||
itemView: SeasonLayout,
|
itemView: SeasonLayout,
|
||||||
|
|
||||||
@ -19,27 +20,6 @@ define(
|
|||||||
this.series = options.series;
|
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 () {
|
itemViewOptions: function () {
|
||||||
return {
|
return {
|
||||||
episodeCollection: this.episodeCollection,
|
episodeCollection: this.episodeCollection,
|
||||||
@ -62,4 +42,8 @@ define(
|
|||||||
this.render();
|
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) {
|
initialize: function (options) {
|
||||||
|
|
||||||
if (!options.episodeCollection) {
|
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 () {
|
_showHideEpisodes: function () {
|
||||||
if (this.showingEpisodes) {
|
if (this.showingEpisodes) {
|
||||||
this.showingEpisodes = false;
|
this.showingEpisodes = false;
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
<span class="col-sm-2">
|
<div class="col-sm-2">
|
||||||
<div>{{host}}</div>
|
{{host}}
|
||||||
</span>
|
</div>
|
||||||
<span class="col-sm-5">
|
<div class="col-sm-5">
|
||||||
<div>{{remotePath}}</div>
|
{{remotePath}}
|
||||||
</span>
|
</div>
|
||||||
<span class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
<div>{{localPath}}</div>
|
{{localPath}}
|
||||||
</span>
|
</div>
|
||||||
<span class="col-sm-1">
|
<div class="col-sm-1">
|
||||||
<div class="pull-right"><i class="icon-nd-edit x-edit" title="" data-original-title="Edit Mapping"></i></div>
|
<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'}}
|
{{genericTagDisplay required 'label label-success'}}
|
||||||
</span>
|
</div>
|
||||||
<span class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
{{genericTagDisplay ignored 'label label-danger'}}
|
{{genericTagDisplay ignored 'label label-danger'}}
|
||||||
</span>
|
</div>
|
||||||
<span class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
{{tagDisplay tags}}
|
{{tagDisplay tags}}
|
||||||
</span>
|
</div>
|
||||||
<span class="col-sm-1">
|
<div class="col-sm-1">
|
||||||
<div class="pull-right"><i class="icon-nd-edit x-edit" title="" data-original-title="Edit"></i></div>
|
<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',
|
template: 'Settings/Profile/Edit/EditProfileViewTemplate',
|
||||||
|
|
||||||
ui: {
|
ui: {
|
||||||
cutoff : '.x-cutoff',
|
cutoff : '.x-cutoff'
|
||||||
delay : '.x-delay',
|
|
||||||
delayMode : '.x-delay-mode'
|
|
||||||
},
|
|
||||||
|
|
||||||
events: {
|
|
||||||
'change .x-delay': 'toggleDelayMode',
|
|
||||||
'keyup .x-delay': 'toggleDelayMode'
|
|
||||||
},
|
},
|
||||||
|
|
||||||
templateHelpers: function () {
|
templateHelpers: function () {
|
||||||
@ -29,30 +22,10 @@ define(
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
onShow: function () {
|
|
||||||
this.toggleDelayMode();
|
|
||||||
},
|
|
||||||
|
|
||||||
getCutoff: function () {
|
getCutoff: function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
return _.findWhere(_.pluck(this.model.get('items'), 'quality'), { id: parseInt(self.ui.cutoff.val(), 10)});
|
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>
|
</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">
|
<div class="form-group">
|
||||||
<label class="col-sm-3 control-label">Cutoff</label>
|
<label class="col-sm-3 control-label">Cutoff</label>
|
||||||
|
|
||||||
|
@ -5,22 +5,29 @@ define(
|
|||||||
'marionette',
|
'marionette',
|
||||||
'Profile/ProfileCollection',
|
'Profile/ProfileCollection',
|
||||||
'Settings/Profile/ProfileCollectionView',
|
'Settings/Profile/ProfileCollectionView',
|
||||||
|
'Settings/Profile/Delay/DelayProfileLayout',
|
||||||
|
'Settings/Profile/Delay/DelayProfileCollection',
|
||||||
'Settings/Profile/Language/LanguageCollection'
|
'Settings/Profile/Language/LanguageCollection'
|
||||||
], function (Marionette, ProfileCollection, ProfileCollectionView, LanguageCollection) {
|
], function (Marionette, ProfileCollection, ProfileCollectionView, DelayProfileLayout, DelayProfileCollection, LanguageCollection) {
|
||||||
return Marionette.Layout.extend({
|
return Marionette.Layout.extend({
|
||||||
template: 'Settings/Profile/ProfileLayoutTemplate',
|
template: 'Settings/Profile/ProfileLayoutTemplate',
|
||||||
|
|
||||||
regions: {
|
regions: {
|
||||||
profile : '#profile'
|
profile : '#profile',
|
||||||
|
delayProfile : '#delay-profile'
|
||||||
},
|
},
|
||||||
|
|
||||||
initialize: function (options) {
|
initialize: function (options) {
|
||||||
this.settings = options.settings;
|
this.settings = options.settings;
|
||||||
ProfileCollection.fetch();
|
ProfileCollection.fetch();
|
||||||
|
|
||||||
|
this.delayProfileCollection = new DelayProfileCollection();
|
||||||
|
this.delayProfileCollection.fetch();
|
||||||
},
|
},
|
||||||
|
|
||||||
onShow: function () {
|
onShow: function () {
|
||||||
this.profile.show(new ProfileCollectionView({collection: ProfileCollection}));
|
this.profile.show(new ProfileCollectionView({collection: ProfileCollection}));
|
||||||
|
this.delayProfile.show(new DelayProfileLayout({collection: this.delayProfileCollection}));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12" id="profile"/>
|
<div class="col-md-12" id="profile"/>
|
||||||
|
|
||||||
|
<div class="col-md-12 delay-profile-region" id="delay-profile"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,11 +5,8 @@
|
|||||||
|
|
||||||
<div class="language">
|
<div class="language">
|
||||||
{{languageLabel}}
|
{{languageLabel}}
|
||||||
|
|
||||||
{{#if_gt grabDelay compare="0"}}
|
|
||||||
<i class="icon-time" title="{{grabDelay}} hour, Mode: {{TitleCase grabDelayMode}}"></i>
|
|
||||||
{{/if_gt}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="allowed-qualities">
|
<ul class="allowed-qualities">
|
||||||
{{allowedLabeler}}
|
{{allowedLabeler}}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -29,3 +29,15 @@
|
|||||||
margin-bottom: 3px;
|
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;
|
padding : 5px;
|
||||||
|
|
||||||
i {
|
i {
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
|
margin-left : 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user