diff --git a/frontend/src/System/Status/Health/Health.js b/frontend/src/System/Status/Health/Health.js index f0e37a29c..0d6da7329 100644 --- a/frontend/src/System/Status/Health/Health.js +++ b/frontend/src/System/Status/Health/Health.js @@ -19,6 +19,7 @@ function getInternalLink(source) { case 'IndexerRssCheck': case 'IndexerSearchCheck': case 'IndexerStatusCheck': + case 'IndexerJackettAllCheck': case 'IndexerLongTermStatusCheck': return ( + { + private List _indexers = new List(); + private IndexerDefinition _definition; + + [SetUp] + public void SetUp() + { + Mocker.GetMock() + .Setup(v => v.All()) + .Returns(_indexers); + + Mocker.GetMock() + .Setup(s => s.GetLocalizedString(It.IsAny())) + .Returns("Some Warning Message"); + } + + private void GivenIndexer(string baseUrl, string apiPath) + { + var torznabSettings = new TorznabSettings + { + BaseUrl = baseUrl, + ApiPath = apiPath + }; + + _definition = new IndexerDefinition + { + Name = "Indexer", + ConfigContract = "TorznabSettings", + Settings = torznabSettings + }; + + _indexers.Add(_definition); + } + + [Test] + public void should_not_return_error_when_no_indexers() + { + Subject.Check().ShouldBeOk(); + } + + [TestCase("http://localhost:9117/", "api")] + public void should_not_return_error_when_no_jackett_all_indexers(string baseUrl, string apiPath) + { + GivenIndexer(baseUrl, apiPath); + + Subject.Check().ShouldBeOk(); + } + + [TestCase("http://localhost:9117/torznab/all/api", "api")] + [TestCase("http://localhost:9117/api/v2.0/indexers/all/results/torznab", "api")] + [TestCase("http://localhost:9117/", "/torznab/all/api")] + [TestCase("http://localhost:9117/", "/api/v2.0/indexers/all/results/torznab")] + public void should_return_warning_if_any_jackett_all_indexer_exists(string baseUrl, string apiPath) + { + GivenIndexer(baseUrl, apiPath); + + Subject.Check().ShouldBeWarning(); + } + } +} diff --git a/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs index 3f81cf6b1..fcf07dd6a 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/TorznabTests/TorznabFixture.cs @@ -1,7 +1,10 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Net.Http; +using FizzWare.NBuilder; using FluentAssertions; +using FluentValidation.Results; using Moq; using NUnit.Framework; using NzbDrone.Common.Http; @@ -10,6 +13,7 @@ using NzbDrone.Core.Indexers.Newznab; using NzbDrone.Core.Indexers.Torznab; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Validation; namespace NzbDrone.Core.Test.IndexerTests.TorznabTests { @@ -31,7 +35,11 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests } }; - _caps = new NewznabCapabilities(); + _caps = new NewznabCapabilities + { + Categories = Builder.CreateListOfSize(1).All().With(t => t.Id = 1).Build().ToList() + }; + Mocker.GetMock() .Setup(v => v.GetCapabilities(It.IsAny())) .Returns(_caps); @@ -104,5 +112,50 @@ namespace NzbDrone.Core.Test.IndexerTests.TorznabTests Subject.PageSize.Should().Be(25); } + + [TestCase("http://localhost:9117/", "/api")] + public void url_and_api_not_jackett_all(string baseUrl, string apiPath) + { + var setting = new TorznabSettings() + { + BaseUrl = baseUrl, + ApiPath = apiPath + }; + + setting.Validate().IsValid.Should().BeTrue(); + } + + [TestCase("http://localhost:9117/torznab/all/api")] + [TestCase("http://localhost:9117/api/v2.0/indexers/all/results/torznab")] + public void jackett_all_url_should_not_validate(string baseUrl) + { + var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_tpb.xml"); + (Subject.Definition.Settings as TorznabSettings).BaseUrl = baseUrl; + + Mocker.GetMock() + .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.Get))) + .Returns(r => new HttpResponse(r, new HttpHeader(), recentFeed)); + + var result = new NzbDroneValidationResult(Subject.Test()); + result.IsValid.Should().BeTrue(); + result.HasWarnings.Should().BeTrue(); + } + + [TestCase("/torznab/all/api")] + [TestCase("/api/v2.0/indexers/all/results/torznab")] + public void jackett_all_api_should_not_validate(string apiPath) + { + var recentFeed = ReadAllText(@"Files/Indexers/Torznab/torznab_tpb.xml"); + + Mocker.GetMock() + .Setup(o => o.Execute(It.Is(v => v.Method == HttpMethod.Get))) + .Returns(r => new HttpResponse(r, new HttpHeader(), recentFeed)); + + (Subject.Definition.Settings as TorznabSettings).ApiPath = apiPath; + + var result = new NzbDroneValidationResult(Subject.Test()); + result.IsValid.Should().BeTrue(); + result.HasWarnings.Should().BeTrue(); + } } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/IndexerJackettAllCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/IndexerJackettAllCheck.cs new file mode 100644 index 000000000..e73c066ac --- /dev/null +++ b/src/NzbDrone.Core/HealthCheck/Checks/IndexerJackettAllCheck.cs @@ -0,0 +1,45 @@ +using System; +using System.Linq; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Indexers; +using NzbDrone.Core.Indexers.Torznab; +using NzbDrone.Core.Localization; +using NzbDrone.Core.ThingiProvider.Events; + +namespace NzbDrone.Core.HealthCheck.Checks +{ + [CheckOn(typeof(ProviderUpdatedEvent))] + [CheckOn(typeof(ProviderDeletedEvent))] + [CheckOn(typeof(ProviderStatusChangedEvent))] + public class IndexerJackettAllCheck : HealthCheckBase + { + private readonly IIndexerFactory _providerFactory; + + public IndexerJackettAllCheck(IIndexerFactory providerFactory, ILocalizationService localizationService) + : base(localizationService) + { + _providerFactory = providerFactory; + } + + public override HealthCheck Check() + { + var jackettAllProviders = _providerFactory.All().Where( + i => i.ConfigContract.Equals("TorznabSettings") && + ((i.Settings as TorznabSettings).BaseUrl.Contains("/torznab/all/api", StringComparison.InvariantCultureIgnoreCase) || + (i.Settings as TorznabSettings).BaseUrl.Contains("/api/v2.0/indexers/all/results/torznab", StringComparison.InvariantCultureIgnoreCase) || + (i.Settings as TorznabSettings).ApiPath.Contains("/torznab/all/api", StringComparison.InvariantCultureIgnoreCase) || + (i.Settings as TorznabSettings).ApiPath.Contains("/api/v2.0/indexers/all/results/torznab", StringComparison.InvariantCultureIgnoreCase))); + + if (jackettAllProviders.Empty()) + { + return new HealthCheck(GetType()); + } + + return new HealthCheck(GetType(), + HealthCheckResult.Warning, + string.Format(_localizationService.GetLocalizedString("IndexerJackettAll"), + string.Join(", ", jackettAllProviders.Select(i => i.Name))), + "#jackett-all-endpoint-used"); + } + } +} diff --git a/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs b/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs index 70bfc450a..70ab6beb3 100644 --- a/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs +++ b/src/NzbDrone.Core/Indexers/Torznab/Torznab.cs @@ -77,6 +77,7 @@ namespace NzbDrone.Core.Indexers.Torznab return; } + failures.AddIfNotNull(JackettAll()); failures.AddIfNotNull(TestCapabilities()); } @@ -107,6 +108,23 @@ namespace NzbDrone.Core.Indexers.Torznab } } + protected virtual ValidationFailure JackettAll() + { + if (Settings.ApiPath.Contains("/torznab/all", StringComparison.InvariantCultureIgnoreCase) || + Settings.ApiPath.Contains("/api/v2.0/indexers/all/results/torznab", StringComparison.InvariantCultureIgnoreCase) || + Settings.BaseUrl.Contains("/torznab/all", StringComparison.InvariantCultureIgnoreCase) || + Settings.BaseUrl.Contains("/api/v2.0/indexers/all/results/torznab", StringComparison.InvariantCultureIgnoreCase)) + { + return new NzbDroneValidationFailure("ApiPath", "Jackett's all endpoint is not supported, please add indexers individually") + { + IsWarning = true, + DetailedDescription = "Jackett's all endpoint is not supported, please add indexers individually" + }; + } + + return null; + } + public override object RequestAction(string action, IDictionary query) { if (action == "newznabCategories") diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 0ee317fd6..048dcf7f3 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -194,6 +194,7 @@ "DownloadClientCheckNoneAvailableMessage": "No download client is available", "DownloadClients": "Download Clients", "DownloadClientSettings": "Download Client Settings", + "DownloadClientsSettingsSummary": "Download clients, download handling and remote path mappings", "DownloadClientStatusCheckAllClientMessage": "All download clients are unavailable due to failures", "DownloadFailedCheckDownloadClientForMoreDetails": "Download failed: check download client for more details", "DownloadFailedInterp": "Download failed: {0}", @@ -311,13 +312,14 @@ "IndexerIdHelpTextWarning": "Using a specific indexer with preferred words can lead to duplicate releases being grabbed", "IndexerIdvalue0IncludeInPreferredWordsRenamingFormat": "Include in {Preferred Words} renaming format", "IndexerIdvalue0OnlySupportedWhenIndexerIsSetToAll": "Only supported when Indexer is set to (All)", + "IndexerJackettAll": "Indexers using the unsupported Jackett 'all' endpoint: {0}", "IndexerLongTermStatusCheckAllClientMessage": "All indexers are unavailable due to failures for more than 6 hours", "IndexerPriority": "Indexer Priority", "IndexerRssHealthCheckNoIndexers": "No indexers available with RSS sync enabled, Readarr will not grab new releases automatically", "Indexers": "Indexers", "IndexerSearchCheckNoAutomaticMessage": "No indexers available with Automatic Search enabled, Readarr will not provide any automatic search results", "IndexerSettings": "Indexer Settings", - "IndexersSettingsSummary": "Download clients, download handling and remote path mappings", + "IndexersSettingsSummary": "Indexers and release restrictions", "IndexerStatusCheckAllClientMessage": "All indexers are unavailable due to failures", "Interval": "Interval", "ISBN": "ISBN",