New: Application Status Warnings

This commit is contained in:
Qstick
2021-02-10 21:44:14 -05:00
parent 96cf058017
commit ed0e11847a
33 changed files with 382 additions and 353 deletions

View File

@@ -61,9 +61,7 @@ class MovieIndexRow extends Component {
const { const {
id, id,
name, name,
enableRss, enable,
enableAutomaticSearch,
enableInteractiveSearch,
tags, tags,
protocol, protocol,
privacy, privacy,
@@ -114,7 +112,7 @@ class MovieIndexRow extends Component {
<IndexerStatusCell <IndexerStatusCell
key={column.name} key={column.name}
className={styles[column.name]} className={styles[column.name]}
enabled={enableRss || enableAutomaticSearch || enableInteractiveSearch} enabled={enable}
status={status} status={status}
longDateFormat={longDateFormat} longDateFormat={longDateFormat}
timeFormat={timeFormat} timeFormat={timeFormat}
@@ -255,9 +253,7 @@ MovieIndexRow.propTypes = {
privacy: PropTypes.string.isRequired, privacy: PropTypes.string.isRequired,
priority: PropTypes.number.isRequired, priority: PropTypes.number.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
enableRss: PropTypes.bool.isRequired, enable: PropTypes.bool.isRequired,
enableAutomaticSearch: PropTypes.bool.isRequired,
enableInteractiveSearch: PropTypes.bool.isRequired,
status: PropTypes.object, status: PropTypes.object,
capabilities: PropTypes.object.isRequired, capabilities: PropTypes.object.isRequired,
added: PropTypes.string.isRequired, added: PropTypes.string.isRequired,

View File

@@ -38,11 +38,8 @@ function EditIndexerModalContent(props) {
id, id,
implementationName, implementationName,
name, name,
enableRss, enable,
enableAutomaticSearch,
enableInteractiveSearch,
supportsRss, supportsRss,
supportsSearch,
fields, fields,
priority priority
} = item; } = item;
@@ -81,42 +78,14 @@ function EditIndexerModalContent(props) {
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormLabel>{translate('EnableRSS')}</FormLabel> <FormLabel>{translate('Enable')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="enableRss" name="enable"
helpTextWarning={supportsRss.value ? undefined : translate('RSSIsNotSupportedWithThisIndexer')} helpTextWarning={supportsRss.value ? undefined : translate('RSSIsNotSupportedWithThisIndexer')}
isDisabled={!supportsRss.value} isDisabled={!supportsRss.value}
{...enableRss} {...enable}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableAutomaticSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableAutomaticSearch"
helpText={supportsSearch.value ? translate('EnableAutomaticSearchHelpText') : undefined}
helpTextWarning={supportsSearch.value ? undefined : translate('EnableAutomaticSearchHelpTextWarning')}
isDisabled={!supportsSearch.value}
{...enableAutomaticSearch}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableInteractiveSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableInteractiveSearch"
helpText={supportsSearch.value ? translate('EnableInteractiveSearchHelpText') : undefined}
helpTextWarning={supportsSearch.value ? undefined : translate('EnableInteractiveSearchHelpTextWarning')}
isDisabled={!supportsSearch.value}
{...enableInteractiveSearch}
onChange={onInputChange} onChange={onInputChange}
/> />
</FormGroup> </FormGroup>

View File

@@ -65,11 +65,8 @@ class Indexer extends Component {
const { const {
id, id,
name, name,
enableRss, enable,
enableAutomaticSearch,
enableInteractiveSearch,
supportsRss, supportsRss,
supportsSearch,
priority, priority,
showPriority showPriority
} = this.props; } = this.props;
@@ -96,26 +93,12 @@ class Indexer extends Component {
<div className={styles.enabled}> <div className={styles.enabled}>
{ {
supportsRss && enableRss && supportsRss && enable &&
<Label kind={kinds.SUCCESS}> <Label kind={kinds.SUCCESS}>
RSS RSS
</Label> </Label>
} }
{
supportsSearch && enableAutomaticSearch &&
<Label kind={kinds.SUCCESS}>
{translate('AutomaticSearch')}
</Label>
}
{
supportsSearch && enableInteractiveSearch &&
<Label kind={kinds.SUCCESS}>
{translate('InteractiveSearch')}
</Label>
}
{ {
showPriority && showPriority &&
<Label kind={kinds.DEFAULT}> <Label kind={kinds.DEFAULT}>
@@ -123,7 +106,7 @@ class Indexer extends Component {
</Label> </Label>
} }
{ {
!enableRss && !enableAutomaticSearch && !enableInteractiveSearch && !enable &&
<Label <Label
kind={kinds.DISABLED} kind={kinds.DISABLED}
outline={true} outline={true}
@@ -157,9 +140,7 @@ class Indexer extends Component {
Indexer.propTypes = { Indexer.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
enableRss: PropTypes.bool.isRequired, enable: PropTypes.bool.isRequired,
enableAutomaticSearch: PropTypes.bool.isRequired,
enableInteractiveSearch: PropTypes.bool.isRequired,
supportsRss: PropTypes.bool.isRequired, supportsRss: PropTypes.bool.isRequired,
supportsSearch: PropTypes.bool.isRequired, supportsSearch: PropTypes.bool.isRequired,
onCloneIndexerPress: PropTypes.func.isRequired, onCloneIndexerPress: PropTypes.func.isRequired,

View File

@@ -142,9 +142,7 @@ export const reducers = createHandleActions({
[SELECT_INDEXER_SCHEMA]: (state, { payload }) => { [SELECT_INDEXER_SCHEMA]: (state, { payload }) => {
return selectSchema(state, payload, (selectedSchema) => { return selectSchema(state, payload, (selectedSchema) => {
selectedSchema.enableRss = selectedSchema.supportsRss; selectedSchema.enable = selectedSchema.supportsRss;
selectedSchema.enableAutomaticSearch = selectedSchema.supportsSearch;
selectedSchema.enableInteractiveSearch = selectedSchema.supportsSearch;
return selectedSchema; return selectedSchema;
}); });

View File

@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.Applications;
using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Localization;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.HealthCheck.Checks
{
[TestFixture]
public class ApplicationStatusCheckFixture : CoreTest<ApplicationStatusCheck>
{
private List<IApplication> _applications = new List<IApplication>();
private List<ApplicationStatus> _blockedApplications = new List<ApplicationStatus>();
[SetUp]
public void SetUp()
{
Mocker.GetMock<IApplicationFactory>()
.Setup(v => v.GetAvailableProviders())
.Returns(_applications);
Mocker.GetMock<IApplicationStatusService>()
.Setup(v => v.GetBlockedProviders())
.Returns(_blockedApplications);
Mocker.GetMock<ILocalizationService>()
.Setup(s => s.GetLocalizedString(It.IsAny<string>()))
.Returns("Some Warning Message");
}
private Mock<IApplication> GivenIndexer(int i, double backoffHours, double failureHours)
{
var id = i;
var mockIndexer = new Mock<IApplication>();
mockIndexer.SetupGet(s => s.Definition).Returns(new ApplicationDefinition { Id = id });
_applications.Add(mockIndexer.Object);
if (backoffHours != 0.0)
{
_blockedApplications.Add(new ApplicationStatus
{
ProviderId = id,
InitialFailure = DateTime.UtcNow.AddHours(-failureHours),
MostRecentFailure = DateTime.UtcNow.AddHours(-0.1),
EscalationLevel = 5,
DisabledTill = DateTime.UtcNow.AddHours(backoffHours)
});
}
return mockIndexer;
}
[Test]
public void should_not_return_error_when_no_indexers()
{
Subject.Check().ShouldBeOk();
}
[Test]
public void should_return_warning_if_indexer_unavailable()
{
GivenIndexer(1, 2.0, 4.0);
GivenIndexer(2, 0.0, 0.0);
Subject.Check().ShouldBeWarning();
}
[Test]
public void should_return_error_if_all_indexers_unavailable()
{
GivenIndexer(1, 2.0, 4.0);
Subject.Check().ShouldBeError();
}
[Test]
public void should_return_warning_if_few_indexers_unavailable()
{
GivenIndexer(1, 2.0, 4.0);
GivenIndexer(2, 2.0, 4.0);
GivenIndexer(3, 0.0, 0.0);
Subject.Check().ShouldBeWarning();
}
}
}

View File

@@ -21,7 +21,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
.Returns(new List<IIndexer>()); .Returns(new List<IIndexer>());
Mocker.GetMock<IIndexerFactory>() Mocker.GetMock<IIndexerFactory>()
.Setup(s => s.RssEnabled(It.IsAny<bool>())) .Setup(s => s.Enabled(It.IsAny<bool>()))
.Returns(new List<IIndexer>()); .Returns(new List<IIndexer>());
Mocker.GetMock<ILocalizationService>() Mocker.GetMock<ILocalizationService>()
@@ -43,14 +43,14 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks
private void GivenRssEnabled() private void GivenRssEnabled()
{ {
Mocker.GetMock<IIndexerFactory>() Mocker.GetMock<IIndexerFactory>()
.Setup(s => s.RssEnabled(It.IsAny<bool>())) .Setup(s => s.Enabled(It.IsAny<bool>()))
.Returns(new List<IIndexer> { _indexerMock.Object }); .Returns(new List<IIndexer> { _indexerMock.Object });
} }
private void GivenRssFiltered() private void GivenRssFiltered()
{ {
Mocker.GetMock<IIndexerFactory>() Mocker.GetMock<IIndexerFactory>()
.Setup(s => s.RssEnabled(false)) .Setup(s => s.Enabled(false))
.Returns(new List<IIndexer> { _indexerMock.Object }); .Returns(new List<IIndexer> { _indexerMock.Object });
Mocker.GetMock<ILocalizationService>() Mocker.GetMock<ILocalizationService>()

View File

@@ -1,135 +0,0 @@
using System.Collections.Generic;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.HealthCheck.Checks;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Localization;
using NzbDrone.Core.Test.Framework;
namespace NzbDrone.Core.Test.HealthCheck.Checks
{
[TestFixture]
public class IndexerSearchCheckFixture : CoreTest<IndexerSearchCheck>
{
private Mock<IIndexer> _indexerMock;
[SetUp]
public void SetUp()
{
Mocker.GetMock<IIndexerFactory>()
.Setup(s => s.GetAvailableProviders())
.Returns(new List<IIndexer>());
Mocker.GetMock<IIndexerFactory>()
.Setup(s => s.AutomaticSearchEnabled(It.IsAny<bool>()))
.Returns(new List<IIndexer>());
Mocker.GetMock<IIndexerFactory>()
.Setup(s => s.InteractiveSearchEnabled(It.IsAny<bool>()))
.Returns(new List<IIndexer>());
Mocker.GetMock<ILocalizationService>()
.Setup(s => s.GetLocalizedString(It.IsAny<string>()))
.Returns("Some Warning Message");
}
private void GivenIndexer(bool supportsRss, bool supportsSearch)
{
_indexerMock = Mocker.GetMock<IIndexer>();
_indexerMock.SetupGet(s => s.SupportsRss).Returns(supportsRss);
_indexerMock.SetupGet(s => s.SupportsSearch).Returns(supportsSearch);
Mocker.GetMock<IIndexerFactory>()
.Setup(s => s.GetAvailableProviders())
.Returns(new List<IIndexer> { _indexerMock.Object });
}
private void GivenAutomaticSearchEnabled()
{
Mocker.GetMock<IIndexerFactory>()
.Setup(s => s.AutomaticSearchEnabled(It.IsAny<bool>()))
.Returns(new List<IIndexer> { _indexerMock.Object });
}
private void GivenInteractiveSearchEnabled()
{
Mocker.GetMock<IIndexerFactory>()
.Setup(s => s.InteractiveSearchEnabled(It.IsAny<bool>()))
.Returns(new List<IIndexer> { _indexerMock.Object });
}
private void GivenSearchFiltered()
{
Mocker.GetMock<IIndexerFactory>()
.Setup(s => s.AutomaticSearchEnabled(false))
.Returns(new List<IIndexer> { _indexerMock.Object });
Mocker.GetMock<IIndexerFactory>()
.Setup(s => s.InteractiveSearchEnabled(false))
.Returns(new List<IIndexer> { _indexerMock.Object });
Mocker.GetMock<ILocalizationService>()
.Setup(s => s.GetLocalizedString(It.IsAny<string>()))
.Returns("recent indexer errors");
}
[Test]
public void should_return_warning_when_no_indexer_present()
{
Subject.Check().ShouldBeWarning();
}
[Test]
public void should_return_warning_when_no_search_supported_indexer_present()
{
GivenIndexer(true, false);
Subject.Check().ShouldBeWarning();
}
[Test]
public void should_return_ok_when_automatic_and__search_is_enabled()
{
GivenIndexer(false, true);
GivenAutomaticSearchEnabled();
GivenInteractiveSearchEnabled();
Subject.Check().ShouldBeOk();
}
[Test]
public void should_return_warning_when_only_automatic_search_is_enabled()
{
GivenIndexer(false, true);
GivenAutomaticSearchEnabled();
Subject.Check().ShouldBeWarning();
}
[Test]
public void should_return_warning_when_only_interactive_search_is_enabled()
{
GivenIndexer(false, true);
GivenInteractiveSearchEnabled();
Subject.Check().ShouldBeWarning();
}
[Test]
public void should_return_warning_if_search_is_supported_but_disabled()
{
GivenIndexer(false, true);
Subject.Check().ShouldBeWarning();
}
[Test]
public void should_return_filter_warning_if_search_is_enabled_but_filtered()
{
GivenIndexer(false, true);
GivenSearchFiltered();
Subject.Check().ShouldBeWarning("recent indexer errors");
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using NLog; using NLog;
using NzbDrone.Common.Composition; using NzbDrone.Common.Composition;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@@ -8,13 +9,53 @@ namespace NzbDrone.Core.Applications
{ {
public interface IApplicationFactory : IProviderFactory<IApplication, ApplicationDefinition> public interface IApplicationFactory : IProviderFactory<IApplication, ApplicationDefinition>
{ {
List<IApplication> SyncEnabled(bool filterBlockedIndexers = true);
} }
public class ApplicationFactory : ProviderFactory<IApplication, ApplicationDefinition>, IApplicationFactory public class ApplicationFactory : ProviderFactory<IApplication, ApplicationDefinition>, IApplicationFactory
{ {
public ApplicationFactory(IApplicationsRepository providerRepository, IEnumerable<IApplication> providers, IContainer container, IEventAggregator eventAggregator, Logger logger) private readonly IApplicationStatusService _applicationStatusService;
private readonly Logger _logger;
public ApplicationFactory(IApplicationStatusService applicationStatusService,
IApplicationsRepository providerRepository,
IEnumerable<IApplication> providers,
IContainer container,
IEventAggregator eventAggregator,
Logger logger)
: base(providerRepository, providers, container, eventAggregator, logger) : base(providerRepository, providers, container, eventAggregator, logger)
{ {
_applicationStatusService = applicationStatusService;
_logger = logger;
}
public List<IApplication> SyncEnabled(bool filterBlockedClients = true)
{
var enabledClients = GetAvailableProviders().Where(n => ((ApplicationDefinition)n.Definition).Enable);
if (filterBlockedClients)
{
return FilterBlockedApplications(enabledClients).ToList();
}
return enabledClients.ToList();
}
private IEnumerable<IApplication> FilterBlockedApplications(IEnumerable<IApplication> applications)
{
var blockedApplications = _applicationStatusService.GetBlockedProviders().ToDictionary(v => v.ProviderId, v => v);
foreach (var application in applications)
{
ApplicationStatus blockedApplicationStatus;
if (blockedApplications.TryGetValue(application.Definition.Id, out blockedApplicationStatus))
{
_logger.Debug("Temporarily ignoring application {0} till {1} due to recent failures.", application.Definition.Name, blockedApplicationStatus.DisabledTill.Value.ToLocalTime());
continue;
}
yield return application;
}
} }
} }
} }

View File

@@ -1,5 +1,8 @@
using System;
using System.Linq; using System.Linq;
using System.Net;
using NLog; using NLog;
using NzbDrone.Common.Http;
using NzbDrone.Core.Indexers; using NzbDrone.Core.Indexers;
using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Messaging.Commands;
using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Messaging.Events;
@@ -14,11 +17,13 @@ namespace NzbDrone.Core.Applications
IExecute<ApplicationIndexerSyncCommand> IExecute<ApplicationIndexerSyncCommand>
{ {
private readonly IApplicationFactory _applicationsFactory; private readonly IApplicationFactory _applicationsFactory;
private readonly IApplicationStatusService _applicationStatusService;
private readonly Logger _logger; private readonly Logger _logger;
public ApplicationService(IApplicationFactory applicationsFactory, Logger logger) public ApplicationService(IApplicationFactory applicationsFactory, IApplicationStatusService applicationStatusService, Logger logger)
{ {
_applicationsFactory = applicationsFactory; _applicationsFactory = applicationsFactory;
_applicationStatusService = applicationStatusService;
_logger = logger; _logger = logger;
} }
@@ -31,51 +36,103 @@ namespace NzbDrone.Core.Applications
{ {
var app = _applicationsFactory.GetInstance(appDefinition); var app = _applicationsFactory.GetInstance(appDefinition);
app.SyncIndexers(); ExecuteAction(a => a.SyncIndexers(), app);
} }
} }
public void HandleAsync(ProviderAddedEvent<IIndexer> message) public void HandleAsync(ProviderAddedEvent<IIndexer> message)
{ {
var enabledApps = _applicationsFactory.GetAvailableProviders() var enabledApps = _applicationsFactory.SyncEnabled();
.Where(n => ((ApplicationDefinition)n.Definition).Enable);
foreach (var app in enabledApps) foreach (var app in enabledApps)
{ {
app.AddIndexer((IndexerDefinition)message.Definition); ExecuteAction(a => a.AddIndexer((IndexerDefinition)message.Definition), app);
} }
} }
public void HandleAsync(ProviderDeletedEvent<IIndexer> message) public void HandleAsync(ProviderDeletedEvent<IIndexer> message)
{ {
var enabledApps = _applicationsFactory.GetAvailableProviders() var enabledApps = _applicationsFactory.SyncEnabled()
.Where(n => ((ApplicationDefinition)n.Definition).SyncLevel == ApplicationSyncLevel.FullSync); .Where(n => ((ApplicationDefinition)n.Definition).SyncLevel == ApplicationSyncLevel.FullSync);
foreach (var app in enabledApps) foreach (var app in enabledApps)
{ {
app.RemoveIndexer(message.ProviderId); ExecuteAction(a => a.RemoveIndexer(message.ProviderId), app);
} }
} }
public void HandleAsync(ProviderUpdatedEvent<IIndexer> message) public void HandleAsync(ProviderUpdatedEvent<IIndexer> message)
{ {
var enabledApps = _applicationsFactory.GetAvailableProviders() var enabledApps = _applicationsFactory.SyncEnabled()
.Where(n => ((ApplicationDefinition)n.Definition).SyncLevel == ApplicationSyncLevel.FullSync); .Where(n => ((ApplicationDefinition)n.Definition).SyncLevel == ApplicationSyncLevel.FullSync);
foreach (var app in enabledApps) foreach (var app in enabledApps)
{ {
app.UpdateIndexer((IndexerDefinition)message.Definition); ExecuteAction(a => a.UpdateIndexer((IndexerDefinition)message.Definition), app);
} }
} }
public void Execute(ApplicationIndexerSyncCommand message) public void Execute(ApplicationIndexerSyncCommand message)
{ {
var enabledApps = _applicationsFactory.GetAvailableProviders() var enabledApps = _applicationsFactory.SyncEnabled();
.Where(n => ((ApplicationDefinition)n.Definition).Enable);
foreach (var app in enabledApps) foreach (var app in enabledApps)
{ {
app.SyncIndexers(); ExecuteAction(a => a.SyncIndexers(), app);
}
}
private void ExecuteAction(Action<IApplication> applicationAction, IApplication application)
{
try
{
applicationAction(application);
_applicationStatusService.RecordSuccess(application.Definition.Id);
}
catch (WebException webException)
{
if (webException.Status == WebExceptionStatus.NameResolutionFailure ||
webException.Status == WebExceptionStatus.ConnectFailure)
{
_applicationStatusService.RecordConnectionFailure(application.Definition.Id);
}
else
{
_applicationStatusService.RecordFailure(application.Definition.Id);
}
if (webException.Message.Contains("502") || webException.Message.Contains("503") ||
webException.Message.Contains("timed out"))
{
_logger.Warn("{0} server is currently unavailable. {1}", this, webException.Message);
}
else
{
_logger.Warn("{0} {1}", this, webException.Message);
}
}
catch (TooManyRequestsException ex)
{
if (ex.RetryAfter != TimeSpan.Zero)
{
_applicationStatusService.RecordFailure(application.Definition.Id, ex.RetryAfter);
}
else
{
_applicationStatusService.RecordFailure(application.Definition.Id, TimeSpan.FromHours(1));
}
_logger.Warn("API Request Limit reached for {0}", this);
}
catch (HttpException ex)
{
_applicationStatusService.RecordFailure(application.Definition.Id);
_logger.Warn("{0} {1}", this, ex.Message);
}
catch (Exception ex)
{
_applicationStatusService.RecordFailure(application.Definition.Id);
_logger.Error(ex, "An error occurred while talking to application.");
} }
} }
} }

View File

@@ -0,0 +1,8 @@
using NzbDrone.Core.ThingiProvider.Status;
namespace NzbDrone.Core.Applications
{
public class ApplicationStatus : ProviderStatusBase
{
}
}

View File

@@ -0,0 +1,18 @@
using NzbDrone.Core.Datastore;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider.Status;
namespace NzbDrone.Core.Applications
{
public interface IApplicationStatusRepository : IProviderStatusRepository<ApplicationStatus>
{
}
public class ApplicationStatusRepository : ProviderStatusRepository<ApplicationStatus>, IApplicationStatusRepository
{
public ApplicationStatusRepository(IMainDatabase database, IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
}
}
}

View File

@@ -0,0 +1,22 @@
using System;
using NLog;
using NzbDrone.Common.EnvironmentInfo;
using NzbDrone.Core.Messaging.Events;
using NzbDrone.Core.ThingiProvider.Status;
namespace NzbDrone.Core.Applications
{
public interface IApplicationStatusService : IProviderStatusServiceBase<ApplicationStatus>
{
}
public class ApplicationStatusService : ProviderStatusServiceBase<IApplication, ApplicationStatus>, IApplicationStatusService
{
public ApplicationStatusService(IApplicationStatusRepository providerStatusRepository, IEventAggregator eventAggregator, IRuntimeInfo runtimeInfo, Logger logger)
: base(providerStatusRepository, eventAggregator, runtimeInfo, logger)
{
MinimumTimeSinceInitialFailure = TimeSpan.FromMinutes(5);
MaximumEscalationLevel = 5;
}
}
}

View File

@@ -76,7 +76,7 @@ namespace NzbDrone.Core.Applications.Lidarr
var indexers = _lidarrV1Proxy.GetIndexers(Settings); var indexers = _lidarrV1Proxy.GetIndexers(Settings);
//Pull all local indexers (TODO only those that support movie categories.) //Pull all local indexers (TODO only those that support movie categories.)
var prowlarrIndexers = _indexerFactory.GetAvailableProviders(); var prowlarrIndexers = _indexerFactory.Enabled();
//Pull mapping so we can check the mapping to see what already exists. //Pull mapping so we can check the mapping to see what already exists.
var indexerMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id); var indexerMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
@@ -107,9 +107,9 @@ namespace NzbDrone.Core.Applications.Lidarr
{ {
Id = 0, Id = 0,
Name = $"{indexer.Name} (Prowlarr)", Name = $"{indexer.Name} (Prowlarr)",
EnableRss = indexer.EnableRss, EnableRss = true,
EnableAutomaticSearch = indexer.EnableAutomaticSearch, EnableAutomaticSearch = true,
EnableInteractiveSearch = indexer.EnableInteractiveSearch, EnableInteractiveSearch = true,
Priority = indexer.Priority, Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab", Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract, ConfigContract = schema.ConfigContract,

View File

@@ -76,7 +76,7 @@ namespace NzbDrone.Core.Applications.Radarr
var indexers = _radarrV3Proxy.GetIndexers(Settings); var indexers = _radarrV3Proxy.GetIndexers(Settings);
//Pull all local indexers (TODO only those that support movie categories.) //Pull all local indexers (TODO only those that support movie categories.)
var prowlarrIndexers = _indexerFactory.GetAvailableProviders(); var prowlarrIndexers = _indexerFactory.Enabled();
//Pull mapping so we can check the mapping to see what already exists. //Pull mapping so we can check the mapping to see what already exists.
var indexerMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id); var indexerMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
@@ -107,9 +107,9 @@ namespace NzbDrone.Core.Applications.Radarr
{ {
Id = 0, Id = 0,
Name = $"{indexer.Name} (Prowlarr)", Name = $"{indexer.Name} (Prowlarr)",
EnableRss = indexer.EnableRss, EnableRss = true,
EnableAutomaticSearch = indexer.EnableAutomaticSearch, EnableAutomaticSearch = true,
EnableInteractiveSearch = indexer.EnableInteractiveSearch, EnableInteractiveSearch = true,
Priority = indexer.Priority, Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab", Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract, ConfigContract = schema.ConfigContract,

View File

@@ -76,7 +76,7 @@ namespace NzbDrone.Core.Applications.Readarr
var indexers = _readarrV1Proxy.GetIndexers(Settings); var indexers = _readarrV1Proxy.GetIndexers(Settings);
//Pull all local indexers (TODO only those that support movie categories.) //Pull all local indexers (TODO only those that support movie categories.)
var prowlarrIndexers = _indexerFactory.GetAvailableProviders(); var prowlarrIndexers = _indexerFactory.Enabled();
//Pull mapping so we can check the mapping to see what already exists. //Pull mapping so we can check the mapping to see what already exists.
var indexerMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id); var indexerMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
@@ -107,9 +107,9 @@ namespace NzbDrone.Core.Applications.Readarr
{ {
Id = 0, Id = 0,
Name = $"{indexer.Name} (Prowlarr)", Name = $"{indexer.Name} (Prowlarr)",
EnableRss = indexer.EnableRss, EnableRss = true,
EnableAutomaticSearch = indexer.EnableAutomaticSearch, EnableAutomaticSearch = true,
EnableInteractiveSearch = indexer.EnableInteractiveSearch, EnableInteractiveSearch = true,
Priority = indexer.Priority, Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab", Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract, ConfigContract = schema.ConfigContract,

View File

@@ -76,7 +76,7 @@ namespace NzbDrone.Core.Applications.Sonarr
var indexers = _sonarrV3Proxy.GetIndexers(Settings); var indexers = _sonarrV3Proxy.GetIndexers(Settings);
//Pull all local indexers (TODO only those that support movie categories.) //Pull all local indexers (TODO only those that support movie categories.)
var prowlarrIndexers = _indexerFactory.GetAvailableProviders(); var prowlarrIndexers = _indexerFactory.Enabled();
//Pull mapping so we can check the mapping to see what already exists. //Pull mapping so we can check the mapping to see what already exists.
var indexerMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id); var indexerMappings = _appIndexerMapService.GetMappingsForApp(Definition.Id);
@@ -107,9 +107,9 @@ namespace NzbDrone.Core.Applications.Sonarr
{ {
Id = 0, Id = 0,
Name = $"{indexer.Name} (Prowlarr)", Name = $"{indexer.Name} (Prowlarr)",
EnableRss = indexer.EnableRss, EnableRss = true,
EnableAutomaticSearch = indexer.EnableAutomaticSearch, EnableAutomaticSearch = true,
EnableInteractiveSearch = indexer.EnableInteractiveSearch, EnableInteractiveSearch = true,
Priority = indexer.Priority, Priority = indexer.Priority,
Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab", Implementation = indexer.Protocol == DownloadProtocol.Usenet ? "Newznab" : "Torznab",
ConfigContract = schema.ConfigContract, ConfigContract = schema.ConfigContract,

View File

@@ -0,0 +1,24 @@
using FluentMigrator;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(2)]
public class ApplicationStatus : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Delete.Column("EnableAutomaticSearch").FromTable("Indexers");
Delete.Column("EnableInteractiveSearch").FromTable("Indexers");
Rename.Column("EnableRss").OnTable("Indexers").To("Enable");
Create.TableForModel("ApplicationStatus")
.WithColumn("ProviderId").AsInt32().NotNullable().Unique()
.WithColumn("InitialFailure").AsDateTime().Nullable()
.WithColumn("MostRecentFailure").AsDateTime().Nullable()
.WithColumn("EscalationLevel").AsInt32().NotNullable()
.WithColumn("DisabledTill").AsDateTime().Nullable();
}
}
}

View File

@@ -40,7 +40,6 @@ namespace NzbDrone.Core.Datastore
Mapper.Entity<IndexerDefinition>("Indexers").RegisterModel() Mapper.Entity<IndexerDefinition>("Indexers").RegisterModel()
.Ignore(x => x.ImplementationName) .Ignore(x => x.ImplementationName)
.Ignore(i => i.Enable)
.Ignore(i => i.Protocol) .Ignore(i => i.Protocol)
.Ignore(i => i.Privacy) .Ignore(i => i.Privacy)
.Ignore(i => i.SupportsRss) .Ignore(i => i.SupportsRss)
@@ -69,6 +68,8 @@ namespace NzbDrone.Core.Datastore
Mapper.Entity<IndexerStatus>("IndexerStatus").RegisterModel(); Mapper.Entity<IndexerStatus>("IndexerStatus").RegisterModel();
Mapper.Entity<ApplicationStatus>("ApplicationStatus").RegisterModel();
Mapper.Entity<CustomFilter>("CustomFilters").RegisterModel(); Mapper.Entity<CustomFilter>("CustomFilters").RegisterModel();
} }

View File

@@ -0,0 +1,57 @@
using System;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Applications;
using NzbDrone.Core.Localization;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ProviderUpdatedEvent<IApplication>))]
[CheckOn(typeof(ProviderDeletedEvent<IApplication>))]
[CheckOn(typeof(ProviderStatusChangedEvent<IApplication>))]
public class ApplicationStatusCheck : HealthCheckBase
{
private readonly IApplicationFactory _providerFactory;
private readonly IApplicationStatusService _providerStatusService;
public ApplicationStatusCheck(IApplicationFactory providerFactory, IApplicationStatusService providerStatusService, ILocalizationService localizationService)
: base(localizationService)
{
_providerFactory = providerFactory;
_providerStatusService = providerStatusService;
}
public override HealthCheck Check()
{
var enabledProviders = _providerFactory.GetAvailableProviders();
var backOffProviders = enabledProviders.Join(_providerStatusService.GetBlockedProviders(),
i => i.Definition.Id,
s => s.ProviderId,
(i, s) => new { Provider = i, Status = s })
.Where(p => p.Status.InitialFailure.HasValue &&
p.Status.InitialFailure.Value.After(
DateTime.UtcNow.AddHours(-6)))
.ToList();
if (backOffProviders.Empty())
{
return new HealthCheck(GetType());
}
if (backOffProviders.Count == enabledProviders.Count)
{
return new HealthCheck(GetType(),
HealthCheckResult.Error,
_localizationService.GetLocalizedString("ApplicationStatusCheckAllClientMessage"),
"#applications-are-unavailable-due-to-failures");
}
return new HealthCheck(GetType(),
HealthCheckResult.Warning,
string.Format(_localizationService.GetLocalizedString("ApplicationStatusCheckSingleClientMessage"),
string.Join(", ", backOffProviders.Select(v => v.Provider.Definition.Name))),
"#applications-are-unavailable-due-to-failures");
}
}
}

View File

@@ -21,14 +21,14 @@ namespace NzbDrone.Core.HealthCheck.Checks
public override HealthCheck Check() public override HealthCheck Check()
{ {
var enabled = _indexerFactory.RssEnabled(false); var enabled = _indexerFactory.Enabled(false);
if (enabled.Empty()) if (enabled.Empty())
{ {
return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("IndexerRssHealthCheckNoIndexers")); return new HealthCheck(GetType(), HealthCheckResult.Error, _localizationService.GetLocalizedString("IndexerRssHealthCheckNoIndexers"));
} }
var active = _indexerFactory.RssEnabled(true); var active = _indexerFactory.Enabled(true);
if (active.Empty()) if (active.Empty())
{ {

View File

@@ -1,48 +0,0 @@
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Indexers;
using NzbDrone.Core.Localization;
using NzbDrone.Core.ThingiProvider.Events;
namespace NzbDrone.Core.HealthCheck.Checks
{
[CheckOn(typeof(ProviderAddedEvent<IIndexer>))]
[CheckOn(typeof(ProviderUpdatedEvent<IIndexer>))]
[CheckOn(typeof(ProviderDeletedEvent<IIndexer>))]
[CheckOn(typeof(ProviderStatusChangedEvent<IIndexer>))]
public class IndexerSearchCheck : HealthCheckBase
{
private readonly IIndexerFactory _indexerFactory;
public IndexerSearchCheck(IIndexerFactory indexerFactory, ILocalizationService localizationService)
: base(localizationService)
{
_indexerFactory = indexerFactory;
}
public override HealthCheck Check()
{
var automaticSearchEnabled = _indexerFactory.AutomaticSearchEnabled(false);
if (automaticSearchEnabled.Empty())
{
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("IndexerSearchCheckNoAutomaticMessage"));
}
var interactiveSearchEnabled = _indexerFactory.InteractiveSearchEnabled(false);
if (interactiveSearchEnabled.Empty())
{
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("IndexerSearchCheckNoInteractiveMessage"));
}
var active = _indexerFactory.AutomaticSearchEnabled(true);
if (active.Empty())
{
return new HealthCheck(GetType(), HealthCheckResult.Warning, _localizationService.GetLocalizedString("IndexerSearchCheckNoAvailableIndexersMessage"));
}
return new HealthCheck(GetType());
}
}
}

View File

@@ -129,9 +129,7 @@ namespace NzbDrone.Core.IndexerSearch
private List<ReleaseInfo> Dispatch(Func<IIndexer, IndexerPageableQueryResult> searchAction, SearchCriteriaBase criteriaBase) private List<ReleaseInfo> Dispatch(Func<IIndexer, IndexerPageableQueryResult> searchAction, SearchCriteriaBase criteriaBase)
{ {
var indexers = criteriaBase.InteractiveSearch ? var indexers = _indexerFactory.GetAvailableProviders();
_indexerFactory.InteractiveSearchEnabled() :
_indexerFactory.AutomaticSearchEnabled();
if (criteriaBase.IndexerIds != null && criteriaBase.IndexerIds.Count > 0) if (criteriaBase.IndexerIds != null && criteriaBase.IndexerIds.Count > 0)
{ {

View File

@@ -63,9 +63,7 @@ namespace NzbDrone.Core.Indexers.Cardigann
{ {
return new IndexerDefinition return new IndexerDefinition
{ {
EnableRss = false, Enable = true,
EnableAutomaticSearch = false,
EnableInteractiveSearch = false,
Name = definition.Name, Name = definition.Name,
Implementation = GetType().Name, Implementation = GetType().Name,
Settings = new CardigannSettings { DefinitionFile = definition.File }, Settings = new CardigannSettings { DefinitionFile = definition.File },

View File

@@ -91,9 +91,7 @@ namespace NzbDrone.Core.Indexers.Newznab
{ {
return new IndexerDefinition return new IndexerDefinition
{ {
EnableRss = false, Enable = true,
EnableAutomaticSearch = false,
EnableInteractiveSearch = false,
Name = name, Name = name,
Implementation = GetType().Name, Implementation = GetType().Name,
Settings = settings, Settings = settings,

View File

@@ -23,9 +23,7 @@ namespace NzbDrone.Core.Indexers.TorrentPotato
{ {
return new IndexerDefinition return new IndexerDefinition
{ {
EnableRss = false, Enable = true,
EnableAutomaticSearch = false,
EnableInteractiveSearch = false,
Name = name, Name = name,
Implementation = GetType().Name, Implementation = GetType().Name,
Settings = settings, Settings = settings,

View File

@@ -57,9 +57,7 @@ namespace NzbDrone.Core.Indexers.Torznab
{ {
return new IndexerDefinition return new IndexerDefinition
{ {
EnableRss = false, Enable = true,
EnableAutomaticSearch = false,
EnableInteractiveSearch = false,
Name = name, Name = name,
Implementation = GetType().Name, Implementation = GetType().Name,
Settings = settings, Settings = settings,

View File

@@ -50,9 +50,7 @@ namespace NzbDrone.Core.Indexers
yield return new IndexerDefinition yield return new IndexerDefinition
{ {
Name = GetType().Name, Name = GetType().Name,
EnableRss = config.Validate().IsValid && SupportsRss, Enable = config.Validate().IsValid && SupportsRss,
EnableAutomaticSearch = config.Validate().IsValid && SupportsSearch,
EnableInteractiveSearch = config.Validate().IsValid && SupportsSearch,
Implementation = GetType().Name, Implementation = GetType().Name,
Settings = config Settings = config
}; };

View File

@@ -7,9 +7,6 @@ namespace NzbDrone.Core.Indexers
{ {
public class IndexerDefinition : ProviderDefinition public class IndexerDefinition : ProviderDefinition
{ {
public bool EnableRss { get; set; }
public bool EnableAutomaticSearch { get; set; }
public bool EnableInteractiveSearch { get; set; }
public DownloadProtocol Protocol { get; set; } public DownloadProtocol Protocol { get; set; }
public IndexerPrivacy Privacy { get; set; } public IndexerPrivacy Privacy { get; set; }
public bool SupportsRss { get; set; } public bool SupportsRss { get; set; }
@@ -18,8 +15,6 @@ namespace NzbDrone.Core.Indexers
public int Priority { get; set; } = 25; public int Priority { get; set; } = 25;
public DateTime Added { get; set; } public DateTime Added { get; set; }
public override bool Enable => EnableRss || EnableAutomaticSearch || EnableInteractiveSearch;
public IndexerStatus Status { get; set; } public IndexerStatus Status { get; set; }
public List<SettingsField> ExtraFields { get; set; } = new List<SettingsField>(); public List<SettingsField> ExtraFields { get; set; } = new List<SettingsField>();

View File

@@ -14,9 +14,7 @@ namespace NzbDrone.Core.Indexers
{ {
public interface IIndexerFactory : IProviderFactory<IIndexer, IndexerDefinition> public interface IIndexerFactory : IProviderFactory<IIndexer, IndexerDefinition>
{ {
List<IIndexer> RssEnabled(bool filterBlockedIndexers = true); List<IIndexer> Enabled(bool filterBlockedIndexers = true);
List<IIndexer> AutomaticSearchEnabled(bool filterBlockedIndexers = true);
List<IIndexer> InteractiveSearchEnabled(bool filterBlockedIndexers = true);
void DeleteIndexers(List<int> indexerIds); void DeleteIndexers(List<int> indexerIds);
} }
@@ -165,33 +163,9 @@ namespace NzbDrone.Core.Indexers
} }
} }
public List<IIndexer> RssEnabled(bool filterBlockedIndexers = true) public List<IIndexer> Enabled(bool filterBlockedIndexers = true)
{ {
var enabledIndexers = GetAvailableProviders().Where(n => ((IndexerDefinition)n.Definition).EnableRss); var enabledIndexers = GetAvailableProviders().Where(n => ((IndexerDefinition)n.Definition).Enable);
if (filterBlockedIndexers)
{
return FilterBlockedIndexers(enabledIndexers).ToList();
}
return enabledIndexers.ToList();
}
public List<IIndexer> AutomaticSearchEnabled(bool filterBlockedIndexers = true)
{
var enabledIndexers = GetAvailableProviders().Where(n => ((IndexerDefinition)n.Definition).EnableAutomaticSearch);
if (filterBlockedIndexers)
{
return FilterBlockedIndexers(enabledIndexers).ToList();
}
return enabledIndexers.ToList();
}
public List<IIndexer> InteractiveSearchEnabled(bool filterBlockedIndexers = true)
{
var enabledIndexers = GetAvailableProviders().Where(n => ((IndexerDefinition)n.Definition).EnableInteractiveSearch);
if (filterBlockedIndexers) if (filterBlockedIndexers)
{ {

View File

@@ -35,6 +35,8 @@
"ApiKey": "API Key", "ApiKey": "API Key",
"AppDataDirectory": "AppData directory", "AppDataDirectory": "AppData directory",
"AppDataLocationHealthCheckMessage": "Updating will not be possible to prevent deleting AppData on Update", "AppDataLocationHealthCheckMessage": "Updating will not be possible to prevent deleting AppData on Update",
"ApplicationStatusCheckAllClientMessage": "All applications are unavailable due to failures",
"ApplicationStatusCheckSingleClientMessage": "Applications unavailable due to failures: {0}",
"Apply": "Apply", "Apply": "Apply",
"ApplyTags": "Apply Tags", "ApplyTags": "Apply Tags",
"ApplyTagsHelpTexts1": "How to apply tags to the selected movies", "ApplyTagsHelpTexts1": "How to apply tags to the selected movies",
@@ -185,8 +187,6 @@
"DownloadClients": "Download Clients", "DownloadClients": "Download Clients",
"DownloadClientSettings": "Download Client Settings", "DownloadClientSettings": "Download Client Settings",
"DownloadClientsSettingsSummary": "Download clients, download handling and remote path mappings", "DownloadClientsSettingsSummary": "Download clients, download handling and remote path mappings",
"DownloadClientStatusCheckAllClientMessage": "All download clients are unavailable due to failures",
"DownloadClientStatusCheckSingleClientMessage": "Download clients unavailable due to failures: {0}",
"DownloadClientUnavailable": "Download client is unavailable", "DownloadClientUnavailable": "Download client is unavailable",
"Downloaded": "Downloaded", "Downloaded": "Downloaded",
"DownloadedAndMonitored": "Downloaded and Monitored", "DownloadedAndMonitored": "Downloaded and Monitored",

View File

@@ -16,7 +16,7 @@ namespace NzbDrone.Integration.Test.ApiTests
indexers.Should().NotBeEmpty(); indexers.Should().NotBeEmpty();
indexers.Should().NotContain(c => string.IsNullOrWhiteSpace(c.Name)); indexers.Should().NotContain(c => string.IsNullOrWhiteSpace(c.Name));
indexers.Where(c => c.ConfigContract == typeof(NullConfig).Name).Should().OnlyContain(c => c.EnableRss); indexers.Where(c => c.ConfigContract == typeof(NullConfig).Name).Should().OnlyContain(c => c.Enable);
} }
} }
} }

View File

@@ -36,9 +36,7 @@ namespace NzbDrone.Integration.Test
{ {
Indexers.Post(new Prowlarr.Api.V1.Indexers.IndexerResource Indexers.Post(new Prowlarr.Api.V1.Indexers.IndexerResource
{ {
EnableRss = false, Enable = false,
EnableInteractiveSearch = false,
EnableAutomaticSearch = false,
ConfigContract = nameof(FileListSettings), ConfigContract = nameof(FileListSettings),
Implementation = nameof(FileList), Implementation = nameof(FileList),
Name = "NewznabTest", Name = "NewznabTest",

View File

@@ -10,9 +10,7 @@ namespace Prowlarr.Api.V1.Indexers
{ {
public class IndexerResource : ProviderResource public class IndexerResource : ProviderResource
{ {
public bool EnableRss { get; set; } public bool Enable { get; set; }
public bool EnableAutomaticSearch { get; set; }
public bool EnableInteractiveSearch { get; set; }
public bool SupportsRss { get; set; } public bool SupportsRss { get; set; }
public bool SupportsSearch { get; set; } public bool SupportsSearch { get; set; }
public DownloadProtocol Protocol { get; set; } public DownloadProtocol Protocol { get; set; }
@@ -51,9 +49,7 @@ namespace Prowlarr.Api.V1.Indexers
} }
} }
resource.EnableRss = definition.EnableRss; resource.Enable = definition.Enable;
resource.EnableAutomaticSearch = definition.EnableAutomaticSearch;
resource.EnableInteractiveSearch = definition.EnableInteractiveSearch;
resource.SupportsRss = definition.SupportsRss; resource.SupportsRss = definition.SupportsRss;
resource.SupportsSearch = definition.SupportsSearch; resource.SupportsSearch = definition.SupportsSearch;
resource.Capabilities = definition.Capabilities.ToResource(); resource.Capabilities = definition.Capabilities.ToResource();
@@ -88,9 +84,7 @@ namespace Prowlarr.Api.V1.Indexers
} }
} }
definition.EnableRss = resource.EnableRss; definition.Enable = resource.Enable;
definition.EnableAutomaticSearch = resource.EnableAutomaticSearch;
definition.EnableInteractiveSearch = resource.EnableInteractiveSearch;
definition.Priority = resource.Priority; definition.Priority = resource.Priority;
definition.Privacy = resource.Privacy; definition.Privacy = resource.Privacy;
definition.Added = resource.Added; definition.Added = resource.Added;