From 6dab689533ebc3637eef83a5d98cc4ef275411ec Mon Sep 17 00:00:00 2001 From: Alessio Gogna <5177307+alecsg77@users.noreply.github.com> Date: Sat, 15 May 2021 23:52:12 +0200 Subject: [PATCH] [feature] Indexer Test status filter (#11705) Partial solution for #3292 --- README.md | 19 ++-- src/Jackett.Common/Content/custom.js | 11 +++ src/Jackett.Common/Utils/FilterFunc.cs | 9 +- .../Utils/FilterFuncs/TestFilterFunc.cs | 28 ++++++ .../{IndexerStub.cs => IndexerBaseStub.cs} | 2 +- .../Utils/FilterFuncs/LanguageFuncTests.cs | 18 ++-- .../Common/Utils/FilterFuncs/TagFuncTests.cs | 14 +-- .../Common/Utils/FilterFuncs/TestFuncTests.cs | 91 +++++++++++++++++++ .../Common/Utils/FilterFuncs/TypeFuncTests.cs | 17 ++-- 9 files changed, 170 insertions(+), 39 deletions(-) create mode 100644 src/Jackett.Common/Utils/FilterFuncs/TestFilterFunc.cs rename src/Jackett.Test/Common/Utils/FilterFuncs/{IndexerStub.cs => IndexerBaseStub.cs} (97%) create mode 100644 src/Jackett.Test/Common/Utils/FilterFuncs/TestFuncTests.cs diff --git a/README.md b/README.md index 4ebf8197d..38706d988 100644 --- a/README.md +++ b/README.md @@ -586,25 +586,26 @@ To get all Jackett indexers including their capabilities you can use `t=indexers ### Filter indexers -Another special "filter" indexer is avaible at /api/v2.0/indexers/filter/results/torznab -It will query the configured indexers that match the filter expression criterias and return the combined results as "all". +Another special "filter" indexer is available at `/api/v2.0/indexers//results/torznab` +It will query the configured indexers that match the `` expression criterias and return the combined results as "all". Supported filters Filter | Condition -|- -type:type | where the indexer type is equal to type -tag:tag | where the indexer tags contains tag -lang:tag | where the indexer language start with lang +`type:` | where the indexer type is equal to `` +`tag:` | where the indexer tags contains `` +`lang:` | where the indexer language start with `` +`test:{passed|failed}` | where the last indexer test performed `passed` or `failed` Supported operators Operator | Condition -|- -!filter | where not filter -filter1+filter2+... | where filter1 and filter2 and ...filter1,filter2,... | where filter1 or filter2 or ...` | where not `` +`+[+...]` | where `` and `` [and ``...] +`,[+...]` | where `` or `` [or ``...] Example: -The "filter" indexer at /api/v2.0/indexers/tag:group1,!type:private+lang:en/results/torznab will query all the configured indexers tagged with `group1` or all the indexers not private and with `en` language (`en-en`,`en-us`,...) +The "filter" indexer at `/api/v2.0/indexers/tag:group1,!type:private+lang:en/results/torznab` will query all the configured indexers tagged with `group1` or all the indexers not private and with `en` language (`en-en`,`en-us`,...) ## Installation on Windows We recommend you install Jackett as a Windows service using the supplied installer. You may also download the zipped version if you would like to configure everything manually. diff --git a/src/Jackett.Common/Content/custom.js b/src/Jackett.Common/Content/custom.js index 5bbf2a369..694e44ea8 100644 --- a/src/Jackett.Common/Content/custom.js +++ b/src/Jackett.Common/Content/custom.js @@ -84,6 +84,10 @@ function tag_filter(indexer) { return indexer.tags.map(t => t.toLowerCase()).indexOf(this.value.toLowerCase()) > -1; } +function state_filter(indexer) { + return indexer.state == this.value; +} + function getJackettConfig(callback) { api.getServerConfig(callback).fail(function () { doNotify("Error loading Jackett settings, request to Jackett server failed, is server running ?", "danger", "glyphicon glyphicon-alert"); @@ -211,6 +215,9 @@ function configureFilters(indexers) { availableFilters.push(f); } + availableFilters.push({id: "test:passed", apply: state_filter, value: "success" }); + availableFilters.push({id: "test:failed", apply: state_filter, value: "error" }); + ["public", "private", "semi-private"] .map(t => { return { id: "type:" + t, apply: type_filter, value: t } }) .forEach(add); @@ -606,6 +613,10 @@ function updateTestState(id, state, message, parent) { }).rows().invalidate('dom'); if (state != "inprogres") dt.draw(); + + var indexer = configuredIndexers.find(x => x.id == id); + if (indexer) + indexer.state = state; } function testIndexer(id, notifyResult) { diff --git a/src/Jackett.Common/Utils/FilterFunc.cs b/src/Jackett.Common/Utils/FilterFunc.cs index 2f7f71a29..46153c224 100644 --- a/src/Jackett.Common/Utils/FilterFunc.cs +++ b/src/Jackett.Common/Utils/FilterFunc.cs @@ -16,10 +16,11 @@ namespace Jackett.Common.Utils }); public static readonly FilterFuncComponent Language = Component("lang", args => indexer => indexer.Language.StartsWith(args, StringComparison.InvariantCultureIgnoreCase)); public static readonly FilterFuncComponent Type = Component("type", args => indexer => string.Equals(indexer.Type, args, StringComparison.InvariantCultureIgnoreCase)); + public static readonly FilterFuncComponent Test = TestFilterFunc.Default; static FilterFunc() { - Expression = new FilterFuncExpression(Tag, Language, Type); + Expression = new FilterFuncExpression(Tag, Language, Type, Test); } public static bool TryParse(string source, out Func func) @@ -49,10 +50,10 @@ namespace Jackett.Common.Utils public override Func ToFunc(string args) { var func = builder(args); - return indexer => indexer != null - ? indexer.IsConfigured && func(indexer) - : throw new ArgumentNullException(nameof(indexer)); + return indexer => IsValid(indexer) && func(indexer); } } + + protected static bool IsValid(IIndexer indexer) => (indexer?.IsConfigured ?? throw new ArgumentNullException(nameof(indexer))); } } diff --git a/src/Jackett.Common/Utils/FilterFuncs/TestFilterFunc.cs b/src/Jackett.Common/Utils/FilterFuncs/TestFilterFunc.cs new file mode 100644 index 000000000..35d7a1f99 --- /dev/null +++ b/src/Jackett.Common/Utils/FilterFuncs/TestFilterFunc.cs @@ -0,0 +1,28 @@ +using System; + +using Jackett.Common.Indexers; + +namespace Jackett.Common.Utils.FilterFuncs +{ + public class TestFilterFunc : FilterFuncComponent + { + public static readonly TestFilterFunc Default = new TestFilterFunc(); + public const string Passed = "passed"; + public const string Failed = "failed"; + + private TestFilterFunc() : base("test") + { + } + + public override Func ToFunc(string args) + { + if (args == null) + throw new ArgumentNullException(nameof(args)); + if (string.Equals(Passed, args, StringComparison.InvariantCultureIgnoreCase)) + return i => IsValid(i) && i.LastError == null; + if (string.Equals(Failed, args, StringComparison.InvariantCultureIgnoreCase)) + return i => IsValid(i) && i.LastError != null; + throw new ArgumentException($"Invalid filter. Status should be '{Passed}' or '{Failed}'", nameof(args)); + } + } +} diff --git a/src/Jackett.Test/Common/Utils/FilterFuncs/IndexerStub.cs b/src/Jackett.Test/Common/Utils/FilterFuncs/IndexerBaseStub.cs similarity index 97% rename from src/Jackett.Test/Common/Utils/FilterFuncs/IndexerStub.cs rename to src/Jackett.Test/Common/Utils/FilterFuncs/IndexerBaseStub.cs index d22db58a9..6962d7a51 100644 --- a/src/Jackett.Test/Common/Utils/FilterFuncs/IndexerStub.cs +++ b/src/Jackett.Test/Common/Utils/FilterFuncs/IndexerBaseStub.cs @@ -8,7 +8,7 @@ using Newtonsoft.Json.Linq; namespace Jackett.Test.Common.Utils.FilterFuncs { - public class IndexerStub : IIndexer + public abstract class IndexerBaseStub : IIndexer { public virtual string SiteLink => throw TestExceptions.UnexpectedInvocation; diff --git a/src/Jackett.Test/Common/Utils/FilterFuncs/LanguageFuncTests.cs b/src/Jackett.Test/Common/Utils/FilterFuncs/LanguageFuncTests.cs index 9fc427dbe..946e8aed2 100644 --- a/src/Jackett.Test/Common/Utils/FilterFuncs/LanguageFuncTests.cs +++ b/src/Jackett.Test/Common/Utils/FilterFuncs/LanguageFuncTests.cs @@ -6,9 +6,9 @@ namespace Jackett.Test.Common.Utils.FilterFuncs [TestFixture] public class LanguageFuncTests { - private class LanguageIndexerStub : IndexerStub + private class IndexerStub : IndexerBaseStub { - public LanguageIndexerStub(string language) + public IndexerStub(string language) { Language = language; } @@ -24,19 +24,19 @@ namespace Jackett.Test.Common.Utils.FilterFuncs var language = "en"; var region = "US"; - var lrLanguage = new LanguageIndexerStub($"{language.ToLower()}-{region.ToLower()}"); + var lrLanguage = new IndexerStub(language: $"{language.ToLower()}-{region.ToLower()}"); var LRFilterFunc = Language.ToFunc($"{language.ToUpper()}-{region.ToUpper()}"); Assert.IsTrue(LRFilterFunc(lrLanguage)); - var lRLanguage = new LanguageIndexerStub($"{language.ToLower()}-{region.ToUpper()}"); + var lRLanguage = new IndexerStub(language: $"{language.ToLower()}-{region.ToUpper()}"); var LrFilterFunc = Language.ToFunc($"{language.ToUpper()}-{region.ToLower()}"); Assert.IsTrue(LrFilterFunc(lRLanguage)); - var LrLanguage = new LanguageIndexerStub($"{language.ToUpper()}-{region.ToLower()}"); + var LrLanguage = new IndexerStub(language: $"{language.ToUpper()}-{region.ToLower()}"); var lRFilterFunc = Language.ToFunc($"{language.ToLower()}-{region.ToUpper()}"); Assert.IsTrue(lRFilterFunc(LrLanguage)); - var LRLanguage = new LanguageIndexerStub($"{language.ToUpper()}-{region.ToUpper()}"); + var LRLanguage = new IndexerStub(language: $"{language.ToUpper()}-{region.ToUpper()}"); var lrFilterFunc = Language.ToFunc($"{language.ToLower()}-{region.ToLower()}"); Assert.IsTrue(lrFilterFunc(LRLanguage)); } @@ -47,9 +47,9 @@ namespace Jackett.Test.Common.Utils.FilterFuncs var language = "en"; var funcFilter = Language.ToFunc(language); - Assert.IsTrue(funcFilter(new LanguageIndexerStub(language))); - Assert.IsTrue(funcFilter(new LanguageIndexerStub($"{language}-region1"))); - Assert.IsFalse(funcFilter(new LanguageIndexerStub($"language2-{language}"))); + Assert.IsTrue(funcFilter(new IndexerStub(language: language))); + Assert.IsTrue(funcFilter(new IndexerStub(language: $"{language}-region1"))); + Assert.IsFalse(funcFilter(new IndexerStub(language: $"language2-{language}"))); } } } diff --git a/src/Jackett.Test/Common/Utils/FilterFuncs/TagFuncTests.cs b/src/Jackett.Test/Common/Utils/FilterFuncs/TagFuncTests.cs index af7795163..c4a021d7f 100644 --- a/src/Jackett.Test/Common/Utils/FilterFuncs/TagFuncTests.cs +++ b/src/Jackett.Test/Common/Utils/FilterFuncs/TagFuncTests.cs @@ -6,9 +6,9 @@ namespace Jackett.Test.Common.Utils.FilterFuncs [TestFixture] public class TagFuncTests { - private class TagsIndexerStub : IndexerStub + private class IndexerStub : IndexerBaseStub { - public TagsIndexerStub(params string[] tags) + public IndexerStub(params string[] tags) { Tags = tags; } @@ -23,7 +23,7 @@ namespace Jackett.Test.Common.Utils.FilterFuncs { var tagId = "g1"; - var tag = new TagsIndexerStub(tagId); + var tag = new IndexerStub(tagId); var upperTarget = Tag.ToFunc(tagId.ToUpper()); Assert.IsTrue(upperTarget(tag)); @@ -38,10 +38,10 @@ namespace Jackett.Test.Common.Utils.FilterFuncs var tagId = "g1"; var target = Tag.ToFunc(tagId); - Assert.IsTrue(target(new TagsIndexerStub(tagId))); - Assert.IsTrue(target(new TagsIndexerStub(tagId, "g2"))); - Assert.IsTrue(target(new TagsIndexerStub("g2", tagId))); - Assert.IsFalse(target(new TagsIndexerStub("g2"))); + Assert.IsTrue(target(new IndexerStub(tagId))); + Assert.IsTrue(target(new IndexerStub(tagId, "g2"))); + Assert.IsTrue(target(new IndexerStub("g2", tagId))); + Assert.IsFalse(target(new IndexerStub("g2"))); } } } diff --git a/src/Jackett.Test/Common/Utils/FilterFuncs/TestFuncTests.cs b/src/Jackett.Test/Common/Utils/FilterFuncs/TestFuncTests.cs new file mode 100644 index 000000000..a97577575 --- /dev/null +++ b/src/Jackett.Test/Common/Utils/FilterFuncs/TestFuncTests.cs @@ -0,0 +1,91 @@ +using System; +using Jackett.Common.Utils; +using Jackett.Common.Utils.FilterFuncs; +using Jackett.Test.TestHelpers; +using NUnit.Framework; + +namespace Jackett.Test.Common.Utils.FilterFuncs +{ + [TestFixture] + public class TestFuncTests + { + private static readonly IndexerStub HealthyIndexer = new IndexerStub(lastError: null); + private static readonly IndexerStub ErrorIndexer = new IndexerStub(lastError: "error"); + + private class IndexerStub : IndexerBaseStub + { + private readonly string lastError; + + public IndexerStub(string lastError) + { + this.lastError = lastError; + } + + public override bool IsConfigured => true; + + public override string LastError + { + get => lastError; + set => throw TestExceptions.UnexpectedInvocation; + } + } + + [Test] + public void NullStatus_ThrowsException() + { + Assert.Throws(() => FilterFunc.Test.ToFunc(null)); + } + + [Test] + public void EmptyStatus_ThrowsException() + { + Assert.Throws(() => FilterFunc.Test.ToFunc(string.Empty)); + } + + [Test] + public void InvalidStatus_ThrowsException() + { + Assert.Throws(() => FilterFunc.Test.ToFunc(TestFilterFunc.Passed + TestFilterFunc.Failed)); + } + + [Test] + public void PassedFilter() + { + var passedFilterFunc = FilterFunc.Test.ToFunc(TestFilterFunc.Passed); + Assert.IsTrue(passedFilterFunc(HealthyIndexer)); + Assert.IsFalse(passedFilterFunc(ErrorIndexer)); + } + + [Test] + public void FailedFilter() + { + var failedFilterFunc = FilterFunc.Test.ToFunc(TestFilterFunc.Failed); + Assert.IsFalse(failedFilterFunc(HealthyIndexer)); + Assert.IsTrue(failedFilterFunc(ErrorIndexer)); + } + + [Test] + public void PassedFilter_CaseInsensitiveSource() + { + var upperFilterFunc = FilterFunc.Test.ToFunc(TestFilterFunc.Passed.ToUpper()); + Assert.IsTrue(upperFilterFunc(HealthyIndexer)); + Assert.IsFalse(upperFilterFunc(ErrorIndexer)); + + var lowerFilterFunc = FilterFunc.Test.ToFunc(TestFilterFunc.Passed.ToLower()); + Assert.IsTrue(lowerFilterFunc(HealthyIndexer)); + Assert.IsFalse(lowerFilterFunc(ErrorIndexer)); + } + + [Test] + public void FailedFilter_CaseInsensitiveSource() + { + var upperFilterFunc = FilterFunc.Test.ToFunc(TestFilterFunc.Failed.ToUpper()); + Assert.IsFalse(upperFilterFunc(HealthyIndexer)); + Assert.IsTrue(upperFilterFunc(ErrorIndexer)); + + var lowerFilterFunc = FilterFunc.Test.ToFunc(TestFilterFunc.Failed.ToLower()); + Assert.IsFalse(lowerFilterFunc(HealthyIndexer)); + Assert.IsTrue(lowerFilterFunc(ErrorIndexer)); + } + } +} diff --git a/src/Jackett.Test/Common/Utils/FilterFuncs/TypeFuncTests.cs b/src/Jackett.Test/Common/Utils/FilterFuncs/TypeFuncTests.cs index 28ead99b0..1722edecb 100644 --- a/src/Jackett.Test/Common/Utils/FilterFuncs/TypeFuncTests.cs +++ b/src/Jackett.Test/Common/Utils/FilterFuncs/TypeFuncTests.cs @@ -1,4 +1,5 @@ using NUnit.Framework; + using static Jackett.Common.Utils.FilterFunc; namespace Jackett.Test.Common.Utils.FilterFuncs @@ -6,9 +7,9 @@ namespace Jackett.Test.Common.Utils.FilterFuncs [TestFixture] public class TypeFuncTests { - private class TypeIndexerStub : IndexerStub + private class IndexerStub : IndexerBaseStub { - public TypeIndexerStub(string type) + public IndexerStub(string type) { Type = type; } @@ -23,8 +24,8 @@ namespace Jackett.Test.Common.Utils.FilterFuncs { var typeId = "type-id"; - var lowerType = new TypeIndexerStub(typeId.ToLower()); - var upperType = new TypeIndexerStub(typeId.ToUpper()); + var lowerType = new IndexerStub(type: typeId.ToLower()); + var upperType = new IndexerStub(type: typeId.ToUpper()); var upperFilterFunc = Type.ToFunc(typeId.ToUpper()); Assert.IsTrue(upperFilterFunc(lowerType)); @@ -42,11 +43,9 @@ namespace Jackett.Test.Common.Utils.FilterFuncs var funcFilter = Type.ToFunc($"{typeId}"); - Assert.IsFalse(funcFilter(new TypeIndexerStub($"{typeId}suffix"))); - Assert.IsFalse(funcFilter(new TypeIndexerStub($"prefix{typeId}"))); - Assert.IsFalse(funcFilter(new TypeIndexerStub($"prefix{typeId}suffix"))); + Assert.IsFalse(funcFilter(new IndexerStub(type: $"{typeId}suffix"))); + Assert.IsFalse(funcFilter(new IndexerStub(type: $"prefix{typeId}"))); + Assert.IsFalse(funcFilter(new IndexerStub(type: $"prefix{typeId}suffix"))); } } - - }