Release restrictions
New: Required terms assignable to series via tags New: Ignored terms assignable to series via tagss
This commit is contained in:
parent
d6ed475c63
commit
53c2962d2a
|
@ -7,6 +7,5 @@ namespace NzbDrone.Api.Config
|
|||
{
|
||||
public Int32 Retention { get; set; }
|
||||
public Int32 RssSyncInterval { get; set; }
|
||||
public String ReleaseRestrictions { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -197,6 +197,8 @@
|
|||
<Compile Include="Queue\QueueModule.cs" />
|
||||
<Compile Include="Queue\QueueResource.cs" />
|
||||
<Compile Include="ResourceChangeMessage.cs" />
|
||||
<Compile Include="Restrictions\RestrictionModule.cs" />
|
||||
<Compile Include="Restrictions\RestrictionResource.cs" />
|
||||
<Compile Include="REST\BadRequestException.cs" />
|
||||
<Compile Include="REST\ResourceValidator.cs" />
|
||||
<Compile Include="REST\RestModule.cs" />
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Api.Mapping;
|
||||
using NzbDrone.Core.Restrictions;
|
||||
|
||||
namespace NzbDrone.Api.Restrictions
|
||||
{
|
||||
public class RestrictionModule : NzbDroneRestModule<RestrictionResource>
|
||||
{
|
||||
private readonly IRestrictionService _restrictionService;
|
||||
|
||||
|
||||
public RestrictionModule(IRestrictionService restrictionService)
|
||||
{
|
||||
_restrictionService = restrictionService;
|
||||
|
||||
GetResourceById = Get;
|
||||
GetResourceAll = GetAll;
|
||||
CreateResource = Create;
|
||||
UpdateResource = Update;
|
||||
DeleteResource = Delete;
|
||||
}
|
||||
|
||||
private RestrictionResource Get(Int32 id)
|
||||
{
|
||||
return _restrictionService.Get(id).InjectTo<RestrictionResource>();
|
||||
}
|
||||
|
||||
private List<RestrictionResource> GetAll()
|
||||
{
|
||||
return ToListResource(_restrictionService.All);
|
||||
}
|
||||
|
||||
private Int32 Create(RestrictionResource resource)
|
||||
{
|
||||
return _restrictionService.Add(resource.InjectTo<Restriction>()).Id;
|
||||
}
|
||||
|
||||
private void Update(RestrictionResource resource)
|
||||
{
|
||||
_restrictionService.Update(resource.InjectTo<Restriction>());
|
||||
}
|
||||
|
||||
private void Delete(Int32 id)
|
||||
{
|
||||
_restrictionService.Delete(id);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Api.REST;
|
||||
|
||||
namespace NzbDrone.Api.Restrictions
|
||||
{
|
||||
public class RestrictionResource : RestResource
|
||||
{
|
||||
public String Required { get; set; }
|
||||
public String Preferred { get; set; }
|
||||
public String Ignored { get; set; }
|
||||
public HashSet<Int32> Tags { get; set; }
|
||||
|
||||
public RestrictionResource()
|
||||
{
|
||||
Tags = new HashSet<Int32>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -27,5 +27,15 @@ namespace NzbDrone.Common
|
|||
{
|
||||
return !source.Any();
|
||||
}
|
||||
|
||||
public static bool None<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
|
||||
{
|
||||
return !source.Any(predicate);
|
||||
}
|
||||
|
||||
public static bool NotAll<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
|
||||
{
|
||||
return !source.All(predicate);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class NotRestrictedReleaseSpecificationFixture : CoreTest<NotRestrictedReleaseSpecification>
|
||||
{
|
||||
private RemoteEpisode _parseResult;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_parseResult = new RemoteEpisode
|
||||
{
|
||||
Release = new ReleaseInfo
|
||||
{
|
||||
Title = "Dexter.S08E01.EDITED.WEBRip.x264-KYR"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_true_when_restrictions_are_empty()
|
||||
{
|
||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("KYR")]
|
||||
[TestCase("EDITED")]
|
||||
[TestCase("edited")]
|
||||
[TestCase("2HD\nKYR")]
|
||||
[TestCase("2HD\nkyr")]
|
||||
public void should_be_false_when_nzb_contains_a_restricted_term(string restrictions)
|
||||
{
|
||||
Mocker.GetMock<IConfigService>().SetupGet(c => c.ReleaseRestrictions).Returns(restrictions);
|
||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[TestCase("NotReal")]
|
||||
[TestCase("LoL")]
|
||||
[TestCase("Hello\nWorld")]
|
||||
public void should_be_true_when_nzb_does_not_contain_a_restricted_term(string restrictions)
|
||||
{
|
||||
Mocker.GetMock<IConfigService>().SetupGet(c => c.ReleaseRestrictions).Returns(restrictions);
|
||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_try_to_find_empty_string_as_a_match()
|
||||
{
|
||||
Mocker.GetMock<IConfigService>().SetupGet(c => c.ReleaseRestrictions).Returns("test\n");
|
||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Restrictions;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class ReleaseRestrictionsSpecificationFixture : CoreTest<ReleaseRestrictionsSpecification>
|
||||
{
|
||||
private RemoteEpisode _parseResult;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_parseResult = new RemoteEpisode
|
||||
{
|
||||
Series = new Series
|
||||
{
|
||||
Tags = new HashSet<Int32>()
|
||||
},
|
||||
Release = new ReleaseInfo
|
||||
{
|
||||
Title = "Dexter.S08E01.EDITED.WEBRip.x264-KYR"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void GivenRestictions(String required, String ignored)
|
||||
{
|
||||
Mocker.GetMock<IRestrictionService>()
|
||||
.Setup(s => s.AllForTags(It.IsAny<HashSet<Int32>>()))
|
||||
.Returns(new List<Restriction>
|
||||
{
|
||||
new Restriction
|
||||
{
|
||||
Required = required,
|
||||
Ignored = ignored
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_true_when_restrictions_are_empty()
|
||||
{
|
||||
Mocker.GetMock<IRestrictionService>()
|
||||
.Setup(s => s.AllForTags(It.IsAny<HashSet<Int32>>()))
|
||||
.Returns(new List<Restriction>());
|
||||
|
||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_true_when_title_contains_one_required_term()
|
||||
{
|
||||
GivenRestictions("WEBRip", null);
|
||||
|
||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_false_when_title_does_not_contain_any_required_terms()
|
||||
{
|
||||
GivenRestictions("doesnt,exist", null);
|
||||
|
||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_true_when_title_does_not_contain_any_ignored_terms()
|
||||
{
|
||||
GivenRestictions(null, "ignored");
|
||||
|
||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_false_when_title_contains_one_anded_ignored_terms()
|
||||
{
|
||||
GivenRestictions(null, "edited");
|
||||
|
||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[TestCase("EdiTED")]
|
||||
[TestCase("webrip")]
|
||||
[TestCase("X264")]
|
||||
[TestCase("X264,NOTTHERE")]
|
||||
public void should_ignore_case_when_matching_required(String required)
|
||||
{
|
||||
GivenRestictions(required, null);
|
||||
|
||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[TestCase("EdiTED")]
|
||||
[TestCase("webrip")]
|
||||
[TestCase("X264")]
|
||||
[TestCase("X264,NOTTHERE")]
|
||||
public void should_ignore_case_when_matching_ignored(String ignored)
|
||||
{
|
||||
GivenRestictions(null, ignored);
|
||||
|
||||
Subject.IsSatisfiedBy(_parseResult, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -129,7 +129,7 @@
|
|||
<Compile Include="DecisionEngineTests\LanguageSpecificationFixture.cs" />
|
||||
<Compile Include="DecisionEngineTests\MonitoredEpisodeSpecificationFixture.cs" />
|
||||
<Compile Include="DecisionEngineTests\NotInQueueSpecificationFixture.cs" />
|
||||
<Compile Include="DecisionEngineTests\NotRestrictedReleaseSpecificationFixture.cs" />
|
||||
<Compile Include="DecisionEngineTests\ReleaseRestrictionsSpecificationFixture.cs" />
|
||||
<Compile Include="DecisionEngineTests\PrioritizeDownloadDecisionFixture.cs" />
|
||||
<Compile Include="DecisionEngineTests\QualityAllowedByProfileSpecificationFixture.cs" />
|
||||
<Compile Include="DecisionEngineTests\QualityUpgradeSpecificationFixture.cs" />
|
||||
|
|
|
@ -7,8 +7,6 @@ using NzbDrone.Common.EnvironmentInfo;
|
|||
using NzbDrone.Core.Configuration.Events;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Update;
|
||||
|
||||
|
||||
namespace NzbDrone.Core.Configuration
|
||||
{
|
||||
|
@ -103,12 +101,6 @@ namespace NzbDrone.Core.Configuration
|
|||
set { SetValue("RecycleBin", value); }
|
||||
}
|
||||
|
||||
public string ReleaseRestrictions
|
||||
{
|
||||
get { return GetValue("ReleaseRestrictions", String.Empty).Trim('\r', '\n'); }
|
||||
set { SetValue("ReleaseRestrictions", value.Trim('\r', '\n')); }
|
||||
}
|
||||
|
||||
public Int32 RssSyncInterval
|
||||
{
|
||||
get { return GetValueInt("RssSyncInterval", 15); }
|
||||
|
|
|
@ -47,7 +47,6 @@ namespace NzbDrone.Core.Configuration
|
|||
//Indexers
|
||||
Int32 Retention { get; set; }
|
||||
Int32 RssSyncInterval { get; set; }
|
||||
String ReleaseRestrictions { get; set; }
|
||||
|
||||
//UI
|
||||
Int32 FirstDayOfWeek { get; set; }
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
using System.Data;
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Migration(68)]
|
||||
public class add_release_restrictions : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Create.TableForModel("Restrictions")
|
||||
.WithColumn("Required").AsString().Nullable()
|
||||
.WithColumn("Preferred").AsString().Nullable()
|
||||
.WithColumn("Ignored").AsString().Nullable()
|
||||
.WithColumn("Tags").AsString().NotNullable();
|
||||
|
||||
Execute.WithConnection(ConvertRestrictions);
|
||||
Execute.Sql("DELETE FROM Config WHERE [Key] = 'releaserestrictions'");
|
||||
}
|
||||
|
||||
private void ConvertRestrictions(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
using (IDbCommand getRestictionsCmd = conn.CreateCommand())
|
||||
{
|
||||
getRestictionsCmd.Transaction = tran;
|
||||
getRestictionsCmd.CommandText = @"SELECT [Value] FROM Config WHERE [Key] = 'releaserestrictions'";
|
||||
|
||||
using (IDataReader configReader = getRestictionsCmd.ExecuteReader())
|
||||
{
|
||||
while (configReader.Read())
|
||||
{
|
||||
var restrictions = configReader.GetString(0);
|
||||
restrictions = restrictions.Replace("\n", ",");
|
||||
|
||||
using (IDbCommand insertCmd = conn.CreateCommand())
|
||||
{
|
||||
insertCmd.Transaction = tran;
|
||||
insertCmd.CommandText = "INSERT INTO Restrictions (Ignored, Tags) VALUES ('?', '[]')";
|
||||
insertCmd.AddParameter(restrictions);
|
||||
|
||||
insertCmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ using NzbDrone.Core.Organizer;
|
|||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Restrictions;
|
||||
using NzbDrone.Core.RootFolders;
|
||||
using NzbDrone.Core.SeriesStats;
|
||||
using NzbDrone.Core.Tags;
|
||||
|
@ -92,6 +93,7 @@ namespace NzbDrone.Core.Datastore
|
|||
|
||||
Mapper.Entity<RemotePathMapping>().RegisterModel("RemotePathMappings");
|
||||
Mapper.Entity<Tag>().RegisterModel("Tags");
|
||||
Mapper.Entity<Restriction>().RegisterModel("Restrictions");
|
||||
}
|
||||
|
||||
private static void RegisterMappers()
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
using System;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class NotRestrictedReleaseSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly IConfigService _configService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public NotRestrictedReleaseSpecification(IConfigService configService, Logger logger)
|
||||
{
|
||||
_configService = configService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public RejectionType Type { get { return RejectionType.Permanent; } }
|
||||
|
||||
public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
_logger.Debug("Checking if release contains any restricted terms: {0}", subject);
|
||||
|
||||
var restrictionsString = _configService.ReleaseRestrictions;
|
||||
|
||||
if (String.IsNullOrWhiteSpace(restrictionsString))
|
||||
{
|
||||
_logger.Debug("No restrictions configured, allowing: {0}", subject);
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
var restrictions = restrictionsString.Split(new []{ '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var restriction in restrictions)
|
||||
{
|
||||
if (subject.Release.Title.ToLowerInvariant().Contains(restriction.ToLowerInvariant()))
|
||||
{
|
||||
_logger.Debug("{0} is restricted: {1}", subject, restriction);
|
||||
return Decision.Reject("Contains restricted term: {0}", restriction);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Debug("No restrictions apply, allowing: {0}", subject);
|
||||
return Decision.Accept();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Restrictions;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class ReleaseRestrictionsSpecification : IDecisionEngineSpecification
|
||||
{
|
||||
private readonly IRestrictionService _restrictionService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public ReleaseRestrictionsSpecification(IRestrictionService restrictionService, Logger logger)
|
||||
{
|
||||
_restrictionService = restrictionService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public RejectionType Type { get { return RejectionType.Permanent; } }
|
||||
|
||||
public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
|
||||
{
|
||||
_logger.Debug("Checking if release meets restrictions: {0}", subject);
|
||||
|
||||
var title = subject.Release.Title;
|
||||
var restrictions = _restrictionService.AllForTags(subject.Series.Tags);
|
||||
|
||||
var required = restrictions.Where(r => r.Required.IsNotNullOrWhiteSpace());
|
||||
var ignored = restrictions.Where(r => r.Ignored.IsNotNullOrWhiteSpace());
|
||||
|
||||
foreach (var r in required)
|
||||
{
|
||||
var split = r.Required.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
|
||||
if (!ContainsAny(split, title))
|
||||
{
|
||||
_logger.Debug("[{0}] does not contain one of the required terms: {1}", title, r.Required);
|
||||
return Decision.Reject("Does not contain one of the required terms: {0}", r.Required);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var r in ignored)
|
||||
{
|
||||
var split = r.Ignored.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList();
|
||||
|
||||
if (ContainsAny(split, title))
|
||||
{
|
||||
_logger.Debug("[{0}] contains one or more ignored terms: {1}", title, r.Ignored);
|
||||
return Decision.Reject("Contains one or more ignored terms: {0}", r.Ignored);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Debug("[{0}] No restrictions apply, allowing", subject);
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
private static Boolean ContainsAny(List<String> terms, String title)
|
||||
{
|
||||
return terms.Any(t => title.ToLowerInvariant().Contains(t.ToLowerInvariant()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -230,6 +230,7 @@
|
|||
<Compile Include="Datastore\Migration\060_remove_enable_from_indexers.cs" />
|
||||
<Compile Include="Datastore\Migration\062_convert_quality_models.cs" />
|
||||
<Compile Include="Datastore\Migration\065_make_scene_numbering_nullable.cs" />
|
||||
<Compile Include="Datastore\Migration\068_add_release_restrictions.cs" />
|
||||
<Compile Include="Datastore\Migration\066_add_tags.cs" />
|
||||
<Compile Include="Datastore\Migration\067_add_added_to_series.cs" />
|
||||
<Compile Include="Datastore\Migration\Framework\MigrationContext.cs" />
|
||||
|
@ -263,7 +264,7 @@
|
|||
<Compile Include="DecisionEngine\Specifications\CutoffSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\LanguageSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\NotInQueueSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\NotRestrictedReleaseSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\ReleaseRestrictionsSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\NotSampleSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\QualityAllowedByProfileSpecification.cs" />
|
||||
<Compile Include="DecisionEngine\Specifications\RetentionSpecification.cs">
|
||||
|
@ -711,6 +712,9 @@
|
|||
<Compile Include="Queue\QueueScheduler.cs" />
|
||||
<Compile Include="Queue\QueueService.cs" />
|
||||
<Compile Include="Queue\UpdateQueueEvent.cs" />
|
||||
<Compile Include="Restrictions\Restriction.cs" />
|
||||
<Compile Include="Restrictions\RestrictionRepository.cs" />
|
||||
<Compile Include="Restrictions\RestrictionService.cs" />
|
||||
<Compile Include="Rest\JsonNetSerializer.cs" />
|
||||
<Compile Include="Rest\RestClientFactory.cs" />
|
||||
<Compile Include="Rest\RestException.cs" />
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.Restrictions
|
||||
{
|
||||
public class Restriction : ModelBase
|
||||
{
|
||||
public String Required { get; set; }
|
||||
public String Preferred { get; set; }
|
||||
public String Ignored { get; set; }
|
||||
public HashSet<Int32> Tags { get; set; }
|
||||
|
||||
public Restriction()
|
||||
{
|
||||
Tags = new HashSet<Int32>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Restrictions
|
||||
{
|
||||
public interface IRestrictionRepository : IBasicRepository<Restriction>
|
||||
{
|
||||
}
|
||||
|
||||
public class RestrictionRepository : BasicRepository<Restriction>, IRestrictionRepository
|
||||
{
|
||||
public RestrictionRepository(IDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common;
|
||||
|
||||
namespace NzbDrone.Core.Restrictions
|
||||
{
|
||||
public interface IRestrictionService
|
||||
{
|
||||
List<Restriction> All();
|
||||
List<Restriction> AllForTag(Int32 tagId);
|
||||
List<Restriction> AllForTags(HashSet<Int32> tagIds);
|
||||
Restriction Get(Int32 id);
|
||||
void Delete(Int32 id);
|
||||
Restriction Add(Restriction restriction);
|
||||
Restriction Update(Restriction restriction);
|
||||
}
|
||||
|
||||
public class RestrictionService : IRestrictionService
|
||||
{
|
||||
private readonly IRestrictionRepository _repo;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public RestrictionService(IRestrictionRepository repo, Logger logger)
|
||||
{
|
||||
_repo = repo;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public List<Restriction> All()
|
||||
{
|
||||
return _repo.All().ToList();
|
||||
}
|
||||
|
||||
public List<Restriction> AllForTag(Int32 tagId)
|
||||
{
|
||||
return _repo.All().Where(r => r.Tags.Contains(tagId) || r.Tags.Empty()).ToList();
|
||||
}
|
||||
|
||||
public List<Restriction> AllForTags(HashSet<Int32> tagIds)
|
||||
{
|
||||
return _repo.All().Where(r => r.Tags.Intersect(tagIds).Any() || r.Tags.Empty()).ToList();
|
||||
}
|
||||
|
||||
public Restriction Get(Int32 id)
|
||||
{
|
||||
return _repo.Get(id);
|
||||
}
|
||||
|
||||
public void Delete(Int32 id)
|
||||
{
|
||||
_repo.Delete(id);
|
||||
}
|
||||
|
||||
public Restriction Add(Restriction restriction)
|
||||
{
|
||||
return _repo.Insert(restriction);
|
||||
}
|
||||
|
||||
public Restriction Update(Restriction restriction)
|
||||
{
|
||||
return _repo.Update(restriction);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,12 +9,14 @@ define(
|
|||
], function ($, _, TagCollection, TagModel) {
|
||||
|
||||
var originalAdd = $.fn.tagsinput.Constructor.prototype.add;
|
||||
var originalRemove = $.fn.tagsinput.Constructor.prototype.remove;
|
||||
var originalBuild = $.fn.tagsinput.Constructor.prototype.build;
|
||||
|
||||
$.fn.tagsinput.Constructor.prototype.add = function (item, dontPushVal) {
|
||||
var self = this;
|
||||
var tagLimitations = new RegExp('[^-_a-z0-9]', 'i');
|
||||
|
||||
if (typeof item === 'string') {
|
||||
if (typeof item === 'string' && this.options.tag) {
|
||||
|
||||
if (item === null || item === '' || tagLimitations.test(item)) {
|
||||
return;
|
||||
|
@ -42,6 +44,34 @@ define(
|
|||
}
|
||||
};
|
||||
|
||||
$.fn.tagsinput.Constructor.prototype.remove = function (item, dontPushVal) {
|
||||
if (item === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
originalRemove.call(this, item, dontPushVal);
|
||||
};
|
||||
|
||||
$.fn.tagsinput.Constructor.prototype.build = function (options) {
|
||||
var self = this;
|
||||
var defaults = {
|
||||
confirmKeys : [9, 13, 32, 44, 59] //tab, enter, space, comma, semi-colon
|
||||
};
|
||||
|
||||
options = $.extend({}, defaults, options);
|
||||
|
||||
self.$input.on('keydown', function (event) {
|
||||
if (event.which === 9) {
|
||||
var e = $.Event('keypress');
|
||||
e.which = 9;
|
||||
self.$input.trigger(e);
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
originalBuild.call(this, options);
|
||||
};
|
||||
|
||||
$.fn.tagInput = function (options) {
|
||||
var input = this;
|
||||
var model = options.model;
|
||||
|
@ -49,10 +79,11 @@ define(
|
|||
var tags = getExistingTags(model.get(property));
|
||||
|
||||
var tagInput = $(this).tagsinput({
|
||||
freeInput: true,
|
||||
itemValue : 'id',
|
||||
itemText : 'label',
|
||||
trimValue : true,
|
||||
tag : true,
|
||||
freeInput : true,
|
||||
itemValue : 'id',
|
||||
itemText : 'label',
|
||||
trimValue : true,
|
||||
typeaheadjs : {
|
||||
name: 'tags',
|
||||
displayKey: 'label',
|
||||
|
|
|
@ -24,7 +24,6 @@ define(
|
|||
'click .x-remove': '_removeSeries'
|
||||
},
|
||||
|
||||
|
||||
initialize: function () {
|
||||
this.model.set('profiles', Profiles);
|
||||
},
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<legend>Remote Path Mappings</legend>
|
||||
|
||||
<div class="col-md-12">
|
||||
<div id="remotepath-mapping-list">
|
||||
<div class="remotepath-header x-header hidden-xs">
|
||||
<div class="rule-setting-list">
|
||||
<div class="rule-setting-header x-header hidden-xs">
|
||||
<div class="row">
|
||||
<span class="col-sm-2">Host</span>
|
||||
<span class="col-sm-5">Remote Path</span>
|
||||
|
@ -12,9 +12,9 @@
|
|||
</div>
|
||||
<div class="rows x-rows">
|
||||
</div>
|
||||
<div class="remotepath-footer">
|
||||
<div class="rule-setting-footer">
|
||||
<div class="pull-right">
|
||||
<span class="add-remotepath-mapping">
|
||||
<span class="add-rule-setting-mapping">
|
||||
<i class="icon-nd-add x-add" title="Add new mapping" />
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -31,30 +31,3 @@
|
|||
width: 33%;
|
||||
}
|
||||
}
|
||||
|
||||
.add-remotepath-mapping {
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
|
||||
i {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
#remotepath-mapping-list {
|
||||
|
||||
.remotepath-header .row {
|
||||
font-weight: bold;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.rows .row {
|
||||
line-height : 30px;
|
||||
border-top : 1px solid #ddd;
|
||||
vertical-align : middle;
|
||||
padding : 5px;
|
||||
}
|
||||
}
|
|
@ -4,25 +4,32 @@ define([
|
|||
'marionette',
|
||||
'Settings/Indexers/IndexerCollection',
|
||||
'Settings/Indexers/IndexerCollectionView',
|
||||
'Settings/Indexers/Options/IndexerOptionsView'
|
||||
], function (Marionette, IndexerCollection, CollectionView, OptionsView) {
|
||||
'Settings/Indexers/Options/IndexerOptionsView',
|
||||
'Settings/Indexers/Restriction/RestrictionCollection',
|
||||
'Settings/Indexers/Restriction/RestrictionCollectionView'
|
||||
], function (Marionette, IndexerCollection, CollectionView, OptionsView, RestrictionCollection, RestrictionCollectionView) {
|
||||
|
||||
return Marionette.Layout.extend({
|
||||
template: 'Settings/Indexers/IndexerLayoutTemplate',
|
||||
|
||||
regions: {
|
||||
indexers : '#x-indexers-region',
|
||||
indexerOptions : '#x-indexer-options-region'
|
||||
indexers : '#x-indexers-region',
|
||||
indexerOptions : '#x-indexer-options-region',
|
||||
restriction : '#x-restriction-region'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
initialize: function () {
|
||||
this.indexersCollection = new IndexerCollection();
|
||||
this.indexersCollection.fetch();
|
||||
|
||||
this.restrictionCollection = new RestrictionCollection();
|
||||
this.restrictionCollection.fetch();
|
||||
},
|
||||
|
||||
onShow: function () {
|
||||
this.indexers.show(new CollectionView({ collection: this.indexersCollection }));
|
||||
this.indexerOptions.show(new OptionsView({ model: this.model }));
|
||||
this.restriction.show(new RestrictionCollectionView({ collection: this.restrictionCollection }));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<div id="x-indexers-region"></div>
|
||||
<div class="form-horizontal">
|
||||
<div id="x-indexer-options-region"></div>
|
||||
<div id="x-restriction-region"></div>
|
||||
</div>
|
||||
|
|
|
@ -21,17 +21,4 @@
|
|||
<input type="number" name="rssSyncInterval" class="form-control"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group advanced-setting">
|
||||
<label class="col-sm-3 control-label">Release Restrictions</label>
|
||||
|
||||
<div class="col-sm-1 col-sm-push-4 help-inline help-inline-text-area">
|
||||
<i class="icon-nd-form-info" title="Blacklist NZBs based on these words (case-insensitive)"/>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4 col-sm-pull-1">
|
||||
<textarea rows="3" name="releaseRestrictions" class="form-control release-restrictions"></textarea>
|
||||
<div class="text-area-help">Newline-delimited set of rules</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
'use strict';
|
||||
define([
|
||||
'backbone',
|
||||
'Settings/Indexers/Restriction/RestrictionModel'
|
||||
], function (Backbone, RestrictionModel) {
|
||||
|
||||
return Backbone.Collection.extend({
|
||||
model : RestrictionModel,
|
||||
url : window.NzbDrone.ApiRoot + '/Restriction'
|
||||
});
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
'use strict';
|
||||
define([
|
||||
'AppLayout',
|
||||
'marionette',
|
||||
'Settings/Indexers/Restriction/RestrictionItemView',
|
||||
'Settings/Indexers/Restriction/RestrictionEditView',
|
||||
'Tags/TagHelpers',
|
||||
'bootstrap'
|
||||
], function (AppLayout, Marionette, RestrictionItemView, EditView) {
|
||||
|
||||
return Marionette.CompositeView.extend({
|
||||
template : 'Settings/Indexers/Restriction/RestrictionCollectionViewTemplate',
|
||||
itemViewContainer : '.x-rows',
|
||||
itemView : RestrictionItemView,
|
||||
|
||||
events: {
|
||||
'click .x-add' : '_addMapping'
|
||||
},
|
||||
|
||||
_addMapping: function() {
|
||||
var model = this.collection.create({ tags: [] });
|
||||
|
||||
var view = new EditView({ model: model, targetCollection: this.collection});
|
||||
AppLayout.modalRegion.show(view);
|
||||
}
|
||||
});
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
<fieldset class="advanced-setting">
|
||||
<legend>Restrictions</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-4">Must Contain</span>
|
||||
<span class="col-sm-4">Must Not Contain</span>
|
||||
<span class="col-sm-3">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 restriction" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
|
@ -0,0 +1,23 @@
|
|||
'use strict';
|
||||
|
||||
define([
|
||||
'vent',
|
||||
'marionette'
|
||||
], function (vent, Marionette) {
|
||||
return Marionette.ItemView.extend({
|
||||
template: 'Settings/Indexers/Restriction/RestrictionDeleteViewTemplate',
|
||||
|
||||
events: {
|
||||
'click .x-confirm-delete': '_delete'
|
||||
},
|
||||
|
||||
_delete: function () {
|
||||
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 Restriction</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete the restriction for '{{localPath}}'?</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>
|
|
@ -0,0 +1,61 @@
|
|||
'use strict';
|
||||
|
||||
define([
|
||||
'underscore',
|
||||
'vent',
|
||||
'AppLayout',
|
||||
'marionette',
|
||||
'Settings/Indexers/Restriction/RestrictionDeleteView',
|
||||
'Commands/CommandController',
|
||||
'Mixins/AsModelBoundView',
|
||||
'Mixins/AsValidatedView',
|
||||
'Mixins/AsEditModalView',
|
||||
'Mixins/TagInput',
|
||||
'bootstrap',
|
||||
'bootstrap.tagsinput'
|
||||
], function (_, vent, AppLayout, Marionette, DeleteView, CommandController, AsModelBoundView, AsValidatedView, AsEditModalView) {
|
||||
|
||||
var view = Marionette.ItemView.extend({
|
||||
template : 'Settings/Indexers/Restriction/RestrictionEditViewTemplate',
|
||||
|
||||
ui : {
|
||||
required : '.x-required',
|
||||
ignored : '.x-ignored',
|
||||
tags : '.x-tags'
|
||||
},
|
||||
|
||||
_deleteView: DeleteView,
|
||||
|
||||
initialize : function (options) {
|
||||
this.targetCollection = options.targetCollection;
|
||||
},
|
||||
|
||||
onRender : function () {
|
||||
this.ui.required.tagsinput({
|
||||
trimValue : true,
|
||||
tagClass : 'label label-success'
|
||||
});
|
||||
|
||||
this.ui.ignored.tagsinput({
|
||||
trimValue : true,
|
||||
tagClass : 'label label-danger'
|
||||
});
|
||||
|
||||
this.ui.tags.tagInput({
|
||||
model : this.model,
|
||||
property : 'tags'
|
||||
});
|
||||
},
|
||||
|
||||
_onAfterSave : function () {
|
||||
this.targetCollection.add(this.model, { merge : true });
|
||||
vent.trigger(vent.Commands.CloseModalCommand);
|
||||
}
|
||||
});
|
||||
|
||||
AsModelBoundView.call(view);
|
||||
AsValidatedView.call(view);
|
||||
AsEditModalView.call(view);
|
||||
|
||||
return view;
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
{{#if id}}
|
||||
<h3>Edit Restriction</h3>
|
||||
{{else}}
|
||||
<h3>Add Restriction</h3>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="modal-body remotepath-mapping-modal">
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">Must contain</label>
|
||||
|
||||
<div class="col-sm-1 col-sm-push-5 help-inline">
|
||||
<i class="icon-nd-form-info" title="The release must contain at least one of these terms (case insensitive)" />
|
||||
</div>
|
||||
|
||||
<div class="col-sm-5 col-sm-pull-1">
|
||||
<input type="text" name="required" class="form-control x-required"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-sm-3 control-label">Must not contain</label>
|
||||
|
||||
<div class="col-sm-1 col-sm-push-5 help-inline">
|
||||
<i class="icon-nd-form-info" title="The release will be rejected if it contains one or more of terms (case insensitive)" />
|
||||
</div>
|
||||
|
||||
<div class="col-sm-5 col-sm-pull-1">
|
||||
<input type="text" name="ignored" class="form-control x-ignored"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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="Restrictions will apply to series with more or more matching tags. Leave blank to apply to all series" />
|
||||
</div>
|
||||
|
||||
<div class="col-sm-5 col-sm-pull-1">
|
||||
<input type="text" class="form-control x-tags">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
{{#if id}}
|
||||
<button class="btn btn-danger pull-left x-delete">delete</button>
|
||||
{{/if}}
|
||||
|
||||
<button class="btn" data-dismiss="modal">cancel</button>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-primary x-save">save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,30 @@
|
|||
'use strict';
|
||||
|
||||
define([
|
||||
'AppLayout',
|
||||
'marionette',
|
||||
'Settings/Indexers/Restriction/RestrictionEditView'
|
||||
], function (AppLayout, Marionette, EditView) {
|
||||
|
||||
return Marionette.ItemView.extend({
|
||||
template : 'Settings/Indexers/Restriction/RestrictionItemViewTemplate',
|
||||
className : 'row',
|
||||
|
||||
ui: {
|
||||
tags: '.x-tags'
|
||||
},
|
||||
|
||||
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,12 @@
|
|||
<span class="col-sm-4">
|
||||
{{genericTagDisplay required 'label label-success'}}
|
||||
</span>
|
||||
<span class="col-sm-4">
|
||||
{{genericTagDisplay ignored 'label label-danger'}}
|
||||
</span>
|
||||
<span class="col-sm-3">
|
||||
{{tagDisplay tags}}
|
||||
</span>
|
||||
<span class="col-sm-1">
|
||||
<div class="pull-right"><i class="icon-nd-edit x-edit" title="" data-original-title="Edit"></i></div>
|
||||
</span>
|
|
@ -0,0 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
define([
|
||||
'jquery',
|
||||
'backbone.deepmodel'
|
||||
], function ($, DeepModel) {
|
||||
return DeepModel.DeepModel.extend({
|
||||
|
||||
});
|
||||
});
|
|
@ -127,3 +127,34 @@ li.save-and-add:hover {
|
|||
display : none;
|
||||
padding-right : 5px;
|
||||
}
|
||||
|
||||
.add-rule-setting-mapping {
|
||||
cursor : pointer;
|
||||
font-size : 14px;
|
||||
text-align : center;
|
||||
display : inline-block;
|
||||
padding : 2px 6px;
|
||||
|
||||
i {
|
||||
cursor : pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.rule-setting-list {
|
||||
|
||||
.rule-setting-header .row {
|
||||
font-weight : bold;
|
||||
line-height : 40px;
|
||||
}
|
||||
|
||||
.rows .row {
|
||||
line-height : 30px;
|
||||
border-top : 1px solid #ddd;
|
||||
vertical-align : middle;
|
||||
padding : 5px;
|
||||
|
||||
i {
|
||||
cursor : pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,32 @@
|
|||
'use strict';
|
||||
define(
|
||||
[
|
||||
'underscore',
|
||||
'handlebars',
|
||||
'Tags/TagCollection'
|
||||
], function (Handlebars, TagCollection) {
|
||||
], function (_, Handlebars, TagCollection) {
|
||||
|
||||
Handlebars.registerHelper('tagInput', function () {
|
||||
Handlebars.registerHelper('tagDisplay', function (tags) {
|
||||
|
||||
var unit = 'days';
|
||||
var age = this.age;
|
||||
var tagLabels = _.map(TagCollection.filter(function (tag) {
|
||||
return _.contains(tags, tag.get('id'));
|
||||
}), function (tag){
|
||||
return '<span class="label label-info">{0}</span>'.format(tag.get('label'));
|
||||
});
|
||||
|
||||
if (age < 2) {
|
||||
unit = 'hours';
|
||||
age = parseFloat(this.ageHours).toFixed(1);
|
||||
return new Handlebars.SafeString(tagLabels.join(' '));
|
||||
});
|
||||
|
||||
Handlebars.registerHelper('genericTagDisplay', function (tags, classes) {
|
||||
|
||||
if (!tags) {
|
||||
return new Handlebars.SafeString('');
|
||||
}
|
||||
|
||||
return new Handlebars.SafeString('<dt>Age (when grabbed):</dt><dd>{0} {1}</dd>'.format(age, unit));
|
||||
var tagLabels = _.map(tags.split(','), function (tag) {
|
||||
return '<span class="{0}">{1}</span>'.format(classes, tag);
|
||||
});
|
||||
|
||||
return new Handlebars.SafeString(tagLabels.join(' '));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<input type=""/>
|
Loading…
Reference in New Issue