mirror of
https://github.com/Jackett/Jackett.git
synced 2025-09-17 17:34:09 +02:00
[feature] Indexer status (#11706)
Co-authored-by: ilike2burnthing <59480337+ilike2burnthing@users.noreply.github.com>
This commit is contained in:
@@ -596,6 +596,7 @@ Filter | Condition
|
|||||||
`tag:<tag>` | where the indexer tags contains `<tag>`
|
`tag:<tag>` | where the indexer tags contains `<tag>`
|
||||||
`lang:<tag>` | where the indexer language start with `<lang>`
|
`lang:<tag>` | where the indexer language start with `<lang>`
|
||||||
`test:{passed\|failed}` | where the last indexer test performed `passed` or `failed`
|
`test:{passed\|failed}` | where the last indexer test performed `passed` or `failed`
|
||||||
|
`status:{healthy\|failing\|unknown}` | where the indexer state is `healthy` (succesfully operates in the last minutes), `failing` (generates errors in the recent call) or `unknown` (unused for a while)
|
||||||
|
|
||||||
Supported operators
|
Supported operators
|
||||||
Operator | Condition
|
Operator | Condition
|
||||||
@@ -604,9 +605,12 @@ Operator | Condition
|
|||||||
`<expr1>+<expr2>[+<expr3>...]` | where `<expr1>` and `<expr2>` [and `<expr3>`...]
|
`<expr1>+<expr2>[+<expr3>...]` | where `<expr1>` and `<expr2>` [and `<expr3>`...]
|
||||||
`<expr1>,<expr2>[,<expr3>...]` | where `<expr1>` or `<expr2>` [or `<expr3>`...]
|
`<expr1>,<expr2>[,<expr3>...]` | where `<expr1>` or `<expr2>` [or `<expr3>`...]
|
||||||
|
|
||||||
Example:
|
Example 1:
|
||||||
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`,...)
|
||||||
|
|
||||||
|
Example 2:
|
||||||
|
The "filter" indexer at `/api/v2.0/indexers/!status:failing,test:passed` will query all the configured indexers not `failing` or which `passed` its last test.
|
||||||
|
|
||||||
## Installation on Windows
|
## 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.
|
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.
|
||||||
|
|
||||||
|
@@ -36,6 +36,14 @@ namespace Jackett.Common.Indexers
|
|||||||
public virtual bool IsConfigured { get; protected set; }
|
public virtual bool IsConfigured { get; protected set; }
|
||||||
public virtual string[] Tags { get; protected set; }
|
public virtual string[] Tags { get; protected set; }
|
||||||
|
|
||||||
|
// https://github.com/Jackett/Jackett/issues/3292#issuecomment-838586679
|
||||||
|
private TimeSpan HealthyStatusValidity => cacheService.CacheTTL + cacheService.CacheTTL;
|
||||||
|
private static readonly TimeSpan ErrorStatusValidity = TimeSpan.FromMinutes(10);
|
||||||
|
private static readonly TimeSpan MaxStatusValidity = TimeSpan.FromDays(1);
|
||||||
|
|
||||||
|
private int errorCount;
|
||||||
|
private DateTime expireAt;
|
||||||
|
|
||||||
protected Logger logger;
|
protected Logger logger;
|
||||||
protected IIndexerConfigurationService configurationService;
|
protected IIndexerConfigurationService configurationService;
|
||||||
protected IProtectionService protectionService;
|
protected IProtectionService protectionService;
|
||||||
@@ -61,6 +69,10 @@ namespace Jackett.Common.Indexers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual bool IsHealthy => errorCount == 0 && expireAt > DateTime.Now;
|
||||||
|
public virtual bool IsFailing => errorCount > 0 && expireAt > DateTime.Now;
|
||||||
|
|
||||||
|
|
||||||
public abstract TorznabCapabilities TorznabCaps { get; protected set; }
|
public abstract TorznabCapabilities TorznabCaps { get; protected set; }
|
||||||
|
|
||||||
// standard constructor used by most indexers
|
// standard constructor used by most indexers
|
||||||
@@ -92,6 +104,8 @@ namespace Jackett.Common.Indexers
|
|||||||
{
|
{
|
||||||
CookieHeader = string.Empty;
|
CookieHeader = string.Empty;
|
||||||
IsConfigured = false;
|
IsConfigured = false;
|
||||||
|
errorCount = 0;
|
||||||
|
expireAt = DateTime.MinValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void SaveConfig() => configurationService.Save(this as IIndexer, configData.ToJson(protectionService, forDisplay: false));
|
public virtual void SaveConfig() => configurationService.Save(this as IIndexer, configData.ToJson(protectionService, forDisplay: false));
|
||||||
@@ -386,10 +400,14 @@ namespace Jackett.Common.Indexers
|
|||||||
results = FilterResults(query, results);
|
results = FilterResults(query, results);
|
||||||
results = FixResults(query, results);
|
results = FixResults(query, results);
|
||||||
cacheService.CacheResults(this, query, results.ToList());
|
cacheService.CacheResults(this, query, results.ToList());
|
||||||
|
errorCount = 0;
|
||||||
|
expireAt = DateTime.Now.Add(HealthyStatusValidity);
|
||||||
return new IndexerResult(this, results, false);
|
return new IndexerResult(this, results, false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
var delay = Math.Min(MaxStatusValidity.TotalSeconds, ErrorStatusValidity.TotalSeconds * Math.Pow(2, errorCount++));
|
||||||
|
expireAt = DateTime.Now.AddSeconds(delay);
|
||||||
throw new IndexerException(this, ex);
|
throw new IndexerException(this, ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -41,6 +41,8 @@ namespace Jackett.Common.Indexers
|
|||||||
bool IsConfigured { get; }
|
bool IsConfigured { get; }
|
||||||
|
|
||||||
string[] Tags { get; }
|
string[] Tags { get; }
|
||||||
|
bool IsHealthy { get; }
|
||||||
|
bool IsFailing { get; }
|
||||||
|
|
||||||
// Retrieved for starting setup for the indexer via web API
|
// Retrieved for starting setup for the indexer via web API
|
||||||
Task<ConfigurationData> GetConfigurationForSetup();
|
Task<ConfigurationData> GetConfigurationForSetup();
|
||||||
|
@@ -177,6 +177,8 @@ namespace Jackett.Common.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public TimeSpan CacheTTL => TimeSpan.FromSeconds(_serverConfig.CacheTtl);
|
||||||
|
|
||||||
private bool IsCacheEnabled()
|
private bool IsCacheEnabled()
|
||||||
{
|
{
|
||||||
if (!_serverConfig.CacheEnabled)
|
if (!_serverConfig.CacheEnabled)
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Jackett.Common.Indexers;
|
using Jackett.Common.Indexers;
|
||||||
using Jackett.Common.Models;
|
using Jackett.Common.Models;
|
||||||
@@ -11,5 +12,6 @@ namespace Jackett.Common.Services.Interfaces
|
|||||||
List<TrackerCacheResult> GetCachedResults();
|
List<TrackerCacheResult> GetCachedResults();
|
||||||
void CleanIndexerCache(IIndexer indexer);
|
void CleanIndexerCache(IIndexer indexer);
|
||||||
void CleanCache();
|
void CleanCache();
|
||||||
|
TimeSpan CacheTTL { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -17,10 +17,11 @@ namespace Jackett.Common.Utils
|
|||||||
public static readonly FilterFuncComponent Language = Component("lang", args => indexer => indexer.Language.StartsWith(args, StringComparison.InvariantCultureIgnoreCase));
|
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 Type = Component("type", args => indexer => string.Equals(indexer.Type, args, StringComparison.InvariantCultureIgnoreCase));
|
||||||
public static readonly FilterFuncComponent Test = TestFilterFunc.Default;
|
public static readonly FilterFuncComponent Test = TestFilterFunc.Default;
|
||||||
|
public static readonly FilterFuncComponent Status = StatusFilterFunc.Default;
|
||||||
|
|
||||||
static FilterFunc()
|
static FilterFunc()
|
||||||
{
|
{
|
||||||
Expression = new FilterFuncExpression(Tag, Language, Type, Test);
|
Expression = new FilterFuncExpression(Tag, Language, Type, Test, Status);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryParse(string source, out Func<IIndexer, bool> func)
|
public static bool TryParse(string source, out Func<IIndexer, bool> func)
|
||||||
|
30
src/Jackett.Common/Utils/FilterFuncs/StatusFilterFunc.cs
Normal file
30
src/Jackett.Common/Utils/FilterFuncs/StatusFilterFunc.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using Jackett.Common.Indexers;
|
||||||
|
|
||||||
|
namespace Jackett.Common.Utils.FilterFuncs
|
||||||
|
{
|
||||||
|
public class StatusFilterFunc : FilterFuncComponent
|
||||||
|
{
|
||||||
|
public static readonly StatusFilterFunc Default = new StatusFilterFunc();
|
||||||
|
public const string Healthy = "healthy";
|
||||||
|
public const string Failing = "failing";
|
||||||
|
public const string Unknown = "unknown";
|
||||||
|
|
||||||
|
private StatusFilterFunc() : base("status")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Func<IIndexer, bool> ToFunc(string args)
|
||||||
|
{
|
||||||
|
if (args == null)
|
||||||
|
throw new ArgumentNullException(nameof(args));
|
||||||
|
if (string.Equals(Healthy, args, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
return i => IsValid(i) && i.IsHealthy && !i.IsFailing;
|
||||||
|
if (string.Equals(Failing, args, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
return i => IsValid(i) && !i.IsHealthy && i.IsFailing;
|
||||||
|
if (string.Equals(Unknown, args, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
return i => IsValid(i) && ((!i.IsHealthy && !i.IsFailing) || (i.IsHealthy && i.IsFailing));
|
||||||
|
throw new ArgumentException($"Invalid filter. Status should be '{Healthy}', {Failing} or '{Unknown}'", nameof(args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -38,6 +38,10 @@ namespace Jackett.Test.Common.Utils.FilterFuncs
|
|||||||
|
|
||||||
public virtual string[] Tags => throw TestExceptions.UnexpectedInvocation;
|
public virtual string[] Tags => throw TestExceptions.UnexpectedInvocation;
|
||||||
|
|
||||||
|
public virtual bool IsHealthy => throw TestExceptions.UnexpectedInvocation;
|
||||||
|
|
||||||
|
public virtual bool IsFailing => throw TestExceptions.UnexpectedInvocation;
|
||||||
|
|
||||||
public virtual Task<ConfigurationData> GetConfigurationForSetup() => throw TestExceptions.UnexpectedInvocation;
|
public virtual Task<ConfigurationData> GetConfigurationForSetup() => throw TestExceptions.UnexpectedInvocation;
|
||||||
|
|
||||||
public virtual Task<IndexerConfigurationStatus> ApplyConfiguration(JToken configJson) => throw TestExceptions.UnexpectedInvocation;
|
public virtual Task<IndexerConfigurationStatus> ApplyConfiguration(JToken configJson) => throw TestExceptions.UnexpectedInvocation;
|
||||||
|
108
src/Jackett.Test/Common/Utils/FilterFuncs/StatusFuncTests.cs
Normal file
108
src/Jackett.Test/Common/Utils/FilterFuncs/StatusFuncTests.cs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
using System;
|
||||||
|
using Jackett.Common.Utils;
|
||||||
|
using Jackett.Common.Utils.FilterFuncs;
|
||||||
|
using NUnit.Framework;
|
||||||
|
|
||||||
|
namespace Jackett.Test.Common.Utils.FilterFuncs
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class StatusFuncTests
|
||||||
|
{
|
||||||
|
private static readonly IndexerStub HealthyIndexer = new IndexerStub(isHealthy: true, isFailing: false);
|
||||||
|
private static readonly IndexerStub FailingIndexer = new IndexerStub(isHealthy: false, isFailing: true);
|
||||||
|
private static readonly IndexerStub UnknownIndexer = new IndexerStub(isHealthy: false, isFailing: false);
|
||||||
|
private static readonly IndexerStub InvalidIndexer = new IndexerStub(isHealthy: true, isFailing: true);
|
||||||
|
|
||||||
|
private class IndexerStub : IndexerBaseStub
|
||||||
|
{
|
||||||
|
public IndexerStub(bool isHealthy, bool isFailing)
|
||||||
|
{
|
||||||
|
IsHealthy = isHealthy;
|
||||||
|
IsFailing = isFailing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool IsConfigured => true;
|
||||||
|
|
||||||
|
public override bool IsHealthy { get; }
|
||||||
|
public override bool IsFailing { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void NullStatus_ThrowsException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentNullException>(() => FilterFunc.Status.ToFunc(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void EmptyStatus_ThrowsException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(() => FilterFunc.Status.ToFunc(string.Empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void InvalidStatus_ThrowsException()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(() => FilterFunc.Status.ToFunc(StatusFilterFunc.Healthy + StatusFilterFunc.Failing));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void HealthyFilter()
|
||||||
|
{
|
||||||
|
var passedFilterFunc = FilterFunc.Status.ToFunc(StatusFilterFunc.Healthy);
|
||||||
|
Assert.IsTrue(passedFilterFunc(HealthyIndexer));
|
||||||
|
Assert.IsFalse(passedFilterFunc(FailingIndexer));
|
||||||
|
Assert.IsFalse(passedFilterFunc(UnknownIndexer));
|
||||||
|
Assert.IsFalse(passedFilterFunc(InvalidIndexer));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void FailingFilter()
|
||||||
|
{
|
||||||
|
var failingFilterFunc = FilterFunc.Status.ToFunc(StatusFilterFunc.Failing);
|
||||||
|
Assert.IsFalse(failingFilterFunc(HealthyIndexer));
|
||||||
|
Assert.IsTrue(failingFilterFunc(FailingIndexer));
|
||||||
|
Assert.IsFalse(failingFilterFunc(UnknownIndexer));
|
||||||
|
Assert.IsFalse(failingFilterFunc(InvalidIndexer));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void UnknownFilter()
|
||||||
|
{
|
||||||
|
var unknownFilterFunc = FilterFunc.Status.ToFunc(StatusFilterFunc.Unknown);
|
||||||
|
Assert.IsFalse(unknownFilterFunc(HealthyIndexer));
|
||||||
|
Assert.IsFalse(unknownFilterFunc(FailingIndexer));
|
||||||
|
Assert.IsTrue(unknownFilterFunc(UnknownIndexer));
|
||||||
|
Assert.IsTrue(unknownFilterFunc(InvalidIndexer));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void PassedFilter_CaseInsensitiveSource()
|
||||||
|
{
|
||||||
|
var upperFilterFunc = FilterFunc.Status.ToFunc(StatusFilterFunc.Healthy.ToUpper());
|
||||||
|
Assert.IsTrue(upperFilterFunc(HealthyIndexer));
|
||||||
|
|
||||||
|
var lowerFilterFunc = FilterFunc.Status.ToFunc(StatusFilterFunc.Healthy.ToLower());
|
||||||
|
Assert.IsTrue(lowerFilterFunc(HealthyIndexer));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void FailedFilter_CaseInsensitiveSource()
|
||||||
|
{
|
||||||
|
var upperFilterFunc = FilterFunc.Status.ToFunc(StatusFilterFunc.Failing.ToUpper());
|
||||||
|
Assert.IsTrue(upperFilterFunc(FailingIndexer));
|
||||||
|
|
||||||
|
var lowerFilterFunc = FilterFunc.Status.ToFunc(StatusFilterFunc.Failing.ToLower());
|
||||||
|
Assert.IsTrue(lowerFilterFunc(FailingIndexer));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void UnknownFilter_CaseInsensitiveSource()
|
||||||
|
{
|
||||||
|
var upperFilterFunc = FilterFunc.Status.ToFunc(StatusFilterFunc.Unknown.ToUpper());
|
||||||
|
Assert.IsTrue(upperFilterFunc(UnknownIndexer));
|
||||||
|
|
||||||
|
var lowerFilterFunc = FilterFunc.Status.ToFunc(StatusFilterFunc.Unknown.ToLower());
|
||||||
|
Assert.IsTrue(lowerFilterFunc(UnknownIndexer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user